非科班進大廠,《逆襲進大廠》第二彈之C++進階篇59問59答(超硬核干貨)

 2023-12-09 阅读 26 评论 0

摘要:大家好,我是阿秀先扯兩句閑話,前段時間加了一個粉絲,他告訴我說他們老師在班級群里推薦了我,我當時聽到都懵了。。。當時真的有點懵,原來真的有人會看我寫的東西,還愿意推薦給身邊的人看…最重要的是,這還是一位計算機專業

大家好,我是阿秀

先扯兩句閑話,前段時間加了一個粉絲,他告訴我說他們老師在班級群里推薦了我,我當時聽到都懵了。。。

當時真的有點懵,原來真的有人會看我寫的東西,還愿意推薦給身邊的人看…

最重要的是,這還是一位計算機專業的 C++ 從業老師,在這里也感謝這位老師的推薦,以后一定不會辜負您的推薦。

emm,這兩天在另一個文件夾里又發現了一些自己整理的 C++ 筆記,所以 C++ 部分可能要分成四期文章了,這是第二期,第三期是 C++ 提高篇,而最后一篇是 C++ 重頭戲,也是面試必問的 STL 部分。

非科班進大廠,這是本期 C++ 八股文問題目錄。

不逼逼了,《逆襲進大廠系列第二彈 C++ 進階篇直接發車了。

這篇總字數是 37814?個字,嗯,將近 4W 字。

50、static的用法和作用?

1.先來介紹它的第一條也是最重要的一條:隱藏。(static函數,static變量均可)

當同時編譯多個文件時,所有未加static前綴的全局變量和函數都具有全局可見性。

2.static的第二個作用是保持變量內容的持久。(static變量中的記憶功能和全局生存期)存儲在靜態數據區的變量會在程序剛開始運行時就完成初始化,也是唯一的一次初始化。共有兩種變量存儲在靜態存儲區:全局變量和static變量,只不過和全局變量比起來,static可以控制變量的可見范圍,說到底static還是用來隱藏的。

c++大廠、3.static的第三個作用是默認初始化為0(static變量)

其實全局變量也具備這一屬性,因為全局變量也存儲在靜態數據區。在靜態數據區,內存中所有的字節默認值都是0x00,某些時候這一特點可以減少程序員的工作量。

4.static的第四個作用:C++中的類成員聲明static

1) ?函數體內static變量的作用范圍為該函數體,不同于auto變量,該變量的內存只被分配一次,因此其值在下次調用時仍維持上次的值;

2) ?在模塊內的static全局變量可以被模塊內所用函數訪問,但不能被模塊外其它函數訪問;

3) ?在模塊內的static函數只可被這一模塊內的其它函數調用,這個函數的使用范圍被限制在聲明它的模塊內;

逆襲人生游戲進不去?4) ?在類中的static成員變量屬于整個類所擁有,對類的所有對象只有一份拷貝;

5) ?在類中的static成員函數屬于整個類所擁有,這個函數不接收this指針,因而只能訪問類的static成員變量。

類內:

6) ?static類對象必須要在類外進行初始化,static修飾的變量先于對象存在,所以static修飾的變量要在類外初始化;

7) ?由于static修飾的類成員屬于類,不屬于對象,因此static類成員函數是沒有this指針的,this指針是指向本對象的指針。正因為沒有this指針,所以static類成員函數不能訪問非static的類成員,只能訪問 static修飾的類成員;

8) ?static成員函數不能被virtual修飾,static成員不屬于任何對象或實例,所以加上virtual沒有任何實際意義;靜態成員函數沒有this指針,虛函數的實現是為每一個對象分配一個vptr指針,而vptr是通過this指針調用的,所以不能為virtual;虛函數的調用關系,this->vptr->ctable->virtual function

51、靜態變量什么時候初始化

快穿穿進電視劇逆襲,1) ?初始化只有一次,但是可以多次賦值,在主程序之前,編譯器已經為其分配好了內存。

2) ?靜態局部變量和全局變量一樣,數據都存放在全局區域,所以在主程序之前,編譯器已經為其分配好了內存,但在C和C++中靜態局部變量的初始化節點又有點不太一樣。在C中,初始化發生在代碼執行之前,編譯階段分配好內存之后,就會進行初始化,所以我們看到在C語言中無法使用變量對靜態局部變量進行初始化,在程序運行結束,變量所處的全局內存會被全部回收。

3) ?而在C++中,初始化時在執行相關代碼時才會進行初始化,主要是由于C++引入對象后,要進行初始化必須執行相應構造函數和析構函數,在構造函數或析構函數中經常會需要進行某些程序中需要進行的特定操作,并非簡單地分配內存。所以C++標準定為全局或靜態對象是有首次用到時才會進行構造,并通過atexit()來管理。在程序結束,按照構造順序反方向進行逐個析構。所以在C++中是可以使用變量對靜態局部變量進行初始化的。

52、const關鍵字?

1) ?阻止一個變量被改變,可以使用const關鍵字。在定義該const變量時,通常需要對它進行初始化,因為以后就沒有機會再去改變它了;

2) ?對指針來說,可以指定指針本身為const,也可以指定指針所指的數據為const,或二者同時指定為const;

3) ?在一個函數聲明中,const可以修飾形參,表明它是一個輸入參數,在函數內部不能改變其值;

后進逆襲,4) ?對于類的成員函數,若指定其為const類型,則表明其是一個常函數,不能修改類的成員變量,類的常對象只能訪問類的常成員函數;

5) ?對于類的成員函數,有時候必須指定其返回值為const類型,以使得其返回值不為“左值”。

6) ?const成員函數可以訪問非const對象的非const數據成員、const數據成員,也可以訪問const對象內的所有數據成員;

7) ?非const成員函數可以訪問非const對象的非const數據成員、const數據成員,但不可以訪問const對象的任意數據成員;

8) ?一個沒有明確聲明為const的成員函數被看作是將要修改對象中數據成員的函數,而且編譯器不允許它為一個const對象所調用。因此const對象只能調用const成員函數。

9) ?const類型變量可以通過類型轉換符const_cast將const類型轉換為非const類型;

進小廠還是大廠、10) const類型變量必須定義的時候進行初始化,因此也導致如果類的成員變量有const類型的變量,那么該變量必須在類的初始化列表中進行初始化;

11) 對于函數值傳遞的情況,因為參數傳遞是通過復制實參創建一個臨時變量傳遞進函數的,函數內只能改變臨時變量,但無法改變實參。則這個時候無論加不加const對實參不會產生任何影響。但是在引用或指針傳遞函數調用中,因為傳進去的是一個引用或指針,這樣函數內部可以改變引用或指針所指向的變量,這時const 才是實實在在地保護了實參所指向的變量。因為在編譯階段編譯器對調用函數的選擇是根據實參進行的,所以,只有引用傳遞和指針傳遞可以用是否加const來重載。一個擁有頂層const的形參無法和另一個沒有頂層const的形參區分開來。

53、指針和const的用法

1) ?當const修飾指針時,由于const的位置不同,它的修飾對象會有所不同。

2) ?int *const p2中const修飾p2的值,所以理解為p2的值不可以改變,即p2只能指向固定的一個變量地址,但可以通過*p2讀寫這個變量的值。頂層指針表示指針本身是一個常量

3) ?int const *p1或者const int *p1兩種情況中const修飾*p1,所以理解為*p1的值不可以改變,即不可以給*p1賦值改變p1指向變量的值,但可以通過給p賦值不同的地址改變這個指針指向。

底層指針表示指針所指向的變量是一個常量。

54、形參與實參的區別?

如何進互聯網大廠?1) ?形參變量只有在被調用時才分配內存單元,在調用結束時, 即刻釋放所分配的內存單元。因此,形參只有在函數內部有效。函數調用結束返回主調函數后則不能再使用該形參變量。

2) ?實參可以是常量、變量、表達式、函數等, 無論實參是何種類型的量,在進行函數調用時,它們都必須具有確定的值, 以便把這些值傳送給形參。因此應預先用賦值,輸入等辦法使實參獲得確定值,會產生一個臨時變量。

3) ?實參和形參在數量上,類型上,順序上應嚴格一致, 否則會發生“類型不匹配”的錯誤。

4) ?函數調用中發生的數據傳送是單向的。即只能把實參的值傳送給形參,而不能把形參的值反向地傳送給實參。因此在函數調用過程中,形參的值發生改變,而實參中的值不會變化。

5) ?當形參和實參不是指針類型時,在該函數運行時,形參和實參是不同的變量,他們在內存中位于不同的位置,形參將實參的內容復制一份,在該函數運行結束的時候形參被釋放,而實參內容不會改變。

55、值傳遞、指針傳遞、引用傳遞的區別和效率

1) ? 值傳遞:有一個形參向函數所屬的棧拷貝數據的過程,如果值傳遞的對象是類對象 ? 或是大的結構體對象,將耗費一定的時間和空間。(傳值)

怎么才能進大廠?2) ?指針傳遞:同樣有一個形參向函數所屬的棧拷貝數據的過程,但拷貝的數據是一個固定為4字節的地址。(傳值,傳遞的是地址值)

3) ?引用傳遞:同樣有上述的數據拷貝過程,但其是針對地址的,相當于為該數據所在的地址起了一個別名。(傳地址)

4) ?效率上講,指針傳遞和引用傳遞比值傳遞效率高。一般主張使用引用傳遞,代碼邏輯上更加緊湊、清晰。

56、什么是類的繼承?

1) 類與類之間的關系

has-A包含關系,用以描述一個類由多個部件類構成,實現has-A關系用類的成員屬性表示,即一個類的成員屬性是另一個已經定義好的類;

use-A,一個類使用另一個類,通過類之間的成員函數相互聯系,定義友元或者通過傳遞參數的方式來實現;

大廠面經、is-A,繼承關系,關系具有傳遞性;

2) 繼承的相關概念

所謂的繼承就是一個類繼承了另一個類的屬性和方法,這個新的類包含了上一個類的屬性和方法,被稱為子類或者派生類,被繼承的類稱為父類或者基類;

3) 繼承的特點

子類擁有父類的所有屬性和方法,子類可以擁有父類沒有的屬性和方法,子類對象可以當做父類對象使用;

4) 繼承中的訪問控制

public、protected、private

5) 繼承中的構造和析構函數

6) 繼承中的兼容性原則

57、什么是內存池,如何實現

https://www.bilibili.com/video/BV1Kb411B7N8?p=25 C++內存管理:P23-26

https://www.bilibili.com/video/BV1db411q7B8?p=12 C++STL P11

內存池(Memory Pool) 是一種內存分配方式。通常我們習慣直接使用new、malloc 等申請內存,這樣做的缺點在于:由于所申請內存塊的大小不定,當頻繁使用時會造成大量的內存碎片并進而降低性能。內存池則是在真正使用內存之前,先申請分配一定數量的、大小相等(一般情況下)的內存塊留作備用。當有新的內存需求時,就從內存池中分出一部分內存塊, 若內存塊不夠再繼續申請新的內存。這樣做的一個顯著優點是盡量避免了內存碎片,使得內存分配效率得到提升。

