面試總結5--C++基礎問題Part1


從2015年4月開始參加實習招聘筆試面試,再到8月底開始內推,9月、10月正式校招,大大小小面試參加了不下20場,也拿到了自己心儀的Offer。總結了一些C++面試中常問的基礎題目,希望能夠給有需要的朋友一些幫助。由於大多數題目是自己總結的也有些是網上搜集的資料,如有不恰當之處請多包涵!


1、STL中的vector:增減元素對迭代器的影響?

解答:這個問題主要是針對連續內存容器和非連續內存容器。

         a、對於連續內存容器,如vector、deque等,增減元素均會使得當前之後的所有迭代器失效。因此,以刪除元素爲例:由於erase()總是指向被刪除元素的下一個元素的有效迭代器,因此,可以利用該連續內存容器的成員erase()函數的返回值。常見的編程寫法爲:

	for(auto iter = myvec.begin(); iter != myvec.end())  //另外注意這裏用 "!=" 而非 "<"
	{
		if(delete iter)
			iter = myvec.erase(iter);
		else ++iter;
	}


         還有兩種極端的情況是:

         (1)、vector插入元素時位置過於靠前,導致需要後移的元素太多,因此vector增加元素建議使用push_back而非insert;

         (2)、當增加元素後整個vector的大小超過了預設,這時會導致vector重新分分配內存,效率極低。因此習慣的編程方法爲:在聲明瞭一個vector後,立即調用reserve函數,令vector可以動態擴容。通常vector是按照之前大小的2倍來增長的。

         b、對於非連續內存容器,如set、map等。增減元素只會使得當前迭代器無效。仍以刪除元素爲例,由於刪除元素後,erase()返回的迭代器將是無效的迭代器。因此,需要在調用erase()之前,就使得迭代器指向刪除元素的下一個元素。常見的編程寫法爲:

         for(autoiter = myset.begin(); iter != myset.end()) //另外注意這裏用 "!=" 而非 "<"
         {
                  if(deleteiter)
                          myset.erase(iter++);  //使用一個後置自增就OK了
                  else++iter;
         }
        


2、New和malloc的區別?

解答:

       new可分爲operatornew(new 操作)、new operator(new 操作符)和placement new(定位 new)。其中operator new執行和malloc相同的任務,即分配內存,但對構造函數一無所知;而 new operator則調用operator new,分配內存後再調用對象構造函數進行對象的構造。

         其中operatornew是可以重載的。placement new,就是operator new的一個重載版本,允許你在一個已經分配好的內存中構造一個新的對象。而網上對new說法,大多針對operator new而言,因此說new是帶有類型的(以爲調用了類的構造函數),不過如果直接就說new是帶有類型的話,明顯是不合適的,比如原生的operator new。


3、C++如何避免內存泄漏?

解答:這其實可以看做是一個編程風格的問題。

         a、使用RAII(ResourceAcquisition Is Initialization,資源獲取即初始化)技法,以構造函數獲取資源(內存),析構函數釋放。

         b、相比於使用原生指針,更建議使用智能指針,尤其是C++11標準化後的智能指針。

         c、注意delete和delete[]的使用方法。

         d、這是很複雜的一種情況,是關於類的copy constructor的。首先先介紹一些概念。

               同defaultconstructor一樣,標準保證,如果類作者沒有爲class聲明一個copy constructor,那麼編譯器會在需要的時候產生出來(這也是一個常考點:問道"如果類作者未定義出default/copy constructor,編譯器會自動產生一個嗎?"答案是否定的)

                  不過請注意!!這裏編譯器即使產生出來,也是爲滿足它的需求,而非類作者的需求!!

                  而什麼時候是編譯器"需要"的時候呢?是在當這個class【不表現出】bitwise copy semantics(位逐次拷貝,即淺拷貝)的時候。

                  在4中情況下class【不表現出】bitwisecopy semantics

                  (1)、當class內含一個memberobject且該member object聲明瞭一個copy constructor(無論該copy ctor是類作者自己生明的還是編譯器合成的);

                  (2)、當class繼承自一個baseclass且該base class有一個copy constructor(無論該copy ctor是類作者自己生明的還是編譯器合成的);

                  (3)、當class聲明瞭virtualfunction;

                  (4)、當class派生自一個繼承鏈,且該鏈中存在virtual base class時。

         言歸正傳,如果class中僅僅是一些普通資源,那麼bitwisecopy semantics是完全夠用的;然而,擋在該class中存在了一塊動態分配的內存,並且在之後執行了bitwise copy semantics後,將會有一個按位拷貝的對象和原來class中的某個成員指向同一塊heap空間,當執行它們的析構函數後,該內存將被釋放兩次,這是未定義的行爲。因此,在必要的時候需要使用Memberwise copy semantics(即深拷貝),來避免內存泄露。位拷貝拷貝的是地址,而值拷貝則拷貝的是內容。


