C/C++常見面試題目

C/C++常見面試題目

與編譯過程相關的問題

  • 爲什麼在C++裏面,一個類的成員函數不能既是 template 又是 virtual 的。

因爲C++的編譯與鏈接模型是"分離"的。一個C/C++程序就可以被分開編譯,然後用一個linker鏈接起來。這種模型有一個問題,就是各個編譯單元可能對另一個編譯單元一無所知。 一個 function template最後到底會被 instantiate 爲多少個函數,要等整個程序(所有的編譯單元)全部被編譯完成才知道。 同時,virtual function的實現大多利用了一個"虛函數表"的東西,這種實現中,一個類的內存佈局(或者說虛函數表的內存佈局)需要在這個類編譯完成的時候就被完全確定

  • C/C++編譯過程

編譯過程主要分4個過程:編譯預處理;編譯、優化階段、彙編階段、鏈接程序。

具體的細節詳見https://blog.csdn.net/hycxag/article/details/82967579

  • 爲什麼頭文件裏一般只可以有聲明不能有定義

頭文件可以被多個編譯單元包含,如果頭文件裏面有定義的話,那麼每個包含這頭文件的編譯單元都會對同一個符號進行定義,如果該符號爲外部鏈接,則會導致duplicated external symbols鏈接錯誤。

  • 爲什麼公共使用的內聯函數要定義於頭文件裏

因爲編譯時編譯單元之間互不知道,如果內聯被定義於.cpp文件中,編譯其他使用該函數的編譯單元的時候沒有辦法找到函數的定義,因些無法對函數進行展開(內聯函數不展開,即不採用在使用處標記函數代碼再跳轉的方式,而是直接將代碼嵌入)。所以如果內聯函數定義於.cpp裏,那麼就只有這個.cpp文件能使用它。故.h中的inline 函數可以被多個cpp包含而不造成符號衝突,因爲它會被直接嵌入到調用的地方,內部聯結不形成外部符號,對外不可見。

  • 爲什麼函數默認是外部鏈接

如果函數默認是內部鏈接,那麼大家會傾向於把函數連同其定義都放入頭文件中。然而,函數是多變的,可能會經常修改,這樣一來,所以包含它的模塊都需要被重新編譯,很麻煩。另外一方面,如果函數中定義了靜態變量,這樣每一個包含該函數的模塊都會有一個靜態變量(因爲假設是默認內部鏈接),導致不一致。 

  • 爲什麼const常量默認是內部鏈接而變量(全局)默認是外部鏈接?

因爲它是常量,初始化後就不能改變,這樣即使每一個包含它的模塊都有一份它的複製,那也不會導致不一致。如果變量默認是內部鏈接,它是可變的量,所以在每個包含它的模塊中,它的值可能會被改變,從而導致不一致的狀況出現。

  • 爲什麼類的靜態數據成員不可以就地初始化?

因爲類體一般是放在頭文件中的,如果允許其靜態成員就地初始化,那就相當於允許在頭文件中定義變量了。

STL中相關的問題

  • STL at()和重載的operator()有什麼關係

array、deque、vector不能通過operator向容器中添加元素;而map、unordered_map類可以通過operator[]向容器中添加元素。所有容器均不能通過at()函數向容器重添加元素

at()函數在被調用時,會檢查下標的有效性(與容器的size()比較而不是capacity()),若下標有效則返回對應位置的元素,否則拋出std::out_of_range異常。而operator函數在被調用時,不檢查下標的有效性。

  • STL中的內存管理allocator機制

會採用兩種分配的機制。大對象(>128字節)直接通過malloc向系統的堆空間分配;小對象通過預先分配好的內存池中取出。這樣做的好處:小對象快速分配;避免內存碎片產生,減緩了OS的內存管理壓力;儘可能最大化利用內存(內存池尚有的空閒區域不足以分配所需的大小時,分配算法會將其鏈入到對應的空閒列表中,然後會嘗試從空閒列表中尋找是否有合適大小的區域)