這里簡單描述一下《STL源碼剖析》中的內存池實現機制:

allocate包裝malloc,deallocate包裝free

一般是一次20*2個的申請,先用一半,留著一半,為什么也沒個說法,侯捷在STL那邊書里說好像是C++委員會成員認為20是個比較好的數字,既不大也不小

  1. 首先客戶端會調用malloc()配置一定數量的區塊(固定大小的內存塊,通常為8的倍數),假設40個32bytes的區塊,其中20個區塊(一半)給程序實際使用,1個區塊交出,另外19個處于維護狀態。剩余20個(一半)留給內存池,此時一共有(20*32byte)

  2. 客戶端之后有有內存需求,想申請(20*64bytes)的空間,這時內存池只有(20*32bytes),就先將(10*64bytes)個區塊返回,1個區塊交出,另外9個處于維護狀態,此時內存池空空如也

  3. 接下來如果客戶端還有內存需求,就必須再調用malloc()配置空間,此時新申請的區塊數量會增加一個隨著配置次數越來越大的附加量,同樣一半提供程序使用,另一半留給內存池。申請內存的時候用永遠是先看內存池有無剩余,有的話就用上,然后掛在0-15號某一條鏈表上,要不然就重新申請。

  4. 如果整個堆的空間都不夠了,就會在原先已經分配區塊中尋找能滿足當前需求的區塊數量,能滿足就返回,不能滿足就向客戶端報bad_alloc異常

《STL源碼解析》侯捷 P68

allocator就是用來分配內存的,最重要的兩個函數是allocate和deallocate,就是用來申請內存和回收內存的,外部(一般指容器)調用的時候只需要知道這些就夠了。內部實現,目前的所有編譯器都是直接調用的::operator new()和::operator delete(),說白了就是和直接使用new運算符的效果是一樣的,所以老師說它們都沒做任何特殊處理。

最開始GC2.9之前:

new和 operator new 的區別:new 是個運算符,編輯器會調用 operator new(0)

operator new()里面有調用malloc的操作,那同樣的 operator delete()里面有調用的free的操作

GC2.9的alloc的一個比較好的分配器的實現規則

維護一條0-15號的一共16條鏈表,其中0表示8 bytes ,1表示 16 bytes,2表示 24bytes。。。。而15 表示 16* 8 = 128bytes,如果在申請時并不是8的倍數,那就找剛好能滿足內存大小的那個位置。比如想申請 12,那就是找16了,想申請 20 ,那就找 24 了

但是現在GC4.9及其之后 也還有,變成_pool_alloc這個名字了,不再是默認的了,你需要自己去指定它可以自己指定,比如說vector<string,__gnu_cxx::pool_allocvec;這樣來使用它,現在用的又回到以前那種對malloc和free的包裝形式了

58、從匯編層去解釋一下引用

9:??????int?x?=?1;00401048??mov?????dword?ptr?[ebp-4],110:?????int?&b?=?x;0040104F???lea?????eax,[ebp-4]00401052??mov?????dword?ptr?[ebp-8],eax

x的地址為ebp-4,b的地址為ebp-8,因為棧內的變量內存是從高往低進行分配的,所以b的地址比x的低。

lea eax,[ebp-4] 這條語句將x的地址ebp-4放入eax寄存器

mov dword ptr [ebp-8],eax 這條語句將eax的值放入b的地址

ebp-8中上面兩條匯編的作用即:將x的地址存入變量b中,這不和將某個變量的地址存入指針變量是一樣的嗎?所以從匯編層次來看,的確引用是通過指針來實現的。

59、深拷貝與淺拷貝是怎么回事?

1) ?淺復制 :只是拷貝了基本類型的數據,而引用類型數據,復制后也是會發生引用,我們把這種拷貝叫做“(淺復制)淺拷貝”,換句話說,淺復制僅僅是指向被復制的內存地址,如果原地址中對象被改變了,那么淺復制出來的對象也會相應改變。

深復制 :在計算機中開辟了一塊新的內存地址用于存放復制的對象。

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

60、C++模板是什么,你知道底層怎么實現的?

1) ?編譯器并不是把函數模板處理成能夠處理任意類的函數;編譯器從函數模板通過具體類型產生不同的函數;編譯器會對函數模板進行兩次編譯:在聲明的地方對模板代碼本身進行編譯,在調用的地方對參數替換后的代碼進行編譯。

2) ?這是因為函數模板要被實例化后才能成為真正的函數,在使用函數模板的源文件中包含函數模板的頭文件,如果該頭文件中只有聲明,沒有定義,那編譯器無法實例化該模板,最終導致鏈接錯誤。

61、new和malloc的區別?

1、 new/delete是C++關鍵字,需要編譯器支持。malloc/free是庫函數,需要頭文件支持;

2、 使用new操作符申請內存分配時無須指定內存塊的大小,編譯器會根據類型信息自行計算。而malloc則需要顯式地指出所需內存的尺寸。

3、 new操作符內存分配成功時,返回的是對象類型的指針,類型嚴格與對象匹配,無須進行類型轉換,故new是符合類型安全性的操作符。而malloc內存分配成功則是返回void * ,需要通過強制類型轉換將void*指針轉換成我們需要的類型。

4、 new內存分配失敗時,會拋出bac_alloc異常。malloc分配內存失敗時返回NULL。

5、 new會先調用operator new函數,申請足夠的內存(通常底層使用malloc實現)。然后調用類型的構造函數,初始化成員變量,最后返回自定義類型指針。delete先調用析構函數,然后調用operator delete函數釋放內存(通常底層使用free實現)。malloc/free是庫函數,只能動態的申請和釋放內存,無法強制要求其做自定義類型對象構造和析構工作。

62、delete p、delete [] p、allocator都有什么作用?

1、 動態數組管理new一個數組時,[]中必須是一個整數,但是不一定是常量整數,普通數組必須是一個常量整數;

2、 new動態數組返回的并不是數組類型,而是一個元素類型的指針;

3、 delete[]時,數組中的元素按逆序的順序進行銷毀;

4、 new在內存分配上面有一些局限性,new的機制是將內存分配和對象構造組合在一起,同樣的,delete也是將對象析構和內存釋放組合在一起的。allocator將這兩部分分開進行,allocator申請一部分內存,不進行初始化對象,只有當需要的時候才進行初始化操作。

63、new和delete的實現原理, delete是如何知道釋放內存的大小的額?

1、 new簡單類型直接調用operator new分配內存;

而對于復雜結構,先調用operator new分配內存,然后在分配的內存上調用構造函數;

對于簡單類型,new[]計算好大小后調用operator new;

對于復雜數據結構,new[]先調用operator new[]分配內存,然后在p的前四個字節寫入數組大小n,然后調用n次構造函數,針對復雜類型,new[]會額外存儲數組大小;

① ? new表達式調用一個名為operator new(operator new[])函數,分配一塊足夠大的、原始的、未命名的內存空間;

② ? 編譯器運行相應的構造函數以構造這些對象,并為其傳入初始值;

③ ? 對象被分配了空間并構造完成,返回一個指向該對象的指針。

2、 delete簡單數據類型默認只是調用free函數;復雜數據類型先調用析構函數再調用operator delete;針對簡單類型,delete和delete[]等同。假設指針p指向new[]分配的內存。因為要4字節存儲數組大小,實際分配的內存地址為[p-4],系統記錄的也是這個地址。delete[]實際釋放的就是p-4指向的內存。而delete會直接釋放p指向的內存,這個內存根本沒有被系統記錄,所以會崩潰。

3、 需要在 new [] 一個對象數組時,需要保存數組的維度,C++ 的做法是在分配數組空間時多分配了 4 個字節的大小,專門保存數組的大小,在 delete [] 時就可以取出這個保存的數,就知道了需要調用析構函數多少次了。

64、malloc申請的存儲空間能用delete釋放嗎

不能,malloc /free主要為了兼容C,new和delete 完全可以取代malloc /free的。

malloc /free的操作對象都是必須明確大小的,而且不能用在動態類上。

new 和delete會自動進行類型檢查和大小,malloc/free不能執行構造函數與析構函數,所以動態對象它是不行的。

當然從理論上說使用malloc申請的內存是可以通過delete釋放的。不過一般不這樣寫的。而且也不能保證每個C++的運行時都能正常。

65、malloc與free的實現原理?

1、 在標準C庫中,提供了malloc/free函數分配釋放內存,這兩個函數底層是由brk、mmap、,munmap這些系統調用實現的;

2、 brk是將數據段(.data)的最高地址指針_edata往高地址推,mmap是在進程的虛擬地址空間中(堆和棧中間,稱為文件映射區域的地方)找一塊空閑的虛擬內存。這兩種方式分配的都是虛擬內存,沒有分配物理內存。在第一次訪問已分配的虛擬地址空間的時候,發生缺頁中斷,操作系統負責分配物理內存,然后建立虛擬內存和物理內存之間的映射關系;

3、 malloc小于128k的內存,使用brk分配內存,將_edata往高地址推;malloc大于128k的內存,使用mmap分配內存,在堆和棧之間找一塊空閑內存分配;brk分配的內存需要等到高地址內存釋放以后才能釋放,而mmap分配的內存可以單獨釋放。當最高地址空間的空閑內存超過128K(可由M_TRIM_THRESHOLD選項調節)時,執行內存緊縮操作(trim)。在上一個步驟free的時候,發現最高地址空閑內存超過128K,于是內存緊縮。

4、 malloc是從堆里面申請內存,也就是說函數返回的指針是指向堆里面的一塊內存。操作系統中有一個記錄空閑內存地址的鏈表。當操作系統收到程序的申請時,就會遍歷該鏈表,然后就尋找第一個空間大于所申請空間的堆結點,然后就將該結點從空閑結點鏈表中刪除,并將該結點的空間分配給程序。

66、malloc、realloc、calloc的區別

1) ? malloc函數

void*?malloc(unsigned?int?num_size);int?*p?=?malloc(20*sizeof(int));申請20個int類型的空間;

2) ? calloc函數

void*?calloc(size_t?n,size_t?size);int?*p?=?calloc(20,?sizeof(int));

省去了人為空間計算;malloc申請的空間的值是隨機初始化的,calloc申請的空間的值是初始化為0的;

3) ? realloc函數

void?realloc(void?*p,?size_t?new_size);

給動態分配的空間分配額外的空間,用于擴充容量。

67、類成員初始化方式?構造函數的執行順序 ?為什么用成員初始化列表會快一些?

1) ?賦值初始化,通過在函數體內進行賦值初始化;列表初始化,在冒號后使用初始化列表進行初始化。

這兩種方式的主要區別在于:

對于在函數體中初始化,是在所有的數據成員被分配內存空間后才進行的。

列表初始化是給數據成員分配內存空間時就進行初始化,就是說分配一個數據成員只要冒號后有此數據成員的賦值表達式(此表達式必須是括號賦值表達式),那么分配了內存空間后在進入函數體之前給數據成員賦值,就是說初始化這個數據成員此時函數體還未執行。