4、STL中排序算法的實現是什麼?

解答:STL中的sort(),在數據量大時,採用quicksort,分段遞歸排序;一旦分段後的數量小於某個門限值,改用Insertion sort,避免quicksort深度遞歸帶來的過大的額外負擔,如果遞歸層次過深,還會改用heapsort。

sort採用的是成熟的"快速排序算法"(目前大部分STL版本已經不是採用簡單的快速排序,而是結合內插排序算法) 可以保證很好的平均性能、複雜度,stable_sort採用的是"歸併排序",分派足夠內存是,其算法複雜度爲n*log(n), 否則其複雜度爲n*log(n)*log(n),其優點是會保持相等


5、類是怎麼通過虛函數實現多態的?

         多態性是“一個接口,多種方法”,多態性分爲兩類: 靜態多態性和動態多態性。以前學過的函數重載和運算符重載實現的多態性屬於靜態多態性,動態多態性是通過虛函數(virtual function)實現的。靜態多態性是指:在程序編譯時系統就能決定調用的是哪個函數,因此靜態多態性又稱編譯時的多態性。動態多態性是在程序運行過程中才動態地確定操作所針對的對象。它又稱運行時的多態性。類中有虛函數存在,所以編譯器就會爲他做添加一個vptr指針,併爲他們分別創建一個表vtbl,vptr指向那個表,每個類都有自己的vtbl,vtbl的作用就是保存自己類中虛函數的地址,我們可以把vtbl形象地看成一個數組,這個數組的每個元素存放的就是虛函數的地址。,只要vptr不同,指向的vtbl就不同,而不同的vtbl裏裝着對應類的虛函數地址,所以這樣虛函數就可以完成它的任務。子類重寫的虛函數的地址直接替換了父類虛函數在虛表中的位置,因此當訪問虛函數時,該虛表中的函數是誰的就訪問誰。

注意:存在虛函數的類都有一個一維的虛函數表叫做虛表。類的對象有一個指向虛表開始的虛指針。虛表是和類對應的,虛表指針是和對象對應的。

對於虛函數調用來說,每一個對象內部都有一個虛表指針,該虛表指針被初始化爲本類的虛表。所以在程序中,不管你的對象類型如何轉換,但該對象內部的虛表指針是固定的,所以呢,才能實現動態的對象函數調用,這就是C++多態性實現的原理

單繼承與多繼承:單繼承所有的虛函數都包含在虛函數表中,多重繼承有多個虛函數表,當子類對父類的虛函數有重寫時,子類的函數覆蓋父類的函數在對應的虛函數位置,當子類有新的虛函數時,這些虛函數被加在第一個虛函數表的後面

虛繼承:使公共的基類在子類中只有一份,我們看到虛繼承在多重繼承的基礎上多了vbtable來存儲到公共基類的偏移


6、操作系統的作業調度機制

         作業(job)是操作系統中一個常見的概念,所謂作業是指用戶在一次計算過程或者事務處理過程中,要求計算機系統所作工作的集合。

所謂作業調度是指按照某種原則,從後備作業隊列中選取作業進入內存,併爲作業做好運行前的準備工作以及作業完成後的善後處理工作。

