C++相關知識彙總
(20210822更新)
Q1:C++的三個特性是什麼?怎麼理解?
Q2:多型的底層是怎麼實現的?
Q3:建構函式裡可以呼叫虛擬函式嗎?
Q4:建構函式可以是虛擬函式嗎?
Q5:靜態函式可以是虛擬函式嗎?
Q6:虛擬函式的安全性有什麼問題嗎?
Q7:解構函式可以是虛擬函式嗎?
Q8:如果解構函式不是虛擬函式,一定會發生記憶體洩漏麼?
Q9:過載、隱藏、重寫(覆蓋)三者的區別是什麼?
Q10:純虛擬函式和虛擬函式的區別是什麼?
Q11:友元函式可以是虛擬函式嗎?
Q12:一個C++編譯的程式,記憶體分為哪幾個部分?
Q13:堆和棧的區別有哪些?
Q14:new-delete與malloc-free的區別是什麼?
Q15:new operator和operator new的區別是什麼?placement new知道麼?
Q16:什麼是深複製什麼是淺複製?
Q17:記憶體洩漏是如何引發的?怎麼處理?
Q18:智慧指標都有哪幾種?
Q19:程序、執行緒、纖程有什麼區別?
Q20:執行緒間同步有哪些方式?
Q21:程序間通訊有哪些方式?
Q22:簡述對TCP/UDP協議的理解
Q23:TCP協議為什麼要三次握手/四次分手?
Q24:簡述一次http請求的完整過程
Q25:指標和引用的區別是什麼?
Q26:C++容器都有哪些?底層是怎麼實現的?
Q27:什麼是紅黑樹?
Q28:為什麼map底層要用紅黑樹實現而不用AVL實現?
Q29:C++有哪些新特性?
Q30:static變數的意義和記憶體分配是怎樣的?static靜態成員函式呢?
Q31:頂層const和底層const是什麼意思?
Q32:volatile關鍵字的意義是什麼?
Q33:四種類型轉換符旨在解決什麼問題?
Q34:什麼是C++的RTTI機制?
Q35:class類的記憶體是怎樣分佈的?
Q36:設計模式有哪些?是如何實現的?
Q37:常見的IO模型有哪些?
——————————————————————我是分割線—————————————————————————
Q1:C++的三個特性是什麼?怎麼理解?
A1:封裝、繼承、多型。
封裝:在面向物件的思想中,將資料和對資料的操作包裝在一個類裡,只對外開放介面將內部細節隱藏,達到資料的抽象性、隱藏性、封裝性。
繼承:保持原有的特性基礎上進行擴充套件,是設計層面的複用。
多型:同一個名字的函式可以有不同的功能。編譯時的多型:函式的過載,函式名相同但引數列表不同。子類物件引用自身類例項的方法也是編譯時多型。執行時的多型:存在繼承關係的類,通過父類的指標或引用,呼叫了一個在父類中是virtual型別的函式,實現動態繫結機制。要求:①父類函式必須為虛擬函式;②子類重寫函式名稱、列表、返回值和父類均相同。
Q2:多型的底層是怎麼實現的?
https://coolshell.cn/articles/12165.html#%E5%AE%89%E5%85%A8%E6%80%A7
A2:編譯時多型:C++的編譯器編譯後的函式名會含有引數列表,保證根據傳參不同調用不同的過載函式。(如果不想讓C++編譯器含引數列表編譯,就加上extern C,會按照C來編譯,就不能實現多型了,很多時候用C++呼叫C的標頭檔案,是因為編譯方式不同導致找不到函式)
執行時多型:基類會在編譯階段形成虛擬函式表,放在記憶體的.rdata只讀資料段。類結構的前四個位元組是指向虛擬函式表的指標,派生類會繼承父類的虛擬函式表,如果是繼承多個父類就是繼承多個虛擬函式表。如果是重寫,那麼派生類的重寫後函式會覆蓋派生類虛擬函式表中的基類虛擬函式,如果不是重寫,則會在派生類虛擬函式表後面追加派生類的函式地址。
Q3:建構函式裡可以呼叫虛擬函式嗎?
A3:可以但沒有意義。派生類要等父類構造完才能構造,C++規定為了避免記憶體異常在父類構造的時候,虛建構函式會被當做普通函式不管派生類裡是否重寫。
Q4:建構函式可以是虛擬函式嗎?
A4:不可以。使用虛擬函式的目的是多型,建構函式是用來構造所在類的物件,不會用於構造派生類物件,不會被繼承。另外,建構函式中會產生虛擬函式表,如果建構函式本身就是虛擬函式,虛擬函式表還沒有產生,虛擬函式地址無法存放。
Q5:靜態函式可以是虛擬函式嗎?
A5:不可以。static成員不屬於任何類物件或類例項,static成員也無法訪問this指標,訪問不了虛擬函式表。
Q6:虛擬函式的安全性有什麼問題嗎?
A6:虛擬函式本質是通過指標訪問虛擬函式表裡的虛擬函式,通過改變指標的地址父類指標也可以訪問子類的自有虛擬函式,另外,即便是父類的虛擬函式是私有函式or保護函式,通過改變指標的地址也可以被訪問,二者都會產生安全問題。
Q7:解構函式可以是虛擬函式嗎?
A7:解構函式必須是虛擬函式!編譯器是通過指標的型別來析構的,子類在析構的時候需要用子類的解構函式,如果父類的解構函式不是虛擬函式,那通過父類指標析構的子類物件將以父類的解構函式來析構子類。
Q8:如果解構函式不是虛擬函式,一定會發生記憶體洩漏麼?(本題答案待考證)
A8:不一定。如果子類沒有動態分配記憶體,那麼棧上的記憶體會自動釋放,不會產生記憶體洩漏。Q9:過載、隱藏、重寫(覆蓋)三者的區別是什麼?
A9:
過載:是指同一可訪問區內被宣告的幾個具有不同引數列表(引數的型別,個數,順序不同)的同名函式,根據引數列表確定呼叫哪個函式,過載不關心函式返回型別(統一為void,否則報錯)。
隱藏:是指派生類的函式遮蔽了與其同名的基類函式,注意只要同名函式,不管引數列表是否相同,基類函式都會被隱藏。
重寫(覆蓋):是指派生類中存在重新定義的函式。其函式名,引數列表,返回值型別,所有都必須同基類中被重寫的函式一致。只有函式體不同(花括號內),派生類呼叫時會呼叫派生類的重寫函式,不會呼叫被重寫函式。重寫的基類中被重寫的函式必須有virtual修飾。
Q10:純虛擬函式和虛擬函式的區別是什麼?
A10:純虛擬函式是沒有被定義的函式,在基類裡只進行了宣告,必須要在派生類裡實現才可以使用。普通的虛擬函式可以在子類中被重寫使用,如果被重寫,那麼基類指標會按照產生的例項來呼叫相應的子類重寫函式,如果沒有被重寫,那麼基類指標呼叫的是自身的虛擬函式。
Q11:友元函式可以是虛擬函式嗎?
A11:友元不是成員函式,只有成員函式才可以是虛擬的,因此友元函式不能是虛擬函式。但可以通過讓友元函式呼叫虛成員函式來解決友元的虛擬問題。
Q12:一個C++編譯的程式,記憶體分為哪幾個部分?
A12:可以分為棧區、堆區、全域性區、常量區和程式碼區。
棧區(stack):由編譯器自動分配與釋放,存放為執行時函式分配的區域性變數、函式引數、返回資料、返回地址等。其操作類似於資料結構中的棧。
堆區(heap):一般由程式設計師分配,如果程式設計師沒有釋放,程式結束時可能有OS回收。其分配類似於連結串列。
全域性區(靜態區static):存放全域性變數、靜態資料、常量。程式結束後由系統釋放。全域性區分為已初始化全域性區(data)和未初始化全域性區(bss)。
常量區(文字常量區):存放常量字串,程式結束後由系統釋放。
程式碼區(自由儲存區):存放函式體(類成員函式和全域性區)的二進位制程式碼。
Q13:堆和棧的區別有哪些?
A13:
1.堆和棧的生長方向不同
對於堆來講,生長方向是向上的,也就是向著記憶體地址增加的方向;對於棧來講,它的生長方向是向下的,是向著記憶體地址減小的方向增長。
2.堆會隨使用產生碎片,棧不會
對於堆來講,頻繁的new/delete勢必會造成記憶體空間的不連續,從而造成大量的碎片,使程式效率降低。對於棧來講,則不會存在這個問題,因為棧是先進後出的佇列
3.管理方式不同
對於棧來講,是由編譯器自動管理,無需我們手工控制;對於堆來說,釋放工作由程式設計師控制,容易產生memory leak。
4.堆未及時釋放容易造成記憶體洩漏
5.堆都是動態分配的,棧可以動態分配可以靜態分配
靜態分配是編譯器完成的,比如區域性變數的分配。動態分配由alloca函式進行分配,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行釋放,無需我們手工實現。
6.分配效率
棧是機器系統提供的資料結構,計算機會在底層對棧提供支援:分配專門的暫存器存放棧的地址,壓棧出棧都有專門的指令執行,因此棧的效率比較高。
堆是C/C++函式庫提供的,它的機制是很複雜的,例如為了分配一塊記憶體,庫函式會按照一定的演算法(具體的演算法可以參考資料結構/作業系統)在堆記憶體中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於記憶體碎片太多),就有可能呼叫系統功能去增加程式資料段的記憶體空間,這樣就有機會分到足夠大小的記憶體,然後進行返回。顯然,堆的效率比棧要低得多。
7.應用場景
堆和棧相比,由於大量new/delete的使用,容易造成大量的記憶體碎片;由於沒有專門的系統支援,效率很低;由於可能引發使用者態和核心態的切換,記憶體的申請,代價變得更加昂貴。所以棧在程式中是應用最廣泛的,就算是函式的呼叫也利用棧去完成,函式呼叫過程中的引數,返回地址,EBP和區域性變數都採用棧的方式存放。所以,程式開發時應儘可能使用棧。
雖然棧有如此眾多的好處,但是由於和堆相比不是那麼靈活,有時候分配大量的記憶體空間,還是用堆好一些。
Q14:new-delete與malloc-free的區別是什麼?
A14:new-delete是運算子,而malloc-free是庫函式,malloc-free不能進行構造和析構。
new-delete的功能可以完全覆蓋malloc-free,但由於C++會呼叫C函式,而C只能使用malloc-free,因此保留了malloc-free。很多編譯器的new/delete都是以malloc/free為基礎來實現的,藉以malloc實現的new。
malloc-free |
new-delete |
|
記憶體分配位置 |
堆 |
自由儲存區 |
分配成功返回值 |
void* |
對應型別指標 |
分配失敗返回值 |
NULL |
丟擲異常 |
分配記憶體大小 |
使用者計算 |
自動 |
處理陣列 |
× |
√ |
已分配記憶體的擴充 |
√ |
× |
互相呼叫 |
× |
√ |
分配記憶體時記憶體不足 |
使用者不可處理 |
使用者可重新申請 |
過載 |
× |
√ |
呼叫構造/析構 |
× |
√ |
Q15:new operator和operator new的區別是什麼?placement new知道麼?
A15:new就是new operator,使用new建立新物件的時候進行了三件事:①使用operator new分配記憶體,底層呼叫malloc實現;②呼叫建構函式;③返回相應型別的指標。
placement new保持一塊記憶體,反覆構造析構,這樣可以省略中間的多次分配記憶體,如果想自己管理記憶體可以使用。
Q16:什麼是深複製什麼是淺複製?
A16:複製建構函式這種,只複製了指標沒有開闢新的記憶體的複製是淺複製,而開闢記憶體將原物件的相關值複製到新的記憶體中的複製是深複製。當進行淺複製,其中一個物件析構,淺複製過來的指標所指的記憶體空間也會被釋放,此時如果有其他使用該記憶體的物件將會引發錯誤。解決方法是定義開闢空間的建構函式進行深拷貝。
Q17:記憶體洩漏是如何引發的?怎麼處理?
A17:記憶體洩漏源於new出的記憶體沒有全部被delete掉導致的記憶體消耗,使用智慧指標能將記憶體洩漏風險降至最低。智慧指標利用C++ RAII(Resource Acquisition Is Initialization,資源獲取就是初始化)原則。RAII的做法是使用一個物件,在其構造時獲取對應的資源,在物件生命期內控制對資源的訪問,使之始終保持有效,最後在物件析構的時候,釋放構造時獲取的資源。(std::mutex互斥鎖就是用的這個原則,退出區域性變數使用域的時候自動釋放鎖)
Q18:智慧指標都有哪幾種?
A18:
1.auto_ptr(舊)--> unique_ptr(C++11)
在構造的時候獲取資源,在析構的時候釋放資源,並進行相關指標操作的過載,使用起來就像普通的指標。獨享資源所有權。由於其建構函式宣告為explicit的,因此不能通過隱式轉換來構造,只能顯示呼叫建構函式。
2.share_ptr(C++11)
允許限定的資源被多個指標共享。
3.weak_ptr
weak_ptr是一種用於解決shared_ptr相互引用時產生死鎖問題的智慧指標。如果有兩個shared_ptr相互引用,那麼這兩個shared_ptr指標的引用計數永遠不會下降為0,資源永遠不會釋放。weak_ptr是對物件的一種弱引用,它不會增加物件的use_count,weak_ptr和shared_ptr可以相互轉化,shared_ptr可以直接賦值給weak_ptr,weak_ptr也可以通過呼叫lock函式來獲得shared_ptr。
Q19:程序、執行緒、纖程有什麼區別?
A19:程序是CPU的基本工作單元,按照就緒、執行、阻塞三種狀態進行程式處理。執行緒是CPU的基本排程單元,程序是執行緒的容器,在CPU切換到基本的程序單元后,程序想提高CPU的利用率,可以採用多執行緒,分食CPU的時間分片。纖程又名協程,是由程式控制的使用更小時間分片的工作單位,在go和lua上能夠比較好的實現,python也有關鍵字支援,Java的Kilim框架模擬出了纖程的功能,C++貌似還不太方便。
Q20:執行緒間同步有哪些方式?
A20:互斥鎖、條件變數、讀寫鎖、訊號量。
Q21:程序間通訊有哪些方式?
A21:管道(父子程序)、有名管道、訊號、訊號量、訊息佇列、共享記憶體、套接字。
Q22:簡述對TCP/UDP協議的理解(TCP-reno/cubic)
A22:TCP,Transmission Control Protocol,傳輸控制協議。面向連線、可靠、基於位元組流。犧牲效率保證傳輸的正確,應用層:FTP、HTTP、SMTP、POP3等協議為了保證傳輸正確都基於TCP協議。
UDP,User Datagram Protocol,使用者資料報協議。無連線、不可靠、基於報文。沒有可靠性保證但效率非常高,通常用於實時資料傳輸,應用層:NFS、TFTP、DHCP、DNS等協議為了能更快傳輸都基於UDP協議。基於UDP的檔案傳輸系統僅適用於傳輸小檔案,因為大檔案有丟失的風險。
Q23:TCP協議為什麼要三次握手/四次分手?
A23:三次握手是為了上下行通道確認,四次分手同理的同時還要避免有一方仍要傳送資料。
Q24:簡述一次http請求的完整過程
A24:(1)域名解析。瀏覽器先查詢是否有網址對映關係,查詢流程如下:本地host檔案àDNS解析器快取à本地DNS伺服器à上一層DNS伺服器à一直向上轉發知道查詢到相應的IPà結果返回本地DNS伺服器à返回客戶機。(2)定址。ARP協議廣播尋找目的IP的下一跳,由存在匹配IP(IP+掩碼)的裝置返回MAC地址,作為路徑的下一跳快取在裝置的路由表中。(3)TCP協議三次握手。(4)傳送http訊息。(5)http伺服器返回響應(頁面檔案)。(6)TCP四次揮手。
Q25:指標和引用的區別是什麼?
A25:指標是指向物件的地址,引用是物件本身。對指標進行處理(++/--等)是改變指標指向,而對引用進行處理是改變物件的值。
Q26:C++容器都有哪些?底層是怎麼實現的?
A26:基本容器有vector,deque,list,map,set等。
vector:動態陣列,空間連續,如果擴容一般按照50%的空間擴容,之後全部拷貝過去,所以如果不停地需要擴容效率就很低。查詢因為是可以索引,效率很高。vector類中是按照first、last、end三個指標實現的,myfirst是容器/元素頂部,mylast是元素尾部,myend是容器尾部,如果元素數量超過end就需要擴容。擴容的事情,如果你提前知道大概要多大的空間,可以直接先用reserve預留出來,省的每次擴容都要拷貝。
deque:deque中的每一段連續空間分佈在記憶體的不連續空間上,然後用一個所謂的map作為主控,記錄每一段記憶體空間的入口,從而做到整體連續的假象。
list:雙向連結串列,插入簡單,沒有空間預留,即使即分配,查詢效率低。
set:底層是紅黑樹,可以自動排序,插入刪除時需要點時間整理樹。
map:底層紅黑樹,key值自動排序,插入需要整理紅黑樹。
Q27:什麼是紅黑樹?
A27:紅黑樹是為了彌補二分搜尋樹多次單邊插入導致不平衡而引入的解決辦法。
紅黑樹首先是一個平衡二叉樹,滿足根節點大於左子樹小於右子樹。
紅黑樹特點:
1.根節點和葉子節點(都是空節點)都是黑色。
2.每個紅色節點的兩個子節點都是黑色。
3.從任一節點到其每個葉子的所有路徑都包含相同數目的黑色節點。
4.每次插入和刪除都通過變色和旋轉來保證紅黑樹符合上面的規則。
Q28:為什麼map底層要用紅黑樹實現而不用AVL實現?
A28:AVL樹是高度平衡,而紅黑樹通過增加節點顏色從而實現部分平衡,這就導致,插入節點兩者都可以最多兩次實現復衡,而刪除節點,紅黑樹最多三次旋轉即可實現復衡,旋轉的量級是O(1),而avl樹需要維護從被刪除節點到根節點這幾個節點的平衡,旋轉的量級是O(logn),所以紅黑樹效率更高,開銷更小,但是因為紅黑樹是非嚴格平衡,所以它的查詢效率比avl樹低。RB-Tree是功能、效能、空間開銷的折中結果。
Q29:C++有哪些新特性?
A29:右值引用,移動語義,完美轉發,智慧指標,lambda 匿名函式
Q30:static變數的意義和記憶體分配是怎樣的?static靜態成員函式呢?
A30:static變數是為了讓一個類的所有物件能夠共享資料,static靜態變數在類體外進行初始化,當執行到相關語句時才進行記憶體空間的分配,記憶體空間在全域性區,作用域為本檔案。static靜態函式只能用於訪問static變數和其他static函式,即便沒有建立物件,static靜態成員函式都可以被呼叫。
Q31:頂層const和底層const是什麼意思?
A31:const在*的左邊是底層const,例如:int iTmp=10;const int *a=&iTmp;(或者int const *a=&iTmp,這兩種表述一樣,只看const和*的相對位置)代表指標a所指的值10是不可以被改變的,可以使用iTmp=9,但是不可以*a=9.但是指標指向可以改變,例如:int iTmp2=9;a=&iTmp2;
const在*的右邊是頂層const,例如:int iTmp=10;int * const a=&iTmp;代表指標的指向不能改變,但是可以通過指標修改指標所指的值。
Q32:volatile關鍵字的意義是什麼?
A32:多執行緒程式中當多個執行緒都會更改某一個變數,應該用volatile宣告,保證每次資料的更改都是從記憶體同步的,而不是從暫存器中取出來的。
Q33:四種類型轉換符旨在解決什麼問題?
A33:為了解決C風格的轉換隨意和提供關鍵字區分,C++設立了四種類型轉換符:
static_cast<newType>(data);①近似型別轉換,intàdouble,shortàint,constà非const,向上轉型(派生類à基類)②void*轉型(malloc),void *àint *,char *àvoid*等。③【這裡沒看懂】有轉換建構函式或者型別轉換函式的類與其它型別之間的轉換,例如 double 轉 Complex(呼叫轉換建構函式)、Complex 轉 double(呼叫型別轉換函式)。
const_cast<newType>(data);用於const/volatileà非const/volatile
reinterpret_cast<newType>(data);不相關的型別轉換,從底層對資料進行重新解釋,高危操作。
dynamic_cast<newType>(data);既可以向上轉型(派生類à基類)又可以向下轉型(基類à派生類),向下轉型需要藉助RTTI機制檢測,必須是安全的才能成功。
Q34:什麼是C++的RTTI機制?
A34:RTTI:Run Time Type Identification,即執行時型別識別。比如執行時,要根據使用者的輸入選擇父類指標的指向,輸入小於100指標指向父類函式,大於等於100指向子類函式。在 C++ 中,只有類中包含了虛擬函式時才會啟用 RTTI 機制,其他所有情況都可以在編譯階段確定型別資訊。
Q35:class類的記憶體是怎樣分佈的?
A35:1.如果例項化一個空類,記憶體中只佔用一個位元組,作為標識。
2.包含成員變數不包含成員函式時,根據記憶體對齊原則,將各個成員變數分佈進記憶體。
3.就算包括成員函式,成員函式是不佔用類的記憶體空間的,所以此時類的記憶體空間仍然和只包含成員變數的時候一樣。而成員函式因為是公共的,一個類只有一份,一般根據編譯器的不同儲存在程式碼區或者只讀區。
4.如果有虛擬函式的話,前四個位元組會有一個虛擬函式表指標指向虛擬函式。(上一節面經有講到),注意子類的虛擬函式表會先拷貝父類的表,然後替換和父類中一樣函式的,最後補上子類自身的函式。
Q36:設計模式有哪些?是如何實現的?
A36:
1.單例模式
主要思想à建構函式私有化+指標禁止+複製構造禁止+執行緒安全+自動釋放
執行緒安全的原因↓
https://stackoverflow.com/questions/34457432/c11-singleton-static-variable-is-thread-safe-why
2.簡單工廠模式&工廠方法模式&抽象工廠模式
(1)簡單工廠模式
思想:告訴工廠類生產什麼,工廠類根據傳參判斷生產什麼,生產的產品有共同的特點。例:球廠,籃球足球排球……
缺點:工廠類集中了所有產品類的建立邏輯,如果產品量較大,會使得工廠類變的非常臃腫。
(2)工廠方法模式
思想:每個工廠生產一種產品,生產那種產品就先造哪個工廠
缺點:產品類資料較多時,需要實現大量的工廠類,這無疑增加了程式碼量。
(3)抽象工廠模式
思想:一個工廠生產多種產品,生產那種產品就先造哪個工廠
缺點:當增加一個新系列的產品時,不僅需要現實具體的產品類,還需要增加一個新的建立介面,擴充套件相對困難。
(4)
未完待續
Q37:常見的IO模型有哪些?
A37:
同步阻塞IO-BIO:等待請求成功後返回
同步非阻塞IO-NIO:請求後立刻返回,不管成功與否,需要多次詢問才能達到成功的結果
非同步阻塞IO-IO多路複用:由select/epoll等多路複用器進行阻塞,任意檔案描述符就緒則返回,關聯Reactor設計模式
非同步非阻塞IO-AIO:是不是真正的非同步IO是按照是否由使用者執行緒自行讀取資料、處理資料區分的。當用戶執行緒收到通知時,資料已經被核心讀取完畢,並放在了使用者執行緒指定的緩衝區內,核心在IO完成後通知使用者執行緒直接使用即可。關聯Proactor設計模式。
可惜的是,在 Linux 下的非同步 I/O 是不完善的, aio 系列函式是由 POSIX 定義的非同步操作介面,不是真正的作業系統級別支援的,而是在使用者空間模擬出來的非同步,並且僅僅支援基於本地檔案的 aio 非同步操作,網路程式設計中的 socket 是不支援的,這也使得基於 Linux 的高效能網路程式都是使用 Reactor 方案。而 Windows 裡實現了一套完整的支援 socket 的非同步程式設計介面,這套介面就是 IOCP,是由作業系統級別實現的非同步 I/O,真正意義上非同步 I/O,因此在 Windows 裡實現高效能網路程式可以使用效率更高的 Proactor 方案。