2) ?一個派生類構造函數的執行順序如下:

① ? 虛擬基類的構造函數(多個虛擬基類則按照繼承的順序執行構造函數)。

② ? 基類的構造函數(多個普通基類也按照繼承的順序執行構造函數)。

③ ? 類類型的成員對象的構造函數(按照初始化順序)

④ ? 派生類自己的構造函數。

3) ?方法一是在構造函數當中做賦值的操作,而方法二是做純粹的初始化操作。我們都知道,C++的賦值操作是會產生臨時對象的。臨時對象的出現會降低程序的效率。

68、成員列表初始化?

1) ?必須使用成員初始化的四種情況

① ? ?當初始化一個引用成員時;

② ? ?當初始化一個常量成員時;

③ ? ?當調用一個基類的構造函數,而它擁有一組參數時;

④ ? ?當調用一個成員類的構造函數,而它擁有一組參數時;

2) ?成員初始化列表做了什么

① ? ?編譯器會一一操作初始化列表,以適當的順序在構造函數之內安插初始化操作,并且在任何顯示用戶代碼之前;

② ? ?list中的項目順序是由類中的成員聲明順序決定的,不是由初始化列表的順序決定的;

69、什么是內存泄露,如何檢測與避免

內存泄露

一般我們常說的內存泄漏是指堆內存的泄漏。堆內存是指程序從堆中分配的,大小任意的(內存塊的大小可以在程序運行期決定)內存塊,使用完后必須顯式釋放的內存。應用程序般使用malloc,、realloc、 new等函數從堆中分配到塊內存,使用完后,程序必須負責相應的調用free或delete釋放該內存塊,否則,這塊內存就不能被再次使用,我們就說這塊內存泄漏了

避免內存泄露的幾種方式

  • 計數法:使用new或者malloc時,讓該數+1,delete或free時,該數-1,程序執行完打印這個計數,如果不為0則表示存在內存泄露

  • 一定要將基類的析構函數聲明為虛函數

  • 對象數組的釋放一定要用delete []

  • 有new就有delete,有malloc就有free,保證它們一定成對出現

檢測工具

  • Linux下可以使用Valgrind工具

  • Windows下可以使用CRT庫

70、對象復用的了解,零拷貝的了解

對象復用

對象復用其本質是一種設計模式:Flyweight享元模式。

通過將對象存儲到“對象池”中實現對象的重復利用,這樣可以避免多次創建重復對象的開銷,節約系統資源。

零拷貝

零拷貝就是一種避免 CPU 將數據從一塊存儲拷貝到另外一塊存儲的技術。

零拷貝技術可以減少數據拷貝和共享總線操作的次數。

在C++中,vector的一個成員函數emplace_back()很好地體現了零拷貝技術,它跟push_back()函數一樣可以將一個元素插入容器尾部,區別在于:使用push_back()函數需要調用拷貝構造函數和轉移構造函數,而使用emplace_back()插入的元素原地構造,不需要觸發拷貝構造和轉移構造,效率更高。舉個例子:

#include?<vector>
#include?<string>
#include?<iostream>
using?namespace?std;struct?Person
{string?name;int?age;//初始構造函數Person(string?p_name,?int?p_age):?name(std::move(p_name)),?age(p_age){cout?<<?"I?have?been?constructed"?<<endl;}//拷貝構造函數Person(const?Person&?other):?name(std::move(other.name)),?age(other.age){cout?<<?"I?have?been?copy?constructed"?<<endl;}//轉移構造函數Person(Person&&?other):?name(std::move(other.name)),?age(other.age){cout?<<?"I?have?been?moved"<<endl;}
};int?main()
{vector<Person>?e;cout?<<?"emplace_back:"?<<endl;e.emplace_back("Jane",?23);?//不用構造類對象vector<Person>?p;cout?<<?"push_back:"<<endl;p.push_back(Person("Mike",36));return?0;
}
//輸出結果:
//emplace_back:
//I?have?been?constructed
//push_back:
//I?have?been?constructed
//I?am?being?moved.

71、解釋一下什么是trivial destructor

trivial destructor”一般是指用戶沒有自定義析構函數,而由系統生成的,這種析構函數在《STL源碼解析》中成為“無關痛癢”的析構函數。

反之,用戶自定義了析構函數,則稱之為“non-trivial destructor”,這種析構函數如果申請了新的空間一定要顯式的釋放,否則會造成內存泄露

對于trivial destructor,如果每次都進行調用,顯然對效率是一種傷害,如何進行判斷呢?《STL源碼解析》中給出的說明是:

首先利用value_type()獲取所指對象的型別,再利用__type_traits判斷該型別的析構函數是否trivial,若是(__true_type),則什么也不做,若為(__false_type),則去調用destory()函數

也就是說,在實際的應用當中,STL庫提供了相關的判斷方法__type_traits,感興趣的讀者可以自行查閱使用方式。除了trivial destructor,還有trivial construct、trivial copy construct等,如果能夠對是否trivial進行區分,可以采用內存處理函數memcpy()、malloc()等更加高效的完成相關操作,提升效率。

《C++中的 trivial destructor》:https://blog.csdn.net/wudishine/article/details/12307611

72、介紹面向對象的三大特性,并且舉例說明

三大特性:繼承、封裝和多態

(1)繼承

讓某種類型對象獲得另一個類型對象的屬性和方法。

它可以使用現有類的所有功能,并在無需重新編寫原來的類的情況下對這些功能進行擴展

常見的繼承有三種方式:

  1. 實現繼承:指使用基類的屬性和方法而無需額外編碼的能力

  2. 接口繼承:指僅使用屬性和方法的名稱、但是子類必須提供實現的能力

  3. 可視繼承:指子窗體(類)使用基窗體(類)的外觀和實現代碼的能力(C++里好像不怎么用)

例如,將人定義為一個抽象類,擁有姓名、性別、年齡等公共屬性,吃飯、睡覺、走路等公共方法,在定義一個具體的人時,就可以繼承這個抽象類,既保留了公共屬性和方法,也可以在此基礎上擴展跳舞、唱歌等特有方法

(2)封裝

數據和代碼捆綁在一起,避免外界干擾和不確定性訪問。

封裝,也就是把客觀事物封裝成抽象的類,并且類可以把自己的數據和方法只讓可信的類或者對象操作,對不可信的進行信息隱藏,例如:將公共的數據或方法使用public修飾,而不希望被訪問的數據或方法采用private修飾。

(3)多態 ?

同一事物表現出不同事物的能力,即向不同對象發送同一消息,不同的對象在接收時會產生不同的行為(重載實現編譯時多態,虛函數實現運行時多態)

多態性是允許你將父對象設置成為和一個或更多的他的子對象相等的技術,賦值之后,父對象就可以根據當前賦值給它的子對象的特性以不同的方式運作。簡單一句話:允許將子類類型的指針賦值給父類類型的指針

實現多態有二種方式:覆蓋(override),重載(overload)。覆蓋:是指子類重新定義父類的虛函數的做法。重載:是指允許存在多個同名函數,而這些函數的參數表不同(或許參數個數不同,或許參數類型不同,或許兩者都不同)。例如:基類是一個抽象對象——人,那教師、運動員也是人,而使用這個抽象對象既可以表示教師、也可以表示運動員。

《C++封裝繼承多態總結》:https://blog.csdn.net/IOT_SHUN/article/details/79674293

73、C++中類的數據成員和成員函數內存分布情況

C++類是由結構體發展得來的,所以他們的成員變量(C語言的結構體只有成員變量)的內存分配機制是一樣的。下面我們以類來說明問題,如果類的問題通了,結構體也也就沒問題啦。類分為成員變量和成員函數,我們先來討論成員變量。

一個類對象的地址就是類所包含的這一片內存空間的首地址,這個首地址也就對應具體某一個成員變量的地址。(在定義類對象的同時這些成員變量也就被定義了),舉個例子:

#include?<iostream>
using?namespace?std;class?Person
{
public:Person(){this->age?=?23;}void?printAge(){cout?<<?this->age?<<endl;}~Person(){}
public:int?age;
};int?main()
{Person?p;cout?<<?"對象地址:"<<?&p?<<endl;cout?<<?"age地址:"<<?&(p.age)?<<endl;cout?<<?"對象大小:"<<?sizeof(p)?<<endl;cout?<<?"age大小:"<<?sizeof(p.age)?<<endl;return?0;
}
//輸出結果
//對象地址:0x7fffec0f15a8
//age地址:0x7fffec0f15a8
//對象大小:4
//age大小:4

從代碼運行結果來看,對象的大小和對象中數據成員的大小是一致的,也就是說,成員函數不占用對象的內存。這是因為所有的函數都是存放在代碼區的,不管是全局函數,還是成員函數。要是成員函數占用類的對象空間,那么將是多么可怕的事情:定義一次類對象就有成員函數占用一段空間。我們再來補充一下靜態成員函數的存放問題:靜態成員函數與一般成員函數的唯一區別就是沒有this指針,因此不能訪問非靜態數據成員,就像我前面提到的,所有函數都存放在代碼區,靜態函數也不例外。所有有人一看到 static 這個單詞就主觀的認為是存放在全局數據區,那是不對的。

《C++類對象成員變量和函數內存分配的問題》:https://blog.csdn.net/z2664836046/article/details/78967313

74、成員初始化列表的概念,為什么用它會快一些?

成員初始化列表的概念

在類的構造函數中,不在函數體內對成員變量賦值,而是在構造函數的花括號前面使用冒號和初始化列表賦值

效率

用初始化列表會快一些的原因是,對于類型,它少了一次調用構造函數的過程,而在函數體中賦值則會多一次調用。而對于內置數據類型則沒有差別。舉個例子:

#include?<iostream>
using?namespace?std;
class?A
{
public:A(){cout?<<?"默認構造函數A()"?<<?endl;}A(int?a){value?=?a;cout?<<?"A(int?"<<value<<")"?<<?endl;}A(const?A&?a){value?=?a.value;cout?<<?"拷貝構造函數A(A&?a):??"<<value?<<?endl;}int?value;
};class?B
{
public:B()?:?a(1){b?=?A(2);}A?a;A?b;
};
int?main()
{B?b;
}//輸出結果:
//A(int?1)
//默認構造函數A()
//A(int?2)

從代碼運行結果可以看出,在構造函數體內部初始化的對象b多了一次構造函數的調用過程,而對象a則沒有。由于對象成員變量的初始化動作發生在進入構造函數之前,對于內置類型沒什么影響,但如果有些成員是類,那么在進入構造函數之前,會先調用一次默認構造函數,進入構造函數后所做的事其實是一次賦值操作(對象已存在),所以如果是在構造函數體內進行賦值的話,等于是一次默認構造加一次賦值,而初始化列表只做一次賦值操作。

《為什么用成員初始化列表會快一些?》:https://blog.csdn.net/JackZhang_123/article/details/82590368

75、(超重要)構造函數為什么不能為虛函數?析構函數為什么要虛函數?