具體的詳細細節見https://blog.csdn.net/hycxag/article/details/82977029

網絡編程相關的問題

  • 服務器端不調用accept會發生什麼

不調用accept時,也能建立連接,即三次握手完成。但不能進行API的控制,即不能進行繼續通訊。以及建立好連接的隊列大大小爲:backlog。從而在Unix系統服務器中,若客戶端調用 connect() ,客戶端連接超時失敗。而在Linux系統中,若客戶端調用 connect()。TCP 的連接隊列滿後,Linux 服務器不會拒絕連接,只是有些會延時連接,有些立刻連接。

詳情參考https://blog.csdn.net/hycxag/article/details/82974484

C/C++中的基本問題

  • C++爲什麼要有class

類是C++用來實現OOP封裝、繼承和多態的核心機制。C++用虛函數實現多態,用RAII(和析構,異常機制)實現自動資源管理,用拷貝和移動定義資源的複製和轉移,進而用隱式成員(Rule of 5,析構,拷貝構造,拷貝賦值,移動構造,移動賦值)來幫助用戶省去手寫冗餘代碼,最終達到不多寫一個字的資源管理。如果說面向對象的概念已經有些過時了,資源管理卻是永不過時的,也是C++從機制上不同於C的最主要一點。

  • C++多態實現及其原理

在C ++程序設計中,多態性是指具有不同功能的函數可以用同一個函數名,這樣就可以用一個函數名調用不同內容的函數。一般來說多態分爲兩種:靜態多態和運行時多態。

靜態多態包含參數多態,過載多態和強制多態,參數多態:採用參數化模板,通過給出不同的類型參數,使得一個結構有多種類型;過載多態:同一個名字在不同的上下文中所代表的含義不同。典型的例子是運算符重載和函數重載;強制多態:編譯程序通過語義操作,把操作對象的類型強行加以變換,以符合函數或操作符的要求。

運行時多態主要是包含多態:包含多態的基礎是虛函數。主要是通過類的繼承和虛函數來實現,當基類和子類擁有同名同參同返回的方法,且該方法聲明爲虛方法,當基類對象,指針,引用指向的是派生類的對象的時候,基類對象,指針,引用在調用基類的方法,實際上調用的是派生類方法。

詳情參考https://blog.csdn.net/hycxag/article/details/82978173

  • 父類的構造方法中調用虛函數,會發生多態嗎

父類的構造方法中調用虛函數,不會發生多態。這個和 vptr 的分步初始化有關。在父類中調用虛函數時,執行的還是父類的函數,沒有發生多態。這是因爲當創建子類對象時,編譯器的執行順序其實是這樣的:

  1. 對象在創建時,由編譯器對 vptr 進行初始化
  2. 子類的構造會先調用父類的構造函數,這個時候 vptr 會先指向父類的虛函數表
  3. 子類構造的時候,vptr 會再指向子類的虛函數表
  4. 對象的創建完成後,vptr 最終的指向才確定
  • C++的虛析構函數

通過基類的指針來刪除派生類的對象時,若析構函數不是虛析構函數,只會調用基類的析構函數,而派生類的析構函數不會被調用,從而造成內存泄漏。

  • 如果父類的析構函數不加virtual關鍵字 :當父類的析構函數不聲明成虛析構函數的時候,當子類繼承父類,父類的指針指向子類時,delete掉父類的指針,只調動父類的析構函數,而不調動子類的析構函數。
  • 如果父類的析構函數加virtual關鍵字 :當父類的析構函數聲明成虛析構函數的時候,當子類繼承父類,父類的指針指向子類時,delete掉父類的指針,先調動子類的析構函數,再調動父類的析構函數。

而在虛函數表中,存放了父類的虛析構函數。故調用父類的析構函數時,此虛析構函數中:先調用子類的析構函數的,再調用父類的析構函數。

  • C++中的純虛函數與抽象類