常用的作業調度算法有五種:先來先服務調度算法短作業優先調度算法響應比高者優先調度算法最高優先數調度算法均衡調度算法


7、深拷貝,淺拷貝?

什麼時候用到拷貝函數? 

a.一個對象以值傳遞的方式傳入函數體;  b.一個對象以值傳遞的方式從函數返回;  c.一個對象需要通過另外一個對象進行初始化。

如果一個類擁有資源,當這個類的對象發生複製過程的時候,資源重新分配,這個過程就是深拷貝,反之,沒有重新分配資源,就是淺拷貝。

在某些狀況下,類內成員變量需要動態開闢堆內存,如果實行位拷貝,也就是把對象裏的值完全複製給另一個對象,如A=B。這時,如果B中有一個成員變量指針已經申請了內存,那A中的那個成員變量也指向同一塊內存。這就出現了問題:當B把內存釋放了(如:析構),這時A內的指針就是野指針了,出現運行錯誤。

淺拷貝和深拷貝主要區別就是複製指針時是否重新創建內存空間。如果沒有沒有創建內存只賦值地址爲淺拷貝,創建新內存把值全部拷貝一份就是深拷貝。淺拷貝在類裏面有指針成員的情況下只會複製指針的地址,會導致兩個成員指針指向同一塊內存,這樣在要是分別delete釋放時就會出現問題,因此需要用深拷貝。


8、子類、父類中的名稱遮掩,如何避免?

    子類public繼承父類的函數,子類的方法名會遮掩父類的相同名的方法,即使父類和子類內的函數有不同的參數類型也適用而且不論函數是virtual或non-virtual。子類要想訪問父類的方法,使用using 父類名::函數名。 或轉交函數。


9、爲什麼auto_ptr不能用在容器中?

STL容器在分配內存的時候,必須要能夠拷貝構造容器的元素。而且拷貝構造的時候,不能修改原來元素的值。而auto_ptr在拷貝構造的時候,一定會修改元素的值。所以STL元素不能使用auto_ptr。因爲兩個auto_ptr不能管理同一塊內存。

 

10、new/delete和malloc/free的區別和聯繫

1、malloc和free是C語言標準函數庫中的兩個函數,new/delete是C++語言中兩個運算符。

2、malloc/free和new/delete都是用來申請動態內存的。

3、new 不止是分配內存,而且會調用類的構造函數,同理delete會調用類的析構函數,而malloc則只分配內存,不會進行初始化類成員的工作,同樣free 也不會調用析構函數。

4、malloc得到的指針無類型,new出來的指針是帶有類型信息的。

5、對於非內部數據類型的對象而言,光用maloc/free無法滿足動態對象的要求。對象在創建的同時 要自動執行構造函數,對象在消亡之前要自動執行析構函數。由於malloc/free是庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加於malloc/free。

6、如果用free釋放“new創建的動態對象”,那麼該對象因無法執行析構函數而可能導致程序出錯。如 果用delete釋放“malloc申請的動態內存”,理論上講程序不會出錯,但是該程序的可讀性很差。所以new/delete 必須配對使用,malloc/free也一樣。

 

11、C++中的Static用法?

靜態全局變量有以下特點: 
• 該變量在全局數據區分配內存; 
• 未經初始化的靜態全局變量會被程序自動初始化爲0(自動變量的值是隨機的,除非它被顯式初始化); 
• 靜態全局變量在聲明它的整個文件都是可見的,而在文件之外是不可見的; 

靜態局部變量有以下特點:
• 該變量在全局數據區分配內存; 
• 靜態局部變量在程序執行到該對象的聲明處時被首次初始化,即以後的函數調用不再進行初始化; 
• 靜態局部變量一般在聲明處初始化,如果沒有顯式初始化,會被程序自動初始化爲0; 
• 它始終駐留在全局數據區,直到程序運行結束。但其作用域爲局部作用域,當定義它的函數或語句塊結束時,其作用域隨之結束;