1、 從存儲空間角度,虛函數相應一個指向vtable虛函數表的指針,這大家都知道,但是這個指向vtable的指針事實上是存儲在對象的內存空間的。

問題出來了,假設構造函數是虛的,就須要通過 vtable來調用,但是對象還沒有實例化,也就是內存空間還沒有,怎么找vtable呢?所以構造函數不能是虛函數。

2、 從使用角度,虛函數主要用于在信息不全的情況下,能使重載的函數得到相應的調用。

構造函數本身就是要初始化實例,那使用虛函數也沒有實際意義呀。

所以構造函數沒有必要是虛函數。虛函數的作用在于通過父類的指針或者引用來調用它的時候可以變成調用子類的那個成員函數。而構造函數是在創建對象時自己主動調用的,不可能通過父類的指針或者引用去調用,因此也就規定構造函數不能是虛函數。

3、構造函數不須要是虛函數,也不同意是虛函數,由于創建一個對象時我們總是要明白指定對象的類型,雖然我們可能通過實驗室的基類的指針或引用去訪問它但析構卻不一定,我們往往通過基類的指針來銷毀對象。這時候假設析構函數不是虛函數,就不能正確識別對象類型從而不能正確調用析構函數。

4、從實現上看,vbtl在構造函數調用后才建立,因而構造函數不可能成為虛函數從實際含義上看,在調用構造函數時還不能確定對象的真實類型(由于子類會調父類的構造函數);并且構造函數的作用是提供初始化,在對象生命期僅僅運行一次,不是對象的動態行為,也沒有必要成為虛函數。

5、當一個構造函數被調用時,它做的首要的事情之中的一個是初始化它的VPTR。

因此,它僅僅能知道它是“當前”類的,而全然忽視這個對象后面是否還有繼承者。當編譯器為這個構造函數產生代碼時,它是為這個類的構造函數產生代碼——既不是為基類,也不是為它的派生類(由于類不知道誰繼承它)。所以它使用的VPTR必須是對于這個類的VTABLE。

并且,僅僅要它是最后的構造函數調用,那么在這個對象的生命期內,VPTR將保持被初始化為指向這個VTABLE, 但假設接著另一個更晚派生的構造函數被調用,這個構造函數又將設置VPTR指向它的 VTABLE,等.直到最后的構造函數結束。

VPTR的狀態是由被最后調用的構造函數確定的。這就是為什么構造函數調用是從基類到更加派生類順序的還有一個理由。可是,當這一系列構造函數調用正發生時,每一個構造函數都已經設置VPTR指向它自己的VTABLE。假設函數調用使用虛機制,它將僅僅產生通過它自己的VTABLE的調用,而不是最后的VTABLE(全部構造函數被調用后才會有最后的VTABLE)。

因為構造函數本來就是為了明確初始化對象成員才產生的,然而virtual function主要是為了再不完全了解細節的情況下也能正確處理對象。另外,virtual函數是在不同類型的對象產生不同的動作,現在對象還沒有產生,如何使用virtual函數來完成你想完成的動作。

直接的講,C++中基類采用virtual虛析構函數是為了防止內存泄漏。

具體地說,如果派生類中申請了內存空間,并在其析構函數中對這些內存空間進行釋放。假設基類中采用的是非虛析構函數,當刪除基類指針指向的派生類對象時就不會觸發動態綁定,因而只會調用基類的析構函數,而不會調用派生類的析構函數。那么在這種情況下,派生類中申請的空間就得不到釋放從而產生內存泄漏。

所以,為了防止這種情況的發生,C++中基類的析構函數應采用virtual虛析構函數。

76、析構函數的作用,如何起作用?

1) ?構造函數只是起初始化值的作用,但實例化一個對象的時候,可以通過實例去傳遞參數,從主函數傳遞到其他的函數里面,這樣就使其他的函數里面有值了。

規則,只要你一實例化對象,系統自動回調用一個構造函數就是你不寫,編譯器也自動調用一次。

2) ?析構函數與構造函數的作用相反,用于撤銷對象的一些特殊任務處理,可以是釋放對象分配的內存空間;特點:析構函數與構造函數同名,但該函數前面加~。

析構函數沒有參數,也沒有返回值,而且不能重載,在一個類中只能有一個析構函數。當撤銷對象時,編譯器也會自動調用析構函數。

每一個類必須有一個析構函數,用戶可以自定義析構函數,也可以是編譯器自動生成默認的析構函數。一般析構函數定義為類的公有成員。

77、構造函數和析構函數可以調用虛函數嗎,為什么

1) 在C++中,提倡不在構造函數和析構函數中調用虛函數;

2) 構造函數和析構函數調用虛函數時都不使用動態聯編,如果在構造函數或析構函數中調用虛函數,則運行的是為構造函數或析構函數自身類型定義的版本;

3) 因為父類對象會在子類之前進行構造,此時子類部分的數據成員還未初始化,因此調用子類的虛函數時不安全的,故而C++不會進行動態聯編;

4) 析構函數是用來銷毀一個對象的,在銷毀一個對象時,先調用子類的析構函數,然后再調用基類的析構函數。所以在調用基類的析構函數時,派生類對象的數據成員已經銷毀,這個時候再調用子類的虛函數沒有任何意義。

78、構造函數、析構函數的執行順序?構造函數和拷貝構造的內部都干了啥?

1) ? ? 構造函數順序

① ? 基類構造函數。如果有多個基類,則構造函數的調用順序是某類在類派生表中出現的順序,而不是它們在成員初始化表中的順序。

② ? 成員類對象構造函數。如果有多個成員類對象則構造函數的調用順序是對象在類中被聲明的順序,而不是它們出現在成員初始化表中的順序。

③ ? 派生類構造函數。

2) ? ? 析構函數順序

① ? 調用派生類的析構函數;

② ? 調用成員類對象的析構函數;

③ ? 調用基類的析構函數。

79、虛析構函數的作用,父類的析構函數是否要設置為虛函數?

1) ?C++中基類采用virtual虛析構函數是為了防止內存泄漏。

具體地說,如果派生類中申請了內存空間,并在其析構函數中對這些內存空間進行釋放。

假設基類中采用的是非虛析構函數,當刪除基類指針指向的派生類對象時就不會觸發動態綁定,因而只會調用基類的析構函數,而不會調用派生類的析構函數。

那么在這種情況下,派生類中申請的空間就得不到釋放從而產生內存泄漏。

所以,為了防止這種情況的發生,C++中基類的析構函數應采用virtual虛析構函數。

2) ?純虛析構函數一定得定義,因為每一個派生類析構函數會被編譯器加以擴張,以靜態調用的方式調用其每一個虛基類以及上一層基類的析構函數。

因此,缺乏任何一個基類析構函數的定義,就會導致鏈接失敗,最好不要把虛析構函數定義為純虛析構函數。

80、構造函數析構函數可否拋出異常

1) ? C++只會析構已經完成的對象,對象只有在其構造函數執行完畢才算是完全構造妥當。在構造函數中發生異常,控制權轉出構造函數之外。

因此,在對象b的構造函數中發生異常,對象b的析構函數不會被調用。因此會造成內存泄漏。

2) ?用auto_ptr對象來取代指針類成員,便對構造函數做了強化,免除了拋出異常時發生資源泄漏的危機,不再需要在析構函數中手動釋放資源;

3) ?如果控制權基于異常的因素離開析構函數,而此時正有另一個異常處于作用狀態,C++會調用terminate函數讓程序結束;

4) ?如果異常從析構函數拋出,而且沒有在當地進行捕捉,那個析構函數便是執行不全的。如果析構函數執行不全,就是沒有完成他應該執行的每一件事情。

81、構造函數一般不定義為虛函數的原因

(1)創建一個對象時需要確定對象的類型,而虛函數是在運行時動態確定其類型的。在構造一個對象時,由于對象還未創建成功,編譯器無法知道對象的實際類型

(2)虛函數的調用需要虛函數表指針vptr,而該指針存放在對象的內存空間中,若構造函數聲明為虛函數,那么由于對象還未創建,還沒有內存空間,更沒有虛函數表vtable地址用來調用虛構造函數了

(3)虛函數的作用在于通過父類的指針或者引用調用它的時候能夠變成調用子類的那個成員函數。而構造函數是在創建對象時自動調用的,不可能通過父類或者引用去調用,因此就規定構造函數不能是虛函數

(4)析構函數一般都要聲明為虛函數,這個應該是老生常談了,這里不再贅述

《為什么C++不能有虛構造函數,卻可以有虛析構函數》:https://dwz.cn/lnfW9H6m

82、類什么時候會析構?

1) ?對象生命周期結束,被銷毀時;

2) ?delete指向對象的指針時,或delete指向對象的基類類型指針,而其基類虛構函數是虛函數時;

3) ?對象i是對象o的成員,o的析構函數被調用時,對象i的析構函數也被調用。

83、構造函數或者析構函數中可以調用虛函數嗎

簡要結論:

  • 從語法上講,調用完全沒有問題。

  • 但是從效果上看,往往不能達到需要的目的。

《Effective C++》的解釋是:
派生類對象構造期間進入基類的構造函數時,對象類型變成了基類類型,而不是派生類類型。同樣,進入基類析構函數時,對象也是基類類型。

舉個例子:

#include<iostream>
using?namespace?std;class?Base
{
public:Base(){Function();}virtual?void?Function(){cout?<<?"Base::Fuction"?<<?endl;}~Base(){Function();}
};class?A?:?public?Base
{
public:A(){Function();}virtual?void?Function(){cout?<<?"A::Function"?<<?endl;}~A(){Function();}
};int?main()
{Base*?a?=?new?Base;delete?a;cout?<<?"-------------------------"?<<endl;Base*?b?=?new?A;//語句1delete?b;
}
//輸出結果
//Base::Fuction
//Base::Fuction
//-------------------------
//Base::Fuction
//A::Function
//Base::Fuction

語句1講道理應該體現多態性,執行類A中的構造和析構函數,從實驗結果來看,語句1并沒有體現,執行流程是先構造基類,所以先調用基類的構造函數,構造完成再執行A自己的構造函數,析構時也是調用基類的析構函數,也就是說構造和析構中調用虛函數并不能達到目的,應該避免

《構造函數或者析構函數中調用虛函數會怎么樣?》:https://dwz.cn/TaJTJONX

84、智能指針的原理、常用的智能指針及實現

原理

智能指針是一個類,用來存儲指向動態分配對象的指針,負責自動釋放動態分配的對象,防止堆內存泄漏。動態分配的資源,交給一個類對象去管理,當類對象聲明周期結束時,自動調用析構函數釋放資源

常用的智能指針

(1) shared_ptr

實現原理:采用引用計數器的方法,允許多個智能指針指向同一個對象,每當多一個指針指向該對象時,指向該對象的所有智能指針內部的引用計數加1,每當減少一個智能指針指向對象時,引用計數會減1,當計數為0的時候會自動的釋放動態分配的資源。

  • 智能指針將一個計數器與類指向的對象相關聯,引用計數器跟蹤共有多少個類對象共享同一指針

  • 每次創建類的新對象時,初始化指針并將引用計數置為1

  • 當對象作為另一對象的副本而創建時,拷貝構造函數拷貝指針并增加與之相應的引用計數

  • 對一個對象進行賦值時,賦值操作符減少左操作數所指對象的引用計數(如果引用計數為減至0,則刪除對象),并增加右操作數所指對象的引用計數

  • 調用析構函數時,構造函數減少引用計數(如果引用計數減至0,則刪除基礎對象)