純虛函數聲明如下:virtual void function()=0;純虛函數一定沒有定義,用來規範派生類的行爲,即接口。包含純虛函數的類是抽象類,抽象類不能定義實例,但是可以聲明指向該抽象類的具體類的指針或者引用;

如果是一個純虛函數,那麼,在虛函數表中,其函數指針的值就是0;即在虛函數表當中,如果是純虛函數,那麼就實實在在的寫上0

  • 不能聲明爲虛函數的函數

  • 普通函數(非成員函數):只能被重載,不能被覆蓋;聲明虛函數也是可以的,但帶來運行效率的降低。故編譯器不會將此函數聲明爲虛函數,而是編譯器在編譯時綁定此函數。
  • 構造函數:因爲構造函數本來就是爲了明確初始化對象成員才產生的,然而虛函數主要是爲了再不完全瞭解細節的情況下也能正確處理對象。另外,virtual函數是在不同類型的對象產生不同的動作,現在對象還沒有產生,如何使用virtual函數來完成你想完成的動作。
  • 內聯函數:內聯函數就是爲了在代碼中直接展開,減少函數調用花費的代價,虛函數是爲了在繼承後對象能夠準確的執行自己的動作,這是不可能統一的。而且,inline函數在編譯時被展開,虛函數在運行時才能動態的邦定函數。
  • 靜態成員函數:每個類來說只有一份代碼,所有的對象都共享這一份代碼,他也沒有要動態邦定的必要性。
  • 友元函數:友元函數並不是成員函數,故不討論是否爲虛函數;但是可以通過讓友元函數調用虛成員函數來解決友元動態綁定的問題
  • 不能被繼承的函數

  • 構造函數(拷貝構造函數):在創建子類對象時,爲了初始化從父類繼承來的數據成員,系統需要調用其父類的構造方法。 。如果沒有顯式的構造函數,編譯器會給一個默認的構造函數,並且該默認的構造函數僅僅在沒有顯式地聲明構造函數情況下創建。
  • 析構函數:只是在子類的析構函數中會自動調用父類的析構函數。
  • 賦值運算符重載函數:子類的賦值運算符重載函數中會調用父類的賦值運算符重載函數。
  • 構造函數和析構函數中不應該調用虛函數

構造派生類對象時,首先調用基類構造函數初始化對象的基類部分。在執行基類構造函數時,對象的派生類部分是未初始化的。實際上,此時的對象還不是一個派生類對象。

析構派生類對象時,首先撤銷/析構他的派生類部分,然後按照與構造順序的逆序撤銷他的基類部分。

因此,在運行構造函數或者析構函數時,對象都是不完整的。爲了適應這種不完整,編譯器將對象的類型視爲在調用構造/析構函數時發生了變換,即:視對象的類型爲當前構造函數/析構函數所在的類的類類型。由此造成的結果是:在基類構造函數或者析構函數中,會將派生類對象當做基類類型對象對待。

而這樣一個結果,會對構造函數、析構函數調用期間調用的虛函數類型的動態綁定對象產生影響,最終的結果是:如果在構造函數或者析構函數中調用虛函數,運行的都將是爲構造函數或者析構函數自身類類型定義的虛函數版本。 無論有構造函數、析構函數直接還是間接調用虛函數

對象的虛函數表地址在對象的構造和析構過程中會隨着部分類的構造和析構而發生變化,這一點應該是編譯器實現相關的。

  • new與malloc,以及delete與free的區別

  • 空結構體的大小

C++語言中的確規定了空結構體和空類所佔內存大小爲1,而C語言中空類和空結構體佔用的大小是0。由於C++語言標準規定了任何不同的對象不能擁有相同的內存地址。如果空類對象大小爲0,那麼此類數組中的各個對象的地址將會一致,明顯違反了此原則。爲了滿足C++標準規定的不同對象不能有相同地址,最簡單方法就是:C++編譯器保證任何類型對象大小不能爲0。故C++編譯器會在空類或空結構體中增加一個虛設的字節(有的編譯器可能不止一個),以確保不同的對象都具有不同的地址。

 

 

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章