定義靜態函數的好處: 
• 靜態函數不能被其它文件所用; 
• 其它文件中可以定義相同名字的函數,不會發生衝突;

類中Static關鍵字:

靜態數據成員有以下特點: 
• 對於非靜態數據成員,每個類對象都有自己的拷貝。而靜態數據成員被當作是類的成員。無論這個類的對象被定義了多少個,靜態數據成員在程序中也只有一份拷貝,由該類型的所有對象共享訪問。也就是說,靜態數據成員是該類的所有對象所共有的。對該類的多個對象來說,靜態數據成員只分配一次內存,供所有對象共用。所以,靜態數據成員的值對每個對象都是一樣的,它的值可以更新; 
• 靜態數據成員存儲在全局數據區。靜態數據成員定義時要分配空間,所以不能在類聲明中定義。在Example 5中,語句int Myclass::Sum=0;是定義靜態數據成員; 
• 靜態數據成員和普通數據成員一樣遵從public,protected,private訪問規則; 
• 因爲靜態數據成員在全局數據區分配內存,屬於本類的所有對象共享,所以,它不屬於特定的類對象,在沒有產生類對象時其作用域就可見,即在沒有產生類的實例時,我們就可以操作它; 
• 靜態數據成員初始化與一般數據成員初始化不同。靜態數據成員初始化的格式爲:
<數據類型><類名>::<靜態數據成員名>=<值> 
• 類的靜態數據成員有兩種訪問形式:
<類對象名>.<靜態數據成員名> 或 <類類型名>::<靜態數據成員名>
如果靜態數據成員的訪問權限允許的話(即public的成員),可在程序中,按上述格式來引用靜態數據成員 ; 
• 同全局變量相比,使用靜態數據成員有兩個優勢: 
1. 靜態數據成員沒有進入程序的全局名字空間,因此不存在與程序中其它全局名字衝突的可能性; 
2. 可以實現信息隱藏。靜態數據成員可以是private成員,而全局變量不能;

靜態成員函數:

• 出現在類體外的函數定義不能指定關鍵字static; 
• 靜態成員之間可以相互訪問,包括靜態成員函數訪問靜態數據成員和訪問靜態成員函數; 
• 非靜態成員函數可以任意地訪問靜態成員函數和靜態數據成員; 
• 靜態成員函數不能訪問非靜態成員函數和非靜態數據成員; 
• 由於沒有this指針的額外開銷,因此靜態成員函數與類的全局函數相比速度上會有少許的增長; 
• 調用靜態成員函數,可以用成員訪問操作符(.)和(->)爲一個類的對象或指向類對象的指針調用靜態成員函數,也可以直接使用如下格式:
<類名>::<靜態成員函數名>(<參數表>)
調用類的靜態成員函數。


12、進程空間佈局?

一個由 C/C++編譯的程序佔用的內存(memory)分爲以下幾個部分:

1.程序代碼區(.text) -      存放函數體的二進制代碼  。

2.文字常量區(.rodata)    -      常量字符串就是放在這裏的,程序結束後由系統釋放(rodata—readonly data)。

3.全局區/靜態區(static)  -      全局變量 和 靜態變量的存儲是放在一塊的。初始化的全局變量和靜態變量在一塊區域(.rwdataor .data),未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域(.bss), 程序結束後由系統釋放。

*在 C++中,已經不再嚴格區分bss data了,它們共享一塊內存區域

4.堆區(heap)   -      一般由程序員分配釋放(new/malloc/calloc delete/free),若程序員不釋放,程序結束時可能由OS 回收。

注意:它與數據結構中的堆是兩回事,但分配方式倒類似於鏈表。

5.棧區(stack)  -      由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。


13、堆和棧的區別?

管理方式:對於棧來講,是由編譯器自動管理;對於堆來說,釋放工作由程序員控制,容易產生memory leak。