(2) unique_ptr

unique_ptr采用的是獨享所有權語義,一個非空的unique_ptr總是擁有它所指向的資源。轉移一個unique_ptr將會把所有權全部從源指針轉移給目標指針,源指針被置空;所以unique_ptr不支持普通的拷貝和賦值操作,不能用在STL標準容器中;局部變量的返回值除外(因為編譯器知道要返回的對象將要被銷毀);如果你拷貝一個unique_ptr,那么拷貝結束后,這兩個unique_ptr都會指向相同的資源,造成在結束時對同一內存指針多次釋放而導致程序崩潰。

(3) weak_ptr

weak_ptr:弱引用。引用計數有一個問題就是互相引用形成環(環形引用),這樣兩個指針指向的內存都無法釋放。需要使用weak_ptr打破環形引用。weak_ptr是一個弱引用,它是為了配合shared_ptr而引入的一種智能指針,它指向一個由shared_ptr管理的對象而不影響所指對象的生命周期,也就是說,它只引用,不計數。如果一塊內存被shared_ptr和weak_ptr同時引用,當所有shared_ptr析構了之后,不管還有沒有weak_ptr引用該內存,內存也會被釋放。所以weak_ptr不保證它指向的內存一定是有效的,在使用之前使用函數lock()檢查weak_ptr是否為空指針。

(4) auto_ptr

主要是為了解決“有異常拋出時發生內存泄漏”的問題 。因為發生異常而無法正常釋放內存。

auto_ptr有拷貝語義,拷貝后源對象變得無效,這可能引發很嚴重的問題;而unique_ptr則無拷貝語義,但提供了移動語義,這樣的錯誤不再可能發生,因為很明顯必須使用std::move()進行轉移。

auto_ptr不支持拷貝和賦值操作,不能用在STL標準容器中。STL容器中的元素經常要支持拷貝、賦值操作,在這過程中auto_ptr會傳遞所有權,所以不能在STL中使用。

智能指針shared_ptr代碼實現:

template<typename?T>
class?SharedPtr
{
public:SharedPtr(T*?ptr?=?NULL):_ptr(ptr),?_pcount(new?int(1)){}SharedPtr(const?SharedPtr&?s):_ptr(s._ptr),?_pcount(s._pcount){*(_pcount)++;}SharedPtr<T>&?operator=(const?SharedPtr&?s){if?(this?!=?&s){if?(--(*(this->_pcount))?==?0){delete?this->_ptr;delete?this->_pcount;}_ptr?=?s._ptr;_pcount?=?s._pcount;*(_pcount)++;}return?*this;}T&?operator*(){return?*(this->_ptr);}T*?operator->(){return?this->_ptr;}~SharedPtr(){--(*(this->_pcount));if?(this->_pcount?==?0){delete?_ptr;_ptr?=?NULL;delete?_pcount;_pcount?=?NULL;}}
private:T*?_ptr;int*?_pcount;//指向引用計數的指針
};

《智能指針的原理及實現》:https://blog.csdn.net/lizhentao0707/article/details/81156384

85、構造函數的幾種關鍵字

default

default關鍵字可以顯式要求編譯器生成合成構造函數,防止在調用時相關構造函數類型沒有定義而報錯

#include?<iostream>
using?namespace?std;class?CString
{
public:CString()?=?default;?//語句1//構造函數CString(const?char*?pstr)?:?_str(pstr){}void*?operator?new()?=?delete;//這樣不允許使用new關鍵字//析構函數~CString(){}
public:string?_str;
};int?main()
{auto?a?=?new?CString();?//語句2cout?<<?"Hello?World"?<<endl;return?0;
}
//運行結果
//Hello?World

如果沒有加語句1,語句2會報錯,表示找不到參數為空的構造函數,將其設置為default可以解決這個問題

delete

delete關鍵字可以刪除構造函數、賦值運算符函數等,這樣在使用的時候會得到友善的提示

#include?<iostream>
using?namespace?std;class?CString
{
public:void*?operator?new()?=?delete;//這樣不允許使用new關鍵字//析構函數~CString(){}
};int?main()
{auto?a?=?new?CString();?//語句1cout?<<?"Hello?World"?<<endl;return?0;
}

在執行語句1時,會提示new方法已經被刪除,如果將new設置為私有方法,則會報慘不忍睹的錯誤,因此使用delete關鍵字可以更加人性化的刪除一些默認方法

=0

將虛函數定義為純虛函數(純虛函數無需定義,= 0只能出現在類內部虛函數的聲明語句處;當然,也可以為純虛函數提供定義,不過函數體必須定義在類的外部)

《C++構造函數的default和delete》:https://blog.csdn.net/u010591680/article/details/71101737

86、C++的四種強制轉換reinterpret_cast/const_cast/static_cast /dynamic_cast

reinterpret_cast

reinterpret_cast(expression)

type-id 必須是一個指針、引用、算術類型、函數指針或者成員指針。它可以用于類型之間進行強制轉換。

const_cast

const_cast(expression)

該運算符用來修改類型的const或volatile屬性。除了const 或volatile修飾之外, type_id和expression的類型是一樣的。用法如下:

  • 常量指針被轉化成非常量的指針,并且仍然指向原來的對象

  • 常量引用被轉換成非常量的引用,并且仍然指向原來的對象

  • const_cast一般用于修改底指針。如const char *p形式

static_cast

static_cast < type-id > (expression)

該運算符把expression轉換為type-id類型,但沒有運行時類型檢查來保證轉換的安全性。它主要有如下幾種用法:

  • 用于類層次結構中基類(父類)和派生類(子類)之間指針或引用引用的轉換

  • 進行上行轉換(把派生類的指針或引用轉換成基類表示)是安全的

  • 進行下行轉換(把基類指針或引用轉換成派生類表示)時,由于沒有動態類型檢查,所以是不安全的

  • 用于基本數據類型之間的轉換,如把int轉換成char,把int轉換成enum。這種轉換的安全性也要開發人員來保證。

  • 把空指針轉換成目標類型的空指針

  • 把任何類型的表達式轉換成void類型

注意:static_cast不能轉換掉expression的const、volatile、或者__unaligned屬性。

dynamic_cast

有類型檢查,基類向派生類轉換比較安全,但是派生類向基類轉換則不太安全

dynamic_cast(expression)

該運算符把expression轉換成type-id類型的對象。type-id 必須是類的指針、類的引用或者void*

如果 type-id 是類指針類型,那么expression也必須是一個指針,如果 type-id 是一個引用,那么 expression 也必須是一個引用

dynamic_cast運算符可以在執行期決定真正的類型,也就是說expression必須是多態類型。如果下行轉換是安全的(也就說,如果基類指針或者引用確實指向一個派生類對象)這個運算符會傳回適當轉型過的指針。如果 如果下行轉換不安全,這個運算符會傳回空指針(也就是說,基類指針或者引用沒有指向一個派生類對象)

dynamic_cast主要用于類層次間的上行轉換和下行轉換,還可以用于類之間的交叉轉換

在類層次間進行上行轉換時,dynamic_cast和static_cast的效果是一樣的

在進行下行轉換時,dynamic_cast具有類型檢查的功能,比static_cast更安全

舉個例子:

#include?<bits/stdc++.h>
using?namespace?std;class?Base
{
public:Base()?:b(1)?{}virtual?void?fun()?{};int?b;
};class?Son?:?public?Base
{
public:Son()?:d(2)?{}int?d;
};int?main()
{int?n?=?97;//reinterpret_castint?*p?=?&n;//以下兩者效果相同char?*c?=?reinterpret_cast<char*>?(p);?char?*c2?=??(char*)(p);cout?<<?"reinterpret_cast輸出:"<<?*c2?<<?endl;//const_castconst?int?*p2?=?&n;int?*p3?=?const_cast<int*>(p2);*p3?=?100;cout?<<?"const_cast輸出:"?<<?*p3?<<?endl;Base*?b1?=?new?Son;Base*?b2?=?new?Base;//static_castSon*?s1?=?static_cast<Son*>(b1);?//同類型轉換Son*?s2?=?static_cast<Son*>(b2);?//下行轉換,不安全cout?<<?"static_cast輸出:"<<?endl;cout?<<?s1->d?<<?endl;cout?<<?s2->d?<<?endl;?//下行轉換,原先父對象沒有d成員,輸出垃圾值//dynamic_castSon*?s3?=?dynamic_cast<Son*>(b1);?//同類型轉換Son*?s4?=?dynamic_cast<Son*>(b2);?//下行轉換,安全cout?<<?"dynamic_cast輸出:"?<<?endl;cout?<<?s3->d?<<?endl;if(s4?==?nullptr)cout?<<?"s4指針為nullptr"?<<?endl;elsecout?<<?s4->d?<<?endl;return?0;
}
//輸出結果
//reinterpret_cast輸出:a
//const_cast輸出:100
//static_cast輸出:
//2
//-33686019
//dynamic_cast輸出:
//2
//s4指針為nullptr

從輸出結果可以看出,在進行下行轉換時,dynamic_cast安全的,如果下行轉換不安全的話其會返回空指針,這樣在進行操作的時候可以預先判斷。而使用static_cast下行轉換存在不安全的情況也可以轉換成功,但是直接使用轉換后的對象進行操作容易造成錯誤。

87、C++函數調用的壓棧過程

從代碼入手,解釋這個過程:

#include?<iostream>
using?namespace?std;int?f(int?n)?
{cout?<<?n?<<?endl;return?n;
}void?func(int?param1,?int?param2)
{int?var1?=?param1;int?var2?=?param2;printf("var1=%d,var2=%d",?f(var1),?f(var2));
}int?main(int?argc,?char*?argv[])
{func(1,?2);return?0;
}
//輸出結果
//2
//1
//var1=1,var2=2

當函數從入口函數main函數開始執行時,編譯器會將我們操作系統的運行狀態,main函數的返回地址、main的參數、mian函數中的變量、進行依次壓棧;

當main函數開始調用func()函數時,編譯器此時會將main函數的運行狀態進行壓棧,再將func()函數的返回地址、func()函數的參數從右到左、func()定義變量依次壓棧;

當func()調用f()的時候,編譯器此時會將func()函數的運行狀態進行壓棧,再將的返回地址、f()函數的參數從右到左、f()定義變量依次壓棧

從代碼的輸出結果可以看出,函數f(var1)、f(var2)依次入棧,而后先執行f(var2),再執行f(var1),最后打印整個字符串,將棧中的變量依次彈出,最后主函數返回。

《C/C++函數調用過程分析》:https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601204.html

《C/C++函數調用的壓棧模型》:https://blog.csdn.net/m0_37717595/article/details/80368411