空間大小:一般來講在 32 位系統下,堆內存可以達到接近 4G 的空間,從這個角度來看堆內存幾乎是沒有什麼限制的。但是對於棧來講,一般都是有一定的空間大小的,例如,在VC6 下面,默認的棧空間大小大約是1M。

碎片問題:對於堆來講,頻繁的new/delete勢必會造成內存空間的不連續,從而造成大量碎片,使程序效率降低;對於棧來講,則不會存在這個問題,因爲棧是先進後出的隊列,永遠都不可能有一個內存塊從棧中間彈出。

生長方向:對於堆來講,生長方向是向上的,也就是向着內存地址增加的方向;對於棧來講,它的生長方向是向下的,是向着內存地址減小的方向增長。

分配方式:堆都是動態分配的,沒有靜態分配的堆;棧有2 種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的,比如局部變量的分配,動態分配由alloca 函數進行分配,但是棧的動態分配和堆是不同的,它的動態分配是由編譯器進行釋放,不需要我們手工實現。

分配效率:棧是機器系統提供的數據結構,計算機會在底層分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高;堆則是C/C++函數庫提供的,它的機制是很複雜的,例如爲了分配一塊內存,庫函數會按照一定的算法(具體的算法可以參考數據結構/操作系統)在堆內存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於內存碎片太多),就有可能調用系統功能去增加程序數據段的內存空間,然後進行返回。顯然,堆的效率比棧要低得多。

無論是堆還是棧,都要防止越界現象的發生。

 

14、MFC中點擊一個銨鈕的整個消息響應流程

一個MFC消息響應函數在程序中有三處相關信息:函數原型、函數實現和以及用來關聯消息和消息響應函數的宏。
(1)在消息響應函數的原型代碼中,函數聲明的前部有一個afx_msg限定符,也是一個宏,該宏表明這個函數是一個消息響應函數的聲明。
(2)消息映射宏:在視圖類的源文件中,BEGIN_MESSAGE_MAP()和END_MASSAGE_MAP()這兩個宏之間定義了消息映射表,例如對於畫線,其中有一個ON_WM_LBUTTONDOWN()消息映射宏,這個宏的作用就是把鼠標左鍵按下消息(WM_LBUTTONDOWN)與一個消息響應函數關聯起來,通過這種機制,一旦有消息產生,程序就會調用相應的消息響應函數來進行處理。
(3)消息響應函數的定義:在視圖類的源文件中,可以看到OnLButtonDown函數的定義。頭文件中在兩個AFX_MSG註釋宏之間是消息響應函數原型的聲明。源文件中有兩處:一處是在兩個AFX_MSG_MAP註釋宏之間的消息映射宏,通過這個宏把消息與消息響應函數關聯起來;另一處是源文件中的消息響應函數的實現代碼。

MFC消息映射機制的具體實現方法是:在每個能接收和處理消息的類中,定義一個消息和消息函數靜態對照表,即消息映射表.在消息映射表中,消息與對應的消息處理函數指針是成對出現的.某個類能處理的所有消息及其對應的消息處理函數的地址都列在這個類所對應的靜態表中.當有消息需要處理時,程序只要搜索該消息靜態表,查看錶中是否含有該消息,就可知道該類能否處理此消息.如果能處理此消息,則同樣依照靜態表能很容易找到並調用對應的消息處理函數.

 

15、override重寫和overload重載的區別

成員函數被重載的特徵:
(1)相同的範圍(在同一個類中);
(2)函數名字相同;
(3)參數不同;
(4)virtual關鍵字可有可無。

函數重載不能靠返回值來進行區分

重寫是指派生類函數重寫基類函數,是C++的多態的表現,特徵是:
(1)不同的範圍(分別位於派生類與基類);
(2)函數名字相同;
(3)參數相同;

(4)返回值(即函數原型)都要與基類的函數相同
(5)基類函數必須有virtual關鍵字。

重寫函數的訪問修飾符可以不同,儘管virtual函數是private的,在派生類中重寫的函數可以是public或protect的

“隱藏”是指派生類的函數屏蔽了與其同名的基類函數,規則如下:

1)如果派生類的函數與基類的函數同名,但是參數不同。此時,不論有無virtual關鍵字,基類的函數將被隱藏。
(2)如果派生類的函數與基類的函數同名,並且參數也相同,但是基類函數沒有virtual關鍵字。此時,基類的函數被隱藏。

 

16linux下如何修改進程優先級?

linux下的進程調度優先級是從-20到19,一共40個級別,數字越大,表示進程的優先級越低。查看進程優先級有兩個辦法:ps和top。top命令顯示的NI列的值。

nice命令的使用在啓動進程時指定請求進程執行優先級。

新建一個進程並設置優先級

 Nice -19tar zcf pack.tar.gz documents

修改已經存在的進程的優先級

 Renice 19 1799


17linux下性能監控命令uptime介紹,平均負載的具體含義是什麼?

linux uptime命令主要用於獲取主機運行時間和查詢linux系統負載等信息。

系統平均負載是指在特定時間間隔內運行隊列中的平均進程數。如果每個CPU內核的當前活動進程數不大於3的話,那麼系統的性能是良好的。如果每個CPU內核的任務數大於5,那麼這臺機器的性能有嚴重問題。


18constdefine區別

1) 編譯器處理方式不同

define宏是在預處理階段展開。

const常量是編譯運行階段使用。

(2) 類型和安全檢查不同

define宏沒有類型,不做任何類型檢查,僅僅是展開。

const常量有具體的類型,在編譯階段會執行類型檢查。

(3) 存儲方式不同

define宏僅僅是展開,有多少地方使用,就展開多少次,不會分配內存。

const常量會在內存中分配(可以是堆中也可以是棧中)。

(4)const 可以節省空間,避免不必要的內存分配。const定義的常量在程序運行過程中只有一份拷貝,而#define定義的常量在內存中有若干個拷貝。

(5) 提高了效率。編譯器通常不爲普通const常量分配存儲空間,而是將它們保存在符號表中,這使得它成爲一個編譯期間的常量,沒有了存儲與讀內存的操作,使得它的效率也很高。

不能在類聲明中初始化const數據成員const數據成員的初始化只能在類構造函數的初始化表中進行,

既然C++中有更好的const爲什麼還要使用宏?

const無法代替宏作爲衛哨來防止文件的重複包含。

類中的常量:

有時我們希望某些常量只在類中有效。由於#define定義的宏常量是全局的,不能達到目的,於是想當然地覺得應該用const修飾數據成員來實現。const數據成員的確是存在的,但其含義卻不是我們所期望的。const數據成員只在某個對象生存期內是常量,而對於整個類而言卻是可變的,因爲類可以創建多個對象,不同的對象其const數據成員的值可以不同。

在整個類中都恆定的常量:應該用類中的枚舉常量來實現。例如

classA

{…

enum{ SIZE1 = 100, SIZE2 = 200}; // 枚舉常量

intarray1[SIZE1];

intarray2[SIZE2];

};

枚舉常量不會佔用對象的存儲空間,它們在編譯時被全部求值。枚舉常量的缺點是:它的隱含數據類型是整數,其最大值有限,且不能表示浮點數(如PI=3.14159)。sizeof(A) = 1200;其中枚舉部長空間。


19、模板代碼膨脹如何消除?

C++模板中與參數無關的代碼分離出來。也就是讓與參數無關的代碼只有一份拷貝。

(1)  模板生成多個類和多個函數,所以任何模板代碼都不該與某個造成膨脹的模板參數產生相依關係。

(2)  因非類型模板參數而造成的代碼膨脹,往往可消除,做法是以函數或類成員變量替換template參數

(3)  因類型參數而造成的代碼膨脹,往往可降低,做法是讓帶有完全相同的二進制表述的具現類型共享實現碼。

 

20、代碼重構常用方法?

1、提取類/抽離方法 

2、提取方法

3、分離條件 

4、引入參數對象/保留全局對象 

5、用符號常量替換無意義數字 

6、重命名方法 


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