88、說說移動構造函數

1) ?我們用對象a初始化對象b,后對象a我們就不在使用了,但是對象a的空間還在呀(在析構之前),既然拷貝構造函數,實際上就是把a對象的內容復制一份到b中,那么為什么我們不能直接使用a的空間呢?這樣就避免了新的空間的分配,大大降低了構造的成本。這就是移動構造函數設計的初衷;

2) ?拷貝構造函數中,對于指針,我們一定要采用深層復制,而移動構造函數中,對于指針,我們采用淺層復制。淺層復制之所以危險,是因為兩個指針共同指向一片內存空間,若第一個指針將其釋放,另一個指針的指向就不合法了。

所以我們只要避免第一個指針釋放空間就可以了。避免的方法就是將第一個指針(比如a->value)置為NULL,這樣在調用析構函數的時候,由于有判斷是否為NULL的語句,所以析構a的時候并不會回收a->value指向的空間;

3) ?移動構造函數的參數和拷貝構造函數不同,拷貝構造函數的參數是一個左值引用,但是移動構造函數的初值是一個右值引用。意味著,移動構造函數的參數是一個右值或者將亡值的引用。也就是說,只用用一個右值,或者將亡值初始化另一個對象的時候,才會調用移動構造函數。而那個move語句,就是將一個左值變成一個將亡值。

89、C++中將臨時變量作為返回值時的處理過程

首先需要明白一件事情,臨時變量,在函數調用過程中是被壓到程序進程的棧中的,當函數退出時,臨時變量出棧,即臨時變量已經被銷毀,臨時變量占用的內存空間沒有被清空,但是可以被分配給其他變量,所以有可能在函數退出時,該內存已經被修改了,對于臨時變量來說已經是沒有意義的值了

C語言里規定:16bit程序中,返回值保存在ax寄存器中,32bit程序中,返回值保持在eax寄存器中,如果是64bit返回值,edx寄存器保存高32bit,eax寄存器保存低32bit

由此可見,函數調用結束后,返回值被臨時存儲到寄存器中,并沒有放到堆或棧中,也就是說與內存沒有關系了。當退出函數的時候,臨時變量可能被銷毀,但是返回值卻被放到寄存器中與臨時變量的生命周期沒有關系

如果我們需要返回值,一般使用賦值語句就可以了

《【C++】臨時變量不能作為函數的返回值?》:https://www.wandouip.com/t5i204349/

(棧上的內存分配、拷貝過程)

90、關于this指針你知道什么?全說出來

  • this指針是類的指針,指向對象的首地址。

  • this指針只能在成員函數中使用,在全局函數、靜態成員函數中都不能用this。

  • this指針只有在成員函數中才有定義,且存儲位置會因編譯器不同有不同存儲位置。

this指針的用處

一個對象的this指針并不是對象本身的一部分,不會影響sizeof(對象)的結果。this作用域是在類內部,當在類的非靜態成員函數中訪問類的非靜態成員的時候(全局函數,靜態函數中不能使用this指針),編譯器會自動將對象本身的地址作為一個隱含參數傳遞給函數。也就是說,即使你沒有寫上this指針,編譯器在編譯的時候也是加上this的,它作為非靜態成員函數的隱含形參,對各成員的訪問均通過this進行

this指針的使用

一種情況就是,在類的非靜態成員函數中返回類對象本身的時候,直接使用 return *this;

另外一種情況是當形參數與成員變量名相同時用于區分,如this->n = n (不能寫成n = n)

類的this指針有以下特點

(1)this只能在成員函數中使用,全局函數、靜態函數都不能使用this。實際上,成員函數默認第一個參數T * const this

如:

class?A{
public:int?func(int?p){}
};

其中,func的原型在編譯器看來應該是:

int func(A * const this,int p);

(2)由此可見,this在成員函數的開始前構造,在成員函數的結束后清除。這個生命周期同任何一個函數的參數是一樣的,沒有任何區別。當調用一個類的成員函數時,編譯器將類的指針作為函數的this參數傳遞進去。如:

A?a;
a.func(10);
//此處,編譯器將會編譯成:
A::func(&a,10);

看起來和靜態函數沒差別,對嗎?不過,區別還是有的。編譯器通常會對this指針做一些優化,因此,this指針的傳遞效率比較高,例如VC通常是通過ecx(計數寄存器)傳遞this參數的。

91、幾個this指針的易混問題

A. this指針是什么時候創建的?

this在成員函數的開始執行前構造,在成員的執行結束后清除。

但是如果class或者struct里面沒有方法的話,它們是沒有構造函數的,只能當做C的struct使用。采用TYPE xx的方式定義的話,在棧里分配內存,這時候this指針的值就是這塊內存的地址。采用new的方式創建對象的話,在堆里分配內存,new操作符通過eax(累加寄存器)返回分配的地址,然后設置給指針變量。之后去調用構造函數(如果有構造函數的話),這時將這個內存塊的地址傳給ecx,之后構造函數里面怎么處理請看上面的回答

B. this指針存放在何處?堆、棧、全局變量,還是其他?

this指針會因編譯器不同而有不同的放置位置。可能是棧,也可能是寄存器,甚至全局變量。在匯編級別里面,一個值只會以3種形式出現:立即數、寄存器值和內存變量值。不是存放在寄存器就是存放在內存中,它們并不是和高級語言變量對應的。

C. this指針是如何傳遞類中的函數的?綁定?還是在函數參數的首參數就是this指針?那么,this指針又是如何找到“類實例后函數的”?

大多數編譯器通過ecx(寄數寄存器)寄存器傳遞this指針。事實上,這也是一個潛規則。一般來說,不同編譯器都會遵從一致的傳參規則,否則不同編譯器產生的obj就無法匹配了。

在call之前,編譯器會把對應的對象地址放到eax中。this是通過函數參數的首參來傳遞的。this指針在調用之前生成,至于“類實例后函數”,沒有這個說法。類在實例化時,只分配類中的變量空間,并沒有為函數分配空間。自從類的函數定義完成后,它就在那兒,不會跑的

D. this指針是如何訪問類中的變量的?

如果不是類,而是結構體的話,那么,如何通過結構指針來訪問結構中的變量呢?如果你明白這一點的話,就很容易理解這個問題了。

在C++中,類和結構是只有一個區別的:類的成員默認是private,而結構是public。

this是類的指針,如果換成結構體,那this就是結構的指針了。

E.我們只有獲得一個對象后,才能通過對象使用this指針。如果我們知道一個對象this指針的位置,可以直接使用嗎?

this指針只有在成員函數中才有定義。因此,你獲得一個對象后,也不能通過對象使用this指針。所以,我們無法知道一個對象的this指針的位置(只有在成員函數里才有this指針的位置)。當然,在成員函數里,你是可以知道this指針的位置的(可以通過&this獲得),也可以直接使用它。

F.每個類編譯后,是否創建一個類中函數表保存函數指針,以便用來調用函數?

普通的類函數(不論是成員函數,還是靜態函數)都不會創建一個函數表來保存函數指針。只有虛函數才會被放到函數表中。但是,即使是虛函數,如果編譯期就能明確知道調用的是哪個函數,編譯器就不會通過函數表中的指針來間接調用,而是會直接調用該函數。正是由于this指針的存在,用來指向不同的對象,從而確保不同對象之間調用相同的函數可以互不干擾

《C++中this指針的用法詳解》http://blog.chinaunix.net/uid-21411227-id-1826942.html

92、構造函數、拷貝構造函數和賦值操作符的區別

構造函數

對象不存在,沒用別的對象初始化,在創建一個新的對象時調用構造函數

拷貝構造函數

對象不存在,但是使用別的已經存在的對象來進行初始化

賦值運算符

對象存在,用別的對象給它賦值,這屬于重載“=”號運算符的范疇,“=”號兩側的對象都是已存在的

舉個例子:

#include?<iostream>
using?namespace?std;class?A
{
public:A(){cout?<<?"我是構造函數"?<<?endl;}A(const?A&?a){cout?<<?"我是拷貝構造函數"?<<?endl;}A&?operator?=?(A&?a){cout?<<?"我是賦值操作符"?<<?endl;return?*this;}~A()?{};
};int?main()
{A?a1;?//調用構造函數A?a2?=?a1;?//調用拷貝構造函數a2?=?a1;?//調用賦值操作符return?0;
}
//輸出結果
//我是構造函數
//我是拷貝構造函數
//我是賦值操作符

93、拷貝構造函數和賦值運算符重載的區別?

  • 拷貝構造函數是函數,賦值運算符是運算符重載。

  • 拷貝構造函數會生成新的類對象,賦值運算符不能。

  • 拷貝構造函數是直接構造一個新的類對象,所以在初始化對象前不需要檢查源對象和新建對象是否相同;賦值運算符需要上述操作并提供兩套不同的復制策略,另外賦值運算符中如果原來的對象有內存分配則需要先把內存釋放掉。

  • 形參傳遞是調用拷貝構造函數(調用的被賦值對象的拷貝構造函數),但并不是所有出現"="的地方都是使用賦值運算符,如下:

    Student s;
    ?Student s1 = s; ? ?// 調用拷貝構造函數
    ?Student s2;
    ?s2 = s; ? ?// 賦值運算符操作

注:類中有指針變量時要重寫析構函數、拷貝構造函數和賦值運算符

94、智能指針的作用;

1) ?C++11中引入了智能指針的概念,方便管理堆內存。使用普通指針,容易造成堆內存泄露(忘記釋放),二次釋放,程序發生異常時內存泄露等問題等,使用智能指針能更好的管理堆內存。

2) ?智能指針在C++11版本之后提供,包含在頭文件中,shared_ptr、unique_ptr、weak_ptr。shared_ptr多個指針指向相同的對象。shared_ptr使用引用計數,每一個shared_ptr的拷貝都指向相同的內存。每使用他一次,內部的引用計數加1,每析構一次,內部的引用計數減1,減為0時,自動刪除所指向的堆內存。shared_ptr內部的引用計數是線程安全的,但是對象的讀取需要加鎖。

3) ?初始化。智能指針是個模板類,可以指定類型,傳入指針通過構造函數初始化。也可以使用make_shared函數初始化。不能將指針直接賦值給一個智能指針,一個是類,一個是指針。例如std::shared_ptrp4 = new int(1);的寫法是錯誤的

拷貝和賦值。拷貝使得對象的引用計數增加1,賦值使得原對象引用計數減1,當計數為0時,自動釋放內存。后來指向的對象引用計數加1,指向后來的對象

4) ?unique_ptr“唯一”擁有其所指對象,同一時刻只能有一個unique_ptr指向給定對象(通過禁止拷貝語義、只有移動語義來實現)。相比與原始指針unique_ptr用于其RAII的特性,使得在出現異常的情況下,動態資源能得到釋放。unique_ptr指針本身的生命周期:從unique_ptr指針創建時開始,直到離開作用域。離開作用域時,若其指向對象,則將其所指對象銷毀(默認使用delete操作符,用戶可指定其他操作)。unique_ptr指針與其所指對象的關系:在智能指針生命周期內,可以改變智能指針所指對象,如創建智能指針時通過構造函數指定、通過reset方法重新指定、通過release方法釋放所有權、通過移動語義轉移所有權。

5) ?智能指針類將一個計數器與類指向的對象相關聯,引用計數跟蹤該類有多少個對象共享同一指針。每次創建類的新對象時,初始化指針并將引用計數置為1;當對象作為另一對象的副本而創建時,拷貝構造函數拷貝指針并增加與之相應的引用計數;對一個對象進行賦值時,賦值操作符減少左操作數所指對象的引用計數(如果引用計數為減至0,則刪除對象),并增加右操作數所指對象的引用計數;調用析構函數時,構造函數減少引用計數(如果引用計數減至0,則刪除基礎對象)。

6) ?weak_ptr 是一種不控制對象生命周期的智能指針, 它指向一個 shared_ptr 管理的對象. 進行該對象的內存管理的是那個強引用的 shared_ptr. weak_ptr只是提供了對管理對象的一個訪問手段。weak_ptr 設計的目的是為配合 shared_ptr 而引入的一種智能指針來協助 shared_ptr 工作, 它只可以從一個 shared_ptr 或另一個 weak_ptr 對象構造, 它的構造和析構不會引起引用記數的增加或減少.

95、說說你了解的auto_ptr作用

1) ?auto_ptr的出現,主要是為了解決“有異常拋出時發生內存泄漏”的問題;拋出異常,將導致指針p所指向的空間得不到釋放而導致內存泄漏;

2) ?auto_ptr構造時取得某個對象的控制權,在析構時釋放該對象。我們實際上是創建一個auto_ptr類型的局部對象,該局部對象析構時,會將自身所擁有的指針空間釋放,所以不會有內存泄漏;

3) ?auto_ptr的構造函數是explicit,阻止了一般指針隱式轉換為 auto_ptr的構造,所以不能直接將一般類型的指針賦值給auto_ptr類型的對象,必須用auto_ptr的構造函數創建對象;

4) ?由于auto_ptr對象析構時會刪除它所擁有的指針,所以使用時避免多個auto_ptr對象管理同一個指針;

5) ?Auto_ptr內部實現,析構函數中刪除對象用的是delete而不是delete[],所以auto_ptr不能管理數組;

6) ?auto_ptr支持所擁有的指針類型之間的隱式類型轉換。

7) ?可以通過*和->運算符對auto_ptr所有用的指針進行提領操作;

8) ?T* get(),獲得auto_ptr所擁有的指針;T* release(),釋放auto_ptr的所有權,并將所有用的指針返回。

96、智能指針的循環引用

循環引用是指使用多個智能指針share_ptr時,出現了指針之間相互指向,從而形成環的情況,有點類似于死鎖的情況,這種情況下,智能指針往往不能正常調用對象的析構函數,從而造成內存泄漏。舉個例子:

#include?<iostream>
using?namespace?std;template?<typename?T>
class?Node
{
public:Node(const?T&?value):_pPre(NULL),?_pNext(NULL),?_value(value){cout?<<?"Node()"?<<?endl;}~Node(){cout?<<?"~Node()"?<<?endl;cout?<<?"this:"?<<?this?<<?endl;}shared_ptr<Node<T>>?_pPre;shared_ptr<Node<T>>?_pNext;T?_value;
};void?Funtest()
{shared_ptr<Node<int>>?sp1(new?Node<int>(1));shared_ptr<Node<int>>?sp2(new?Node<int>(2));cout?<<?"sp1.use_count:"?<<?sp1.use_count()?<<?endl;cout?<<?"sp2.use_count:"?<<?sp2.use_count()?<<?endl;sp1->_pNext?=?sp2;?//sp1的引用+1sp2->_pPre?=?sp1;?//sp2的引用+1cout?<<?"sp1.use_count:"?<<?sp1.use_count()?<<?endl;cout?<<?"sp2.use_count:"?<<?sp2.use_count()?<<?endl;
}
int?main()
{Funtest();system("pause");return?0;
}
//輸出結果
//Node()
//Node()
//sp1.use_count:1
//sp2.use_count:1
//sp1.use_count:2
//sp2.use_count:2

從上面shared_ptr的實現中我們知道了只有當引用計數減減之后等于0,析構時才會釋放對象,而上述情況造成了一個僵局,那就是析構對象時先析構sp2,可是由于sp2的空間sp1還在使用中,所以sp2.use_count減減之后為1,不釋放,sp1也是相同的道理,由于sp1的空間sp2還在使用中,所以sp1.use_count減減之后為1,也不釋放。sp1等著sp2先釋放,sp2等著sp1先釋放,二者互不相讓,導致最終都沒能釋放,內存泄漏。

在實際編程過程中,應該盡量避免出現智能指針之間相互指向的情況,如果不可避免,可以使用弱指針—weak_ptr,它不增加引用計數,只要出了作用域就會自動析構。

《C++ 智能指針(及循環引用問題)》:https://blog.csdn.net/m0_37968340/article/details/76737395

97、什么是虛擬繼承

由于C++支持多繼承,除了public、protected和private三種繼承方式外,還支持虛擬(virtual)繼承,舉個例子:

#include?<iostream>
using?namespace?std;class?A{}
class?B?:?virtual?public?A{};
class?C?:?virtual?public?A{};
class?D?:?public?B,?public?C{};int?main()
{cout?<<?"sizeof(A):"?<<?sizeof?A?<<endl;?//?1,空對象,只有一個占位cout?<<?"sizeof(B):"?<<?sizeof?B?<<endl;?//?4,一個bptr指針,省去占位,不需要對齊cout?<<?"sizeof(C):"?<<?sizeof?C?<<endl;?//?4,一個bptr指針,省去占位,不需要對齊cout?<<?"sizeof(D):"?<<?sizeof?D?<<endl;?//?8,兩個bptr,省去占位,不需要對齊
}

上述代碼所體現的關系是,B和C虛擬繼承A,D又公有繼承B和C,這種方式是一種菱形繼承或者鉆石繼承,可以用如下圖來表示

虛擬繼承的情況下,無論基類被繼承多少次,只會存在一個實體。虛擬繼承基類的子類中,子類會增加某種形式的指針,或者指向虛基類子對象,或者指向一個相關的表格;表格中存放的不是虛基類子對象的地址,就是其偏移量,此類指針被稱為bptr,如上圖所示。如果既存在vptr又存在bptr,某些編譯器會將其優化,合并為一個指針

98、如何獲得結構成員相對于結構開頭的字節偏移量

使用offsetof()函數

舉個例子:

#include?<iostream>
#include?<stddef.h>
using?namespace?std;struct??S
{int?x;char?y;int?z;double?a;
};
int?main()
{cout?<<?offsetof(S,?x)?<<?endl;?//?0cout?<<?offsetof(S,?y)?<<?endl;?//?4cout?<<?offsetof(S,?z)?<<?endl;?//?8cout?<<?offsetof(S,?a)?<<?endl;?//?12return?0;
}在VS2019?+?win下?并不是這樣的cout?<<?offsetof(S,?x)?<<?endl;?//?0cout?<<?offsetof(S,?y)?<<?endl;?//?4cout?<<?offsetof(S,?z)?<<?endl;?//?8cout?<<?offsetof(S,?a)?<<?endl;?//?16?這里是?16的位置,因為?double是8字節,需要找一個8的倍數對齊,
當然了,如果加上??#pragma?pack(4)指定?4字節對齊就可以了
#pragma?pack(4)
struct??S
{int?x;char?y;int?z;double?a;
};
void?test02()
{cout?<<?offsetof(S,?x)?<<?endl;?//?0cout?<<?offsetof(S,?y)?<<?endl;?//?4cout?<<?offsetof(S,?z)?<<?endl;?//?8cout?<<?offsetof(S,?a)?<<?endl;?//?12}

S結構體中各個數據成員的內存空間劃分如下所示,需要注意內存對齊

99、靜態類型和動態類型以及靜態綁定和動態綁定的總結

  • 靜態類型:對象在聲明時采用的類型,在編譯期既已確定;

  • 動態類型:通常是指一個指針或引用目前所指對象的類型,是在運行期決定的;

  • 靜態綁定:綁定的是靜態類型,所對應的函數或屬性依賴于對象的靜態類型,發生在編譯期;

  • 動態綁定:綁定的是動態類型,所對應的函數或屬性依賴于對象的動態類型,發生在運行期;

從上面的定義也可以看出,非虛函數一般都是靜態綁定,而虛函數都是動態綁定(如此才可實現多態性)。
舉個例子:

#include?<iostream>
using?namespace?std;class?A
{
public:/*virtual*/?void?func()?{?std::cout?<<?"A::func()\n";?}
};
class?B?:?public?A
{
public:void?func()?{?std::cout?<<?"B::func()\n";?}
};
class?C?:?public?A
{
public:void?func()?{?std::cout?<<?"C::func()\n";?}
};
int?main()
{C*?pc?=?new?C();?//pc的靜態類型是它聲明的類型C*,動態類型也是C*;B*?pb?=?new?B();?//pb的靜態類型和動態類型也都是B*;A*?pa?=?pc;??????//pa的靜態類型是它聲明的類型A*,動態類型是pa所指向的對象pc的類型C*;pa?=?pb;?????????//pa的動態類型可以更改,現在它的動態類型是B*,但其靜態類型仍是聲明時候的A*;C?*pnull?=?NULL;?//pnull的靜態類型是它聲明的類型C*,沒有動態類型,因為它指向了NULL;pa->func();??????//A::func() pa的靜態類型永遠都是A*,不管其指向的是哪個子類,都是直接調用A::func();pc->func();??????//C::func() pc的動、靜態類型都是C*,因此調用C::func();pnull->func();???//C::func()?不用奇怪為什么空指針也可以調用函數,因為這在編譯期就確定了,和指針空不空沒關系;return?0;
}

如果將A類中的virtual注釋去掉,則運行結果是:

pa->func();??????//B::func()?因為有了virtual虛函數特性,pa的動態類型指向B*,因此先在B中查找,找到后直接調用;
pc->func();??????//C::func() pc的動、靜態類型都是C*,因此也是先在C中查找;
pnull->func();???//空指針異常,因為是func是virtual函數,因此對func的調用只能等到運行期才能確定,然后才發現pnull是空指針;

在上面的例子中,

  • 如果基類A中的func不是virtual函數,那么不論pa、pb、pc指向哪個子類對象,對func的調用都是在定義pa、pb、pc時的靜態類型決定,早已在編譯期確定了。

  • 同樣的空指針也能夠直接調用no-virtual函數而不報錯(這也說明一定要做空指針檢查啊!),因此靜態綁定不能實現多態;

  • 如果func是虛函數,那所有的調用都要等到運行時根據其指向對象的類型才能確定,比起靜態綁定自然是要有性能損失的,但是卻能實現多態特性;

    本文代碼里都是針對指針的情況來分析的,對于引用的情況也同樣適用。

至此總結一下靜態綁定和動態綁定的區別:

  • 靜態綁定發生在編譯期,動態綁定發生在運行期;

  • 對象的動態類型可以更改,但是靜態類型無法更改;

  • 要想實現動態,必須使用動態綁定;

  • 在繼承體系中只有虛函數使用的是動態綁定,其他的全部是靜態綁定;

    建議:

絕對不要重新定義繼承而來的非虛(non-virtual)函數(《Effective C++ 第三版》條款36),因為這樣導致函數調用由對象聲明時的靜態類型確定了,而和對象本身脫離了關系,沒有多態,也這將給程序留下不可預知的隱患和莫名其妙的BUG;另外,在動態綁定也即在virtual函數中,要注意默認參數的使用。當缺省參數和virtual函數一起使用的時候一定要謹慎,不然出了問題怕是很難排查。
看下面的代碼:

#include?<iostream>
using?namespace?std;class?E
{
public:virtual?void?func(int?i?=?0){std::cout?<<?"E::func()\t"?<<?i?<<?"\n";}
};
class?F?:?public?E
{
public:virtual?void?func(int?i?=?1){std::cout?<<?"F::func()\t"?<<?i?<<?"\n";}
};void?test2()
{F*?pf?=?new?F();E*?pe?=?pf;pf->func();?//F::func() 1 ?正常,就該如此;pe->func();?//F::func()?0??哇哦,這是什么情況,調用了子類的函數,卻使用了基類中參數的默認值!
}
int?main()
{test2();return?0;
}

《C++中的靜態綁定和動態綁定》:https://www.cnblogs.com/lizhenghn/p/3657717.html

100、C++ 11有哪些新特性?

  • nullptr替代 NULL

  • 引入了 auto 和 decltype 這兩個關鍵字實現了類型推導

  • 基于范圍的 for 循環for(auto& i : res){}

  • 類和結構體的中初始化列表

  • Lambda 表達式(匿名函數)

  • std::forward_list(單向鏈表)

  • 右值引用和move語義

101、引用是否能實現動態綁定,為什么可以實現?

可以。

引用在創建的時候必須初始化,在訪問虛函數時,編譯器會根據其所綁定的對象類型決定要調用哪個函數。注意只能調用虛函數。

舉個例子:

#include?<iostream>
using?namespace?std;class?Base?
{
public:virtual?void??fun(){cout?<<?"base?::?fun()"?<<?endl;}
};class?Son?:?public?Base
{
public:virtual?void??fun(){cout?<<?"son?::?fun()"?<<?endl;}void?func(){cout?<<?"son?::?not?virtual?function"?<<endl;}
};int?main()
{Son?s;Base&?b?=?s;?//?基類類型引用綁定已經存在的Son對象,引用必須初始化s.fun();?//son::fun()b.fun();?//son?::?fun()return?0;
}

需要說明的是虛函數才具有動態綁定,上面代碼中,Son類中還有一個非虛函數func(),這在b對象中是無法調用的,如果使用基類指針來指向子類也是一樣的。

102、全局變量和局部變量有什么區別?

生命周期不同:全局變量隨主程序創建和創建,隨主程序銷毀而銷毀;局部變量在局部函數內部,甚至局部循環體等內部存在,退出就不存在;

使用方式不同:通過聲明后全局變量在程序的各個部分都可以用到;局部變量分配在堆棧區,只能在局部使用。

操作系統和編譯器通過內存分配的位置可以區分兩者,全局變量分配在全局數據段并且在程序開始運行的時候被加載,局部變量則分配在堆棧里面 。

《C++經典面試題》:https://www.cnblogs.com/yjd_hycf_space/p/7495640.html

103、指針加減計算要注意什么?

指針加減本質是對其所指地址的移動,移動的步長跟指針的類型是有關系的,因此在涉及到指針加減運算需要十分小心,加多或者減多都會導致指針指向一塊未知的內存地址,如果再進行操作就會很危險。

舉個例子:

#include?<iostream>
using?namespace?std;int?main()
{int?*a,?*b,?c;a?=?(int*)0x500;b?=?(int*)0x520;c?=?b?-?a;printf("%d\n",?c);?//?8a?+=?0x020;c?=?b?-?a;printf("%d\n",?c);?//?-24return?0;
}

首先變量a和b都是以16進制的形式初始化,將它們轉成10進制分別是1280(5*16\^2=1280)和1312(5*16\^2+2*16=1312), 那么它們的差值為32,也就是說a和b所指向的地址之間間隔32個位,但是考慮到是int類型占4位,所以c的值為32/4=8

a自增16進制0x20之后,其實際地址變為1280 + 2*16*4 = 1408,(因為一個int占4位,所以要乘4),這樣它們的差值就變成了1312 - 1280 = -96,所以c的值就變成了-96/4 = -24

遇到指針的計算,需要明確的是指針每移動一位,它實際跨越的內存間隔是指針類型的長度,建議都轉成10進制計算,計算結果除以類型長度取得結果

104、 怎樣判斷兩個浮點數是否相等?

對兩個浮點數判斷大小和是否相等不能直接用==來判斷,會出錯!明明相等的兩個數比較反而是不相等!對于兩個浮點數比較只能通過相減并與預先設定的精度比較,記得要取絕對值!浮點數與0的比較也應該注意。與浮點數的表示方式有關。

105、方法調用的原理(棧、匯編)

1) ?機器用棧來傳遞過程參數、存儲返回信息、保存寄存器用于以后恢復,以及本地存儲。而為單個過程分配的那部分棧稱為幀棧;幀棧可以認為是程序棧的一段,它有兩個端點,一個標識起始地址,一個標識著結束地址,兩個指針結束地址指針esp,開始地址指針ebp;

2) ?由一系列棧幀構成,這些棧幀對應一個過程,而且每一個棧指針+4的位置存儲函數返回地址;每一個棧幀都建立在調用者的下方,當被調用者執行完畢時,這一段棧幀會被釋放。由于棧幀是向地址遞減的方向延伸,因此如果我們將棧指針減去一定的值,就相當于給棧幀分配了一定空間的內存。如果將棧指針加上一定的值,也就是向上移動,那么就相當于壓縮了棧幀的長度,也就是說內存被釋放了。

3) ?過程實現

① ? 備份原來的幀指針,調整當前的棧幀指針到棧指針位置;

② ? 建立起來的棧幀就是為被調用者準備的,當被調用者使用棧幀時,需要給臨時變量分配預留內存;

③ ? 使用建立好的棧幀,比如讀取和寫入,一般使用mov,push以及pop指令等等。

④ ? 恢復被調用者寄存器當中的值,這一過程其實是從棧幀中將備份的值再恢復到寄存器,不過此時這些值可能已經不在棧頂了

⑤ ? 恢復被調用者寄存器當中的值,這一過程其實是從棧幀中將備份的值再恢復到寄存器,不過此時這些值可能已經不在棧頂了。

⑥ ? 釋放被調用者的棧幀,釋放就意味著將棧指針加大,而具體的做法一般是直接將棧指針指向幀指針,因此會采用類似下面的匯編代碼處理。

⑦ ? 恢復調用者的棧幀,恢復其實就是調整棧幀兩端,使得當前棧幀的區域又回到了原始的位置。

⑧ ? 彈出返回地址,跳出當前過程,繼續執行調用者的代碼。

4) ?過程調用和返回指令

① ? call指令

② ? leave指令

③ ? ret指令

106、C++中的指針參數傳遞和引用參數傳遞有什么區別?底層原理你知道嗎?

1) 指針參數傳遞本質上是值傳遞,它所傳遞的是一個地址值。

值傳遞過程中,被調函數的形式參數作為被調函數的局部變量處理,會在棧中開辟內存空間以存放由主調函數傳遞進來的實參值,從而形成了實參的一個副本(替身)。

值傳遞的特點是,被調函數對形式參數的任何操作都是作為局部變量進行的,不會影響主調函數的實參變量的值(形參指針變了,實參指針不會變)。

2) 引用參數傳遞過程中,被調函數的形式參數也作為局部變量在棧中開辟了內存空間,但是這時存放的是由主調函數放進來的實參變量的地址。

被調函數對形參(本體)的任何操作都被處理成間接尋址,即通過棧中存放的地址訪問主調函數中的實參變量(根據別名找到主調函數中的本體)。

因此,被調函數對形參的任何操作都會影響主調函數中的實參變量。

3) 引用傳遞和指針傳遞是不同的,雖然他們都是在被調函數棧空間上的一個局部變量,但是任何對于引用參數的處理都會通過一個間接尋址的方式操作到主調函數中的相關變量。

而對于指針傳遞的參數,如果改變被調函數中的指針地址,它將應用不到主調函數的相關變量。如果想通過指針參數傳遞來改變主調函數中的相關變量(地址),那就得使用指向指針的指針或者指針引用。

4) 從編譯的角度來講,程序在編譯時分別將指針和引用添加到符號表上,符號表中記錄的是變量名及變量所對應地址。

指針變量在符號表上對應的地址值為指針變量的地址值,而引用在符號表上對應的地址值為引用對象的地址值(與實參名字不同,地址相同)。

符號表生成之后就不會再改,因此指針可以改變其指向的對象(指針變量中的值可以改),而引用對象則不能修改。

107、類如何實現只能靜態分配和只能動態分配

1) ?前者是把new、delete運算符重載為private屬性。后者是把構造、析構函數設為protected屬性,再用子類來動態創建

2) ?建立類的對象有兩種方式:

① ? 靜態建立,靜態建立一個類對象,就是由編譯器為對象在棧空間中分配內存;

② ? 動態建立,A *p = new A();動態建立一個類對象,就是使用new運算符為對象在堆空間中分配內存。這個過程分為兩步,第一步執行operator new()函數,在堆中搜索一塊內存并進行分配;第二步調用類構造函數構造對象;

3) ?只有使用new運算符,對象才會被建立在堆上,因此只要限制new運算符就可以實現類對象只能建立在棧上,可以將new運算符設為私有。

108、如果想將某個類用作基類,為什么該類必須定義而非聲明?

派生類中包含并且可以使用它從基類繼承而來的成員,為了使用這些成員,派生類必須知道他們是什么。

結語

好家伙,終于完了。你要是能看到這里,那真是個狠人。

整理這些東西真心不容易!眼睛快看瞎了,求個三連吧!感謝大家!

就醬,拜了個拜!下期再見!

還有春節不打烊,持續更文

— END —

Hi,小伙伴你好,我是阿秀,一枚從底層慢慢爬到互聯網大廠的程序員。

歡迎「關注」,阿秀親手帶你構建計算機體系結構,「領略」不一樣的程序世界,也可以「點贊、收藏、分享」,十分感謝!

版权声明:本站所有资料均为网友推荐收集整理而来,仅供学习和研究交流使用。

原文链接:https://hbdhgg.com/4/193524.html

发表评论:

本站为非赢利网站,部分文章来源或改编自互联网及其他公众平台,主要目的在于分享信息,版权归原作者所有,内容仅供读者参考,如有侵权请联系我们删除!

Copyright © 2022 匯編語言學習筆記 Inc. 保留所有权利。

底部版权信息