www.久久久久|狼友网站av天堂|精品国产无码a片|一级av色欲av|91在线播放视频|亚洲无码主播在线|国产精品草久在线|明星AV网站在线|污污内射久久一区|婷婷综合视频网站

當(dāng)前位置:首頁 > > C語言與CPP編程
[導(dǎo)讀]《逆襲進(jìn)大廠》系列第二彈 C++ 進(jìn)階篇直接發(fā)車了。


是本期 C++ 八股文問題目錄。

不逼逼了,《逆襲進(jìn)大廠系列第二彈 C++ 進(jìn)階篇直接發(fā)車了。

這篇總字?jǐn)?shù)是 37814 個(gè)字,嗯,將近 4W 字。

50、static的用法和作用?

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

當(dāng)同時(shí)編譯多個(gè)文件時(shí),所有未加static前綴的全局變量和函數(shù)都具有全局可見性。

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

3.static的第三個(gè)作用是默認(rèn)初始化為0(static變量)

其實(shí)全局變量也具備這一屬性,因?yàn)槿肿兞恳泊鎯υ陟o態(tài)數(shù)據(jù)區(qū)。在靜態(tài)數(shù)據(jù)區(qū),內(nèi)存中所有的字節(jié)默認(rèn)值都是0x00,某些時(shí)候這一特點(diǎn)可以減少程序員的工作量。

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

1)  函數(shù)體內(nèi)static變量的作用范圍為該函數(shù)體,不同于auto變量,該變量的內(nèi)存只被分配一次,因此其值在下次調(diào)用時(shí)仍維持上次的值;

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

3)  在模塊內(nèi)的static函數(shù)只可被這一模塊內(nèi)的其它函數(shù)調(diào)用,這個(gè)函數(shù)的使用范圍被限制在聲明它的模塊內(nèi);

4)  在類中的static成員變量屬于整個(gè)類所擁有,對類的所有對象只有一份拷貝;

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

類內(nèi):

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

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

8)  static成員函數(shù)不能被virtual修飾,static成員不屬于任何對象或?qū)嵗?,所以加上virtual沒有任何實(shí)際意義;靜態(tài)成員函數(shù)沒有this指針,虛函數(shù)的實(shí)現(xiàn)是為每一個(gè)對象分配一個(gè)vptr指針,而vptr是通過this指針調(diào)用的,所以不能為virtual;虛函數(shù)的調(diào)用關(guān)系,this->vptr->ctable->virtual function

51、靜態(tài)變量什么時(shí)候初始化

1)  初始化只有一次,但是可以多次賦值,在主程序之前,編譯器已經(jīng)為其分配好了內(nèi)存。

2)  靜態(tài)局部變量和全局變量一樣,數(shù)據(jù)都存放在全局區(qū)域,所以在主程序之前,編譯器已經(jīng)為其分配好了內(nèi)存,但在C和C++中靜態(tài)局部變量的初始化節(jié)點(diǎn)又有點(diǎn)不太一樣。在C中,初始化發(fā)生在代碼執(zhí)行之前,編譯階段分配好內(nèi)存之后,就會(huì)進(jìn)行初始化,所以我們看到在C語言中無法使用變量對靜態(tài)局部變量進(jìn)行初始化,在程序運(yùn)行結(jié)束,變量所處的全局內(nèi)存會(huì)被全部回收。

3)  而在C++中,初始化時(shí)在執(zhí)行相關(guān)代碼時(shí)才會(huì)進(jìn)行初始化,主要是由于C++引入對象后,要進(jìn)行初始化必須執(zhí)行相應(yīng)構(gòu)造函數(shù)和析構(gòu)函數(shù),在構(gòu)造函數(shù)或析構(gòu)函數(shù)中經(jīng)常會(huì)需要進(jìn)行某些程序中需要進(jìn)行的特定操作,并非簡單地分配內(nèi)存。所以C++標(biāo)準(zhǔn)定為全局或靜態(tài)對象是有首次用到時(shí)才會(huì)進(jìn)行構(gòu)造,并通過atexit()來管理。在程序結(jié)束,按照構(gòu)造順序反方向進(jìn)行逐個(gè)析構(gòu)。所以在C++中是可以使用變量對靜態(tài)局部變量進(jìn)行初始化的。

52、const關(guān)鍵字?

1)  阻止一個(gè)變量被改變,可以使用const關(guān)鍵字。在定義該const變量時(shí),通常需要對它進(jìn)行初始化,因?yàn)橐院缶蜎]有機(jī)會(huì)再去改變它了;

2)  對指針來說,可以指定指針本身為const,也可以指定指針?biāo)傅臄?shù)據(jù)為const,或二者同時(shí)指定為const;

3)  在一個(gè)函數(shù)聲明中,const可以修飾形參,表明它是一個(gè)輸入?yún)?shù),在函數(shù)內(nèi)部不能改變其值;

4)  對于類的成員函數(shù),若指定其為const類型,則表明其是一個(gè)常函數(shù),不能修改類的成員變量,類的常對象只能訪問類的常成員函數(shù);

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

6)  const成員函數(shù)可以訪問非const對象的非const數(shù)據(jù)成員、const數(shù)據(jù)成員,也可以訪問const對象內(nèi)的所有數(shù)據(jù)成員;

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

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

9)  const類型變量可以通過類型轉(zhuǎn)換符const_cast將const類型轉(zhuǎn)換為非const類型;

10) const類型變量必須定義的時(shí)候進(jìn)行初始化,因此也導(dǎo)致如果類的成員變量有const類型的變量,那么該變量必須在類的初始化列表中進(jìn)行初始化;

11) 對于函數(shù)值傳遞的情況,因?yàn)閰?shù)傳遞是通過復(fù)制實(shí)參創(chuàng)建一個(gè)臨時(shí)變量傳遞進(jìn)函數(shù)的,函數(shù)內(nèi)只能改變臨時(shí)變量,但無法改變實(shí)參。則這個(gè)時(shí)候無論加不加const對實(shí)參不會(huì)產(chǎn)生任何影響。但是在引用或指針傳遞函數(shù)調(diào)用中,因?yàn)閭鬟M(jìn)去的是一個(gè)引用或指針,這樣函數(shù)內(nèi)部可以改變引用或指針?biāo)赶虻淖兞浚@時(shí)const 才是實(shí)實(shí)在在地保護(hù)了實(shí)參所指向的變量。因?yàn)樵诰幾g階段編譯器對調(diào)用函數(shù)的選擇是根據(jù)實(shí)參進(jìn)行的,所以,只有引用傳遞和指針傳遞可以用是否加const來重載。一個(gè)擁有頂層const的形參無法和另一個(gè)沒有頂層const的形參區(qū)分開來。

53、指針和const的用法

1)  當(dāng)const修飾指針時(shí),由于const的位置不同,它的修飾對象會(huì)有所不同。

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

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

底層指針表示指針?biāo)赶虻淖兞渴且粋€(gè)常量。

54、形參與實(shí)參的區(qū)別?

1)  形參變量只有在被調(diào)用時(shí)才分配內(nèi)存單元,在調(diào)用結(jié)束時(shí), 即刻釋放所分配的內(nèi)存單元。因此,形參只有在函數(shù)內(nèi)部有效。函數(shù)調(diào)用結(jié)束返回主調(diào)函數(shù)后則不能再使用該形參變量。

2)  實(shí)參可以是常量、變量、表達(dá)式、函數(shù)等, 無論實(shí)參是何種類型的量,在進(jìn)行函數(shù)調(diào)用時(shí),它們都必須具有確定的值, 以便把這些值傳送給形參。因此應(yīng)預(yù)先用賦值,輸入等辦法使實(shí)參獲得確定值,會(huì)產(chǎn)生一個(gè)臨時(shí)變量。

3)  實(shí)參和形參在數(shù)量上,類型上,順序上應(yīng)嚴(yán)格一致, 否則會(huì)發(fā)生“類型不匹配”的錯(cuò)誤。

4)  函數(shù)調(diào)用中發(fā)生的數(shù)據(jù)傳送是單向的。即只能把實(shí)參的值傳送給形參,而不能把形參的值反向地傳送給實(shí)參。因此在函數(shù)調(diào)用過程中,形參的值發(fā)生改變,而實(shí)參中的值不會(huì)變化。

5)  當(dāng)形參和實(shí)參不是指針類型時(shí),在該函數(shù)運(yùn)行時(shí),形參和實(shí)參是不同的變量,他們在內(nèi)存中位于不同的位置,形參將實(shí)參的內(nèi)容復(fù)制一份,在該函數(shù)運(yùn)行結(jié)束的時(shí)候形參被釋放,而實(shí)參內(nèi)容不會(huì)改變。

55、值傳遞、指針傳遞、引用傳遞的區(qū)別和效率

1)   值傳遞:有一個(gè)形參向函數(shù)所屬的??截悢?shù)據(jù)的過程,如果值傳遞的對象是類對象   或是大的結(jié)構(gòu)體對象,將耗費(fèi)一定的時(shí)間和空間。(傳值)

2)  指針傳遞:同樣有一個(gè)形參向函數(shù)所屬的??截悢?shù)據(jù)的過程,但拷貝的數(shù)據(jù)是一個(gè)固定為4字節(jié)的地址。(傳值,傳遞的是地址值)

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

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

56、什么是類的繼承?

1) 類與類之間的關(guān)系

has-A包含關(guān)系,用以描述一個(gè)類由多個(gè)部件類構(gòu)成,實(shí)現(xiàn)has-A關(guān)系用類的成員屬性表示,即一個(gè)類的成員屬性是另一個(gè)已經(jīng)定義好的類;

use-A,一個(gè)類使用另一個(gè)類,通過類之間的成員函數(shù)相互聯(lián)系,定義友元或者通過傳遞參數(shù)的方式來實(shí)現(xiàn);

is-A,繼承關(guān)系,關(guān)系具有傳遞性;

2) 繼承的相關(guān)概念

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

3) 繼承的特點(diǎn)

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

4) 繼承中的訪問控制

public、protected、private

5) 繼承中的構(gòu)造和析構(gòu)函數(shù)

6) 繼承中的兼容性原則

57、什么是內(nèi)存池,如何實(shí)現(xiàn)

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

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

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

這里簡單描述一下《STL源碼剖析》中的內(nèi)存池實(shí)現(xiàn)機(jī)制:

allocate包裝malloc,deallocate包裝free

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

  1. 首先客戶端會(huì)調(diào)用malloc()配置一定數(shù)量的區(qū)塊(固定大小的內(nèi)存塊,通常為8的倍數(shù)),假設(shè)40個(gè)32bytes的區(qū)塊,其中20個(gè)區(qū)塊(一半)給程序?qū)嶋H使用,1個(gè)區(qū)塊交出,另外19個(gè)處于維護(hù)狀態(tài)。剩余20個(gè)(一半)留給內(nèi)存池,此時(shí)一共有(20*32byte)

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

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

  4. 如果整個(gè)堆的空間都不夠了,就會(huì)在原先已經(jīng)分配區(qū)塊中尋找能滿足當(dāng)前需求的區(qū)塊數(shù)量,能滿足就返回,不能滿足就向客戶端報(bào)bad_alloc異常

《STL源碼解析》侯捷 P68

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

最開始GC2.9之前:

new和 operator new 的區(qū)別:new 是個(gè)運(yùn)算符,編輯器會(huì)調(diào)用 operator new(0)

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

GC2.9的alloc的一個(gè)比較好的分配器的實(shí)現(xiàn)規(guī)則

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

但是現(xiàn)在GC4.9及其之后 也還有,變成_pool_alloc這個(gè)名字了,不再是默認(rèn)的了,你需要自己去指定它可以自己指定,比如說vector

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

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

x的地址為ebp-4,b的地址為ebp-8,因?yàn)闂?nèi)的變量內(nèi)存是從高往低進(jìn)行分配的,所以b的地址比x的低。

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

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

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

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

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

深復(fù)制 :在計(jì)算機(jī)中開辟了一塊新的內(nèi)存地址用于存放復(fù)制的對象。

2)  在某些狀況下,類內(nèi)成員變量需要?jiǎng)討B(tài)開辟堆內(nèi)存,如果實(shí)行位拷貝,也就是把對象里的值完全復(fù)制給另一個(gè)對象,如A=B。這時(shí),如果B中有一個(gè)成員變量指針已經(jīng)申請了內(nèi)存,那A中的那個(gè)成員變量也指向同一塊內(nèi)存。這就出現(xiàn)了問題:當(dāng)B把內(nèi)存釋放了(如:析構(gòu)),這時(shí)A內(nèi)的指針就是野指針了,出現(xiàn)運(yùn)行錯(cuò)誤。

60、C++模板是什么,你知道底層怎么實(shí)現(xiàn)的?

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

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

61、new和malloc的區(qū)別?

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

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

3、 new操作符內(nèi)存分配成功時(shí),返回的是對象類型的指針,類型嚴(yán)格與對象匹配,無須進(jìn)行類型轉(zhuǎn)換,故new是符合類型安全性的操作符。而malloc內(nèi)存分配成功則是返回void * ,需要通過強(qiáng)制類型轉(zhuǎn)換將void*指針轉(zhuǎn)換成我們需要的類型。

4、 new內(nèi)存分配失敗時(shí),會(huì)拋出bac_alloc異常。malloc分配內(nèi)存失敗時(shí)返回NULL。

5、 new會(huì)先調(diào)用operator new函數(shù),申請足夠的內(nèi)存(通常底層使用malloc實(shí)現(xiàn))。然后調(diào)用類型的構(gòu)造函數(shù),初始化成員變量,最后返回自定義類型指針。delete先調(diào)用析構(gòu)函數(shù),然后調(diào)用operator delete函數(shù)釋放內(nèi)存(通常底層使用free實(shí)現(xiàn))。malloc/free是庫函數(shù),只能動(dòng)態(tài)的申請和釋放內(nèi)存,無法強(qiáng)制要求其做自定義類型對象構(gòu)造和析構(gòu)工作。

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

1、 動(dòng)態(tài)數(shù)組管理new一個(gè)數(shù)組時(shí),[]中必須是一個(gè)整數(shù),但是不一定是常量整數(shù),普通數(shù)組必須是一個(gè)常量整數(shù);

2、 new動(dòng)態(tài)數(shù)組返回的并不是數(shù)組類型,而是一個(gè)元素類型的指針;

3、 delete[]時(shí),數(shù)組中的元素按逆序的順序進(jìn)行銷毀;

4、 new在內(nèi)存分配上面有一些局限性,new的機(jī)制是將內(nèi)存分配和對象構(gòu)造組合在一起,同樣的,delete也是將對象析構(gòu)和內(nèi)存釋放組合在一起的。allocator將這兩部分分開進(jìn)行,allocator申請一部分內(nèi)存,不進(jìn)行初始化對象,只有當(dāng)需要的時(shí)候才進(jìn)行初始化操作。

63、new和delete的實(shí)現(xiàn)原理, delete是如何知道釋放內(nèi)存的大小的額?

1、 new簡單類型直接調(diào)用operator new分配內(nèi)存;

而對于復(fù)雜結(jié)構(gòu),先調(diào)用operator new分配內(nèi)存,然后在分配的內(nèi)存上調(diào)用構(gòu)造函數(shù);

對于簡單類型,new[]計(jì)算好大小后調(diào)用operator new;

對于復(fù)雜數(shù)據(jù)結(jié)構(gòu),new[]先調(diào)用operator new[]分配內(nèi)存,然后在p的前四個(gè)字節(jié)寫入數(shù)組大小n,然后調(diào)用n次構(gòu)造函數(shù),針對復(fù)雜類型,new[]會(huì)額外存儲數(shù)組大?。?/span>

①   new表達(dá)式調(diào)用一個(gè)名為operator new(operator new[])函數(shù),分配一塊足夠大的、原始的、未命名的內(nèi)存空間;

②   編譯器運(yùn)行相應(yīng)的構(gòu)造函數(shù)以構(gòu)造這些對象,并為其傳入初始值;

③   對象被分配了空間并構(gòu)造完成,返回一個(gè)指向該對象的指針。

2、 delete簡單數(shù)據(jù)類型默認(rèn)只是調(diào)用free函數(shù);復(fù)雜數(shù)據(jù)類型先調(diào)用析構(gòu)函數(shù)再調(diào)用operator delete;針對簡單類型,delete和delete[]等同。假設(shè)指針p指向new[]分配的內(nèi)存。因?yàn)橐?字節(jié)存儲數(shù)組大小,實(shí)際分配的內(nèi)存地址為[p-4],系統(tǒng)記錄的也是這個(gè)地址。delete[]實(shí)際釋放的就是p-4指向的內(nèi)存。而delete會(huì)直接釋放p指向的內(nèi)存,這個(gè)內(nèi)存根本沒有被系統(tǒng)記錄,所以會(huì)崩潰。

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

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

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

malloc /free的操作對象都是必須明確大小的,而且不能用在動(dòng)態(tài)類上。

new 和delete會(huì)自動(dòng)進(jìn)行類型檢查和大小,malloc/free不能執(zhí)行構(gòu)造函數(shù)與析構(gòu)函數(shù),所以動(dòng)態(tài)對象它是不行的。

當(dāng)然從理論上說使用malloc申請的內(nèi)存是可以通過delete釋放的。不過一般不這樣寫的。而且也不能保證每個(gè)C++的運(yùn)行時(shí)都能正常。

65、malloc與free的實(shí)現(xiàn)原理?

1、 在標(biāo)準(zhǔn)C庫中,提供了malloc/free函數(shù)分配釋放內(nèi)存,這兩個(gè)函數(shù)底層是由brk、mmap、,munmap這些系統(tǒng)調(diào)用實(shí)現(xiàn)的;

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

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

4、 malloc是從堆里面申請內(nèi)存,也就是說函數(shù)返回的指針是指向堆里面的一塊內(nèi)存。操作系統(tǒng)中有一個(gè)記錄空閑內(nèi)存地址的鏈表。當(dāng)操作系統(tǒng)收到程序的申請時(shí),就會(huì)遍歷該鏈表,然后就尋找第一個(gè)空間大于所申請空間的堆結(jié)點(diǎn),然后就將該結(jié)點(diǎn)從空閑結(jié)點(diǎn)鏈表中刪除,并將該結(jié)點(diǎn)的空間分配給程序。

66、malloc、realloc、calloc的區(qū)別

1)   malloc函數(shù)

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

2)   calloc函數(shù)

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

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

3)   realloc函數(shù)

void realloc(void *p, size_t new_size);

給動(dòng)態(tài)分配的空間分配額外的空間,用于擴(kuò)充容量。

67、類成員初始化方式?構(gòu)造函數(shù)的執(zhí)行順序 ?為什么用成員初始化列表會(huì)快一些?

1)  賦值初始化,通過在函數(shù)體內(nèi)進(jìn)行賦值初始化;列表初始化,在冒號后使用初始化列表進(jìn)行初始化。

這兩種方式的主要區(qū)別在于:

對于在函數(shù)體中初始化,是在所有的數(shù)據(jù)成員被分配內(nèi)存空間后才進(jìn)行的。

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

2)  一個(gè)派生類構(gòu)造函數(shù)的執(zhí)行順序如下:

①   虛擬基類的構(gòu)造函數(shù)(多個(gè)虛擬基類則按照繼承的順序執(zhí)行構(gòu)造函數(shù))。

②   基類的構(gòu)造函數(shù)(多個(gè)普通基類也按照繼承的順序執(zhí)行構(gòu)造函數(shù))。

③   類類型的成員對象的構(gòu)造函數(shù)(按照初始化順序)

④   派生類自己的構(gòu)造函數(shù)。

3)  方法一是在構(gòu)造函數(shù)當(dāng)中做賦值的操作,而方法二是做純粹的初始化操作。我們都知道,C++的賦值操作是會(huì)產(chǎn)生臨時(shí)對象的。臨時(shí)對象的出現(xiàn)會(huì)降低程序的效率。

68、成員列表初始化?

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

①    當(dāng)初始化一個(gè)引用成員時(shí);

②    當(dāng)初始化一個(gè)常量成員時(shí);

③    當(dāng)調(diào)用一個(gè)基類的構(gòu)造函數(shù),而它擁有一組參數(shù)時(shí);

④    當(dāng)調(diào)用一個(gè)成員類的構(gòu)造函數(shù),而它擁有一組參數(shù)時(shí);

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

①    編譯器會(huì)一一操作初始化列表,以適當(dāng)?shù)捻樞蛟跇?gòu)造函數(shù)之內(nèi)安插初始化操作,并且在任何顯示用戶代碼之前;

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

69、什么是內(nèi)存泄露,如何檢測與避免

內(nèi)存泄露

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

避免內(nèi)存泄露的幾種方式

  • 計(jì)數(shù)法:使用new或者malloc時(shí),讓該數(shù)+1,delete或free時(shí),該數(shù)-1,程序執(zhí)行完打印這個(gè)計(jì)數(shù),如果不為0則表示存在內(nèi)存泄露

  • 一定要將基類的析構(gòu)函數(shù)聲明為虛函數(shù)

  • 對象數(shù)組的釋放一定要用delete []

  • 有new就有delete,有malloc就有free,保證它們一定成對出現(xiàn)

檢測工具

  • Linux下可以使用Valgrind工具

  • Windows下可以使用CRT庫

70、對象復(fù)用的了解,零拷貝的了解

對象復(fù)用

對象復(fù)用其本質(zhì)是一種設(shè)計(jì)模式:Flyweight享元模式。

通過將對象存儲到“對象池”中實(shí)現(xiàn)對象的重復(fù)利用,這樣可以避免多次創(chuàng)建重復(fù)對象的開銷,節(jié)約系統(tǒng)資源。

零拷貝

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

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

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

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

71、解釋一下什么是trivial destructor

trivial destructor”一般是指用戶沒有自定義析構(gòu)函數(shù),而由系統(tǒng)生成的,這種析構(gòu)函數(shù)在《STL源碼解析》中成為“無關(guān)痛癢”的析構(gòu)函數(shù)。

反之,用戶自定義了析構(gòu)函數(shù),則稱之為“non-trivial destructor”,這種析構(gòu)函數(shù)如果申請了新的空間一定要顯式的釋放,否則會(huì)造成內(nèi)存泄露

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

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

也就是說,在實(shí)際的應(yīng)用當(dāng)中,STL庫提供了相關(guān)的判斷方法__type_traits,感興趣的讀者可以自行查閱使用方式。除了trivial destructor,還有trivial construct、trivial copy construct等,如果能夠?qū)κ欠駎rivial進(jìn)行區(qū)分,可以采用內(nèi)存處理函數(shù)memcpy()、malloc()等更加高效的完成相關(guān)操作,提升效率。

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

72、介紹面向?qū)ο蟮娜筇匦裕⑶遗e例說明

三大特性:繼承、封裝和多態(tài)

(1)繼承

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

它可以使用現(xiàn)有類的所有功能,并在無需重新編寫原來的類的情況下對這些功能進(jìn)行擴(kuò)展

常見的繼承有三種方式:

  1. 實(shí)現(xiàn)繼承:指使用基類的屬性和方法而無需額外編碼的能力

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

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

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

(2)封裝

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

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

(3)多態(tài)

同一事物表現(xiàn)出不同事物的能力,即向不同對象發(fā)送同一消息,不同的對象在接收時(shí)會(huì)產(chǎn)生不同的行為(重載實(shí)現(xiàn)編譯時(shí)多態(tài),虛函數(shù)實(shí)現(xiàn)運(yùn)行時(shí)多態(tài))。

多態(tài)性是允許你將父對象設(shè)置成為和一個(gè)或更多的他的子對象相等的技術(shù),賦值之后,父對象就可以根據(jù)當(dāng)前賦值給它的子對象的特性以不同的方式運(yùn)作。簡單一句話:允許將子類類型的指針賦值給父類類型的指針

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

《C++封裝繼承多態(tài)總結(jié)》:https://blog.csdn.net/IOT_SHUN/article/details/79674293

73、C++中類的數(shù)據(jù)成員和成員函數(shù)內(nèi)存分布情況

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

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

#include  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;
} //輸出結(jié)果 //對象地址:0x7fffec0f15a8 //age地址:0x7fffec0f15a8 //對象大?。? //age大?。? 

從代碼運(yùn)行結(jié)果來看,對象的大小和對象中數(shù)據(jù)成員的大小是一致的,也就是說,成員函數(shù)不占用對象的內(nèi)存。這是因?yàn)樗械暮瘮?shù)都是存放在代碼區(qū)的,不管是全局函數(shù),還是成員函數(shù)。要是成員函數(shù)占用類的對象空間,那么將是多么可怕的事情:定義一次類對象就有成員函數(shù)占用一段空間。我們再來補(bǔ)充一下靜態(tài)成員函數(shù)的存放問題:靜態(tài)成員函數(shù)與一般成員函數(shù)的唯一區(qū)別就是沒有this指針,因此不能訪問非靜態(tài)數(shù)據(jù)成員,就像我前面提到的,所有函數(shù)都存放在代碼區(qū),靜態(tài)函數(shù)也不例外。所有有人一看到 static 這個(gè)單詞就主觀的認(rèn)為是存放在全局?jǐn)?shù)據(jù)區(qū),那是不對的。

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

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

成員初始化列表的概念

在類的構(gòu)造函數(shù)中,不在函數(shù)體內(nèi)對成員變量賦值,而是在構(gòu)造函數(shù)的花括號前面使用冒號和初始化列表賦值

效率

用初始化列表會(huì)快一些的原因是,對于類型,它少了一次調(diào)用構(gòu)造函數(shù)的過程,而在函數(shù)體中賦值則會(huì)多一次調(diào)用。而對于內(nèi)置數(shù)據(jù)類型則沒有差別。舉個(gè)例子:

#include  using namespace std; class A { public:
    A()
    { cout << "默認(rèn)構(gòu)造函數(shù)A()" << endl;
    }
    A(int a)
    {
        value = a; cout << "A(int "<")" << endl;
    }
    A(const A& a)
    {
        value = a.value; cout << "拷貝構(gòu)造函數(shù)A(A& a):  "<endl;
    } int value;
}; class B { public:
    B() : a(1)
    {
        b = A(2);
    }
    A a;
    A b;
}; int main() {
    B b;
} //輸出結(jié)果: //A(int 1) //默認(rèn)構(gòu)造函數(shù)A() //A(int 2) 

從代碼運(yùn)行結(jié)果可以看出,在構(gòu)造函數(shù)體內(nèi)部初始化的對象b多了一次構(gòu)造函數(shù)的調(diào)用過程,而對象a則沒有。由于對象成員變量的初始化動(dòng)作發(fā)生在進(jìn)入構(gòu)造函數(shù)之前,對于內(nèi)置類型沒什么影響,但如果有些成員是類,那么在進(jìn)入構(gòu)造函數(shù)之前,會(huì)先調(diào)用一次默認(rèn)構(gòu)造函數(shù),進(jìn)入構(gòu)造函數(shù)后所做的事其實(shí)是一次賦值操作(對象已存在),所以如果是在構(gòu)造函數(shù)體內(nèi)進(jìn)行賦值的話,等于是一次默認(rèn)構(gòu)造加一次賦值,而初始化列表只做一次賦值操作。

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

75、(超重要)構(gòu)造函數(shù)為什么不能為虛函數(shù)?析構(gòu)函數(shù)為什么要虛函數(shù)?

1、 從存儲空間角度,虛函數(shù)相應(yīng)一個(gè)指向vtable虛函數(shù)表的指針,這大家都知道,但是這個(gè)指向vtable的指針事實(shí)上是存儲在對象的內(nèi)存空間的。

問題出來了,假設(shè)構(gòu)造函數(shù)是虛的,就須要通過 vtable來調(diào)用,但是對象還沒有實(shí)例化,也就是內(nèi)存空間還沒有,怎么找vtable呢?所以構(gòu)造函數(shù)不能是虛函數(shù)。

2、 從使用角度,虛函數(shù)主要用于在信息不全的情況下,能使重載的函數(shù)得到相應(yīng)的調(diào)用。

構(gòu)造函數(shù)本身就是要初始化實(shí)例,那使用虛函數(shù)也沒有實(shí)際意義呀。

所以構(gòu)造函數(shù)沒有必要是虛函數(shù)。虛函數(shù)的作用在于通過父類的指針或者引用來調(diào)用它的時(shí)候可以變成調(diào)用子類的那個(gè)成員函數(shù)。而構(gòu)造函數(shù)是在創(chuàng)建對象時(shí)自己主動(dòng)調(diào)用的,不可能通過父類的指針或者引用去調(diào)用,因此也就規(guī)定構(gòu)造函數(shù)不能是虛函數(shù)。

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

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

5、當(dāng)一個(gè)構(gòu)造函數(shù)被調(diào)用時(shí),它做的首要的事情之中的一個(gè)是初始化它的VPTR。

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

并且,僅僅要它是最后的構(gòu)造函數(shù)調(diào)用,那么在這個(gè)對象的生命期內(nèi),VPTR將保持被初始化為指向這個(gè)VTABLE, 但假設(shè)接著另一個(gè)更晚派生的構(gòu)造函數(shù)被調(diào)用,這個(gè)構(gòu)造函數(shù)又將設(shè)置VPTR指向它的 VTABLE,等.直到最后的構(gòu)造函數(shù)結(jié)束。

VPTR的狀態(tài)是由被最后調(diào)用的構(gòu)造函數(shù)確定的。這就是為什么構(gòu)造函數(shù)調(diào)用是從基類到更加派生類順序的還有一個(gè)理由。可是,當(dāng)這一系列構(gòu)造函數(shù)調(diào)用正發(fā)生時(shí),每一個(gè)構(gòu)造函數(shù)都已經(jīng)設(shè)置VPTR指向它自己的VTABLE。假設(shè)函數(shù)調(diào)用使用虛機(jī)制,它將僅僅產(chǎn)生通過它自己的VTABLE的調(diào)用,而不是最后的VTABLE(全部構(gòu)造函數(shù)被調(diào)用后才會(huì)有最后的VTABLE)。

因?yàn)闃?gòu)造函數(shù)本來就是為了明確初始化對象成員才產(chǎn)生的,然而virtual function主要是為了再不完全了解細(xì)節(jié)的情況下也能正確處理對象。另外,virtual函數(shù)是在不同類型的對象產(chǎn)生不同的動(dòng)作,現(xiàn)在對象還沒有產(chǎn)生,如何使用virtual函數(shù)來完成你想完成的動(dòng)作。

直接的講,C++中基類采用virtual虛析構(gòu)函數(shù)是為了防止內(nèi)存泄漏。

具體地說,如果派生類中申請了內(nèi)存空間,并在其析構(gòu)函數(shù)中對這些內(nèi)存空間進(jìn)行釋放。假設(shè)基類中采用的是非虛析構(gòu)函數(shù),當(dāng)刪除基類指針指向的派生類對象時(shí)就不會(huì)觸發(fā)動(dòng)態(tài)綁定,因而只會(huì)調(diào)用基類的析構(gòu)函數(shù),而不會(huì)調(diào)用派生類的析構(gòu)函數(shù)。那么在這種情況下,派生類中申請的空間就得不到釋放從而產(chǎn)生內(nèi)存泄漏。

所以,為了防止這種情況的發(fā)生,C++中基類的析構(gòu)函數(shù)應(yīng)采用virtual虛析構(gòu)函數(shù)。

76、析構(gòu)函數(shù)的作用,如何起作用?

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

規(guī)則,只要你一實(shí)例化對象,系統(tǒng)自動(dòng)回調(diào)用一個(gè)構(gòu)造函數(shù)就是你不寫,編譯器也自動(dòng)調(diào)用一次。

2)  析構(gòu)函數(shù)與構(gòu)造函數(shù)的作用相反,用于撤銷對象的一些特殊任務(wù)處理,可以是釋放對象分配的內(nèi)存空間;特點(diǎn):析構(gòu)函數(shù)與構(gòu)造函數(shù)同名,但該函數(shù)前面加~。

析構(gòu)函數(shù)沒有參數(shù),也沒有返回值,而且不能重載,在一個(gè)類中只能有一個(gè)析構(gòu)函數(shù)。當(dāng)撤銷對象時(shí),編譯器也會(huì)自動(dòng)調(diào)用析構(gòu)函數(shù)。

每一個(gè)類必須有一個(gè)析構(gòu)函數(shù),用戶可以自定義析構(gòu)函數(shù),也可以是編譯器自動(dòng)生成默認(rèn)的析構(gòu)函數(shù)。一般析構(gòu)函數(shù)定義為類的公有成員。

77、構(gòu)造函數(shù)和析構(gòu)函數(shù)可以調(diào)用虛函數(shù)嗎,為什么

1) 在C++中,提倡不在構(gòu)造函數(shù)和析構(gòu)函數(shù)中調(diào)用虛函數(shù);

2) 構(gòu)造函數(shù)和析構(gòu)函數(shù)調(diào)用虛函數(shù)時(shí)都不使用動(dòng)態(tài)聯(lián)編,如果在構(gòu)造函數(shù)或析構(gòu)函數(shù)中調(diào)用虛函數(shù),則運(yùn)行的是為構(gòu)造函數(shù)或析構(gòu)函數(shù)自身類型定義的版本;

3) 因?yàn)楦割悓ο髸?huì)在子類之前進(jìn)行構(gòu)造,此時(shí)子類部分的數(shù)據(jù)成員還未初始化,因此調(diào)用子類的虛函數(shù)時(shí)不安全的,故而C++不會(huì)進(jìn)行動(dòng)態(tài)聯(lián)編;

4) 析構(gòu)函數(shù)是用來銷毀一個(gè)對象的,在銷毀一個(gè)對象時(shí),先調(diào)用子類的析構(gòu)函數(shù),然后再調(diào)用基類的析構(gòu)函數(shù)。所以在調(diào)用基類的析構(gòu)函數(shù)時(shí),派生類對象的數(shù)據(jù)成員已經(jīng)銷毀,這個(gè)時(shí)候再調(diào)用子類的虛函數(shù)沒有任何意義。

78、構(gòu)造函數(shù)、析構(gòu)函數(shù)的執(zhí)行順序?構(gòu)造函數(shù)和拷貝構(gòu)造的內(nèi)部都干了啥?

1)     構(gòu)造函數(shù)順序

①   基類構(gòu)造函數(shù)。如果有多個(gè)基類,則構(gòu)造函數(shù)的調(diào)用順序是某類在類派生表中出現(xiàn)的順序,而不是它們在成員初始化表中的順序。

②   成員類對象構(gòu)造函數(shù)。如果有多個(gè)成員類對象則構(gòu)造函數(shù)的調(diào)用順序是對象在類中被聲明的順序,而不是它們出現(xiàn)在成員初始化表中的順序。

③   派生類構(gòu)造函數(shù)。

2)     析構(gòu)函數(shù)順序

①   調(diào)用派生類的析構(gòu)函數(shù);

②   調(diào)用成員類對象的析構(gòu)函數(shù);

③   調(diào)用基類的析構(gòu)函數(shù)。

79、虛析構(gòu)函數(shù)的作用,父類的析構(gòu)函數(shù)是否要設(shè)置為虛函數(shù)?

1)  C++中基類采用virtual虛析構(gòu)函數(shù)是為了防止內(nèi)存泄漏。

具體地說,如果派生類中申請了內(nèi)存空間,并在其析構(gòu)函數(shù)中對這些內(nèi)存空間進(jìn)行釋放。

假設(shè)基類中采用的是非虛析構(gòu)函數(shù),當(dāng)刪除基類指針指向的派生類對象時(shí)就不會(huì)觸發(fā)動(dòng)態(tài)綁定,因而只會(huì)調(diào)用基類的析構(gòu)函數(shù),而不會(huì)調(diào)用派生類的析構(gòu)函數(shù)。

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

所以,為了防止這種情況的發(fā)生,C++中基類的析構(gòu)函數(shù)應(yīng)采用virtual虛析構(gòu)函數(shù)。

2)  純虛析構(gòu)函數(shù)一定得定義,因?yàn)槊恳粋€(gè)派生類析構(gòu)函數(shù)會(huì)被編譯器加以擴(kuò)張,以靜態(tài)調(diào)用的方式調(diào)用其每一個(gè)虛基類以及上一層基類的析構(gòu)函數(shù)。

因此,缺乏任何一個(gè)基類析構(gòu)函數(shù)的定義,就會(huì)導(dǎo)致鏈接失敗,最好不要把虛析構(gòu)函數(shù)定義為純虛析構(gòu)函數(shù)。

80、構(gòu)造函數(shù)析構(gòu)函數(shù)可否拋出異常

1)   C++只會(huì)析構(gòu)已經(jīng)完成的對象,對象只有在其構(gòu)造函數(shù)執(zhí)行完畢才算是完全構(gòu)造妥當(dāng)。在構(gòu)造函數(shù)中發(fā)生異常,控制權(quán)轉(zhuǎn)出構(gòu)造函數(shù)之外。

因此,在對象b的構(gòu)造函數(shù)中發(fā)生異常,對象b的析構(gòu)函數(shù)不會(huì)被調(diào)用。因此會(huì)造成內(nèi)存泄漏。

2)  用auto_ptr對象來取代指針類成員,便對構(gòu)造函數(shù)做了強(qiáng)化,免除了拋出異常時(shí)發(fā)生資源泄漏的危機(jī),不再需要在析構(gòu)函數(shù)中手動(dòng)釋放資源;

3)  如果控制權(quán)基于異常的因素離開析構(gòu)函數(shù),而此時(shí)正有另一個(gè)異常處于作用狀態(tài),C++會(huì)調(diào)用terminate函數(shù)讓程序結(jié)束;

4)  如果異常從析構(gòu)函數(shù)拋出,而且沒有在當(dāng)?shù)剡M(jìn)行捕捉,那個(gè)析構(gòu)函數(shù)便是執(zhí)行不全的。如果析構(gòu)函數(shù)執(zhí)行不全,就是沒有完成他應(yīng)該執(zhí)行的每一件事情。

81、構(gòu)造函數(shù)一般不定義為虛函數(shù)的原因

(1)創(chuàng)建一個(gè)對象時(shí)需要確定對象的類型,而虛函數(shù)是在運(yùn)行時(shí)動(dòng)態(tài)確定其類型的。在構(gòu)造一個(gè)對象時(shí),由于對象還未創(chuàng)建成功,編譯器無法知道對象的實(shí)際類型

(2)虛函數(shù)的調(diào)用需要虛函數(shù)表指針vptr,而該指針存放在對象的內(nèi)存空間中,若構(gòu)造函數(shù)聲明為虛函數(shù),那么由于對象還未創(chuàng)建,還沒有內(nèi)存空間,更沒有虛函數(shù)表vtable地址用來調(diào)用虛構(gòu)造函數(shù)了

(3)虛函數(shù)的作用在于通過父類的指針或者引用調(diào)用它的時(shí)候能夠變成調(diào)用子類的那個(gè)成員函數(shù)。而構(gòu)造函數(shù)是在創(chuàng)建對象時(shí)自動(dòng)調(diào)用的,不可能通過父類或者引用去調(diào)用,因此就規(guī)定構(gòu)造函數(shù)不能是虛函數(shù)

(4)析構(gòu)函數(shù)一般都要聲明為虛函數(shù),這個(gè)應(yīng)該是老生常談了,這里不再贅述

《為什么C++不能有虛構(gòu)造函數(shù),卻可以有虛析構(gòu)函數(shù)》:https://dwz.cn/lnfW9H6m

82、類什么時(shí)候會(huì)析構(gòu)?

1)  對象生命周期結(jié)束,被銷毀時(shí);

2)  delete指向?qū)ο蟮闹羔槙r(shí),或delete指向?qū)ο蟮幕愵愋椭羔槪浠愄摌?gòu)函數(shù)是虛函數(shù)時(shí);

3)  對象i是對象o的成員,o的析構(gòu)函數(shù)被調(diào)用時(shí),對象i的析構(gòu)函數(shù)也被調(diào)用。

83、構(gòu)造函數(shù)或者析構(gòu)函數(shù)中可以調(diào)用虛函數(shù)嗎

簡要結(jié)論:

  • 從語法上講,調(diào)用完全沒有問題。

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

《Effective C++》的解釋是:
派生類對象構(gòu)造期間進(jìn)入基類的構(gòu)造函數(shù)時(shí),對象類型變成了基類類型,而不是派生類類型。同樣,進(jìn)入基類析構(gòu)函數(shù)時(shí),對象也是基類類型。

舉個(gè)例子:

#include 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;//語句1 delete b;
} //輸出結(jié)果 //Base::Fuction //Base::Fuction //------------------------- //Base::Fuction //A::Function //Base::Fuction 

語句1講道理應(yīng)該體現(xiàn)多態(tài)性,執(zhí)行類A中的構(gòu)造和析構(gòu)函數(shù),從實(shí)驗(yàn)結(jié)果來看,語句1并沒有體現(xiàn),執(zhí)行流程是先構(gòu)造基類,所以先調(diào)用基類的構(gòu)造函數(shù),構(gòu)造完成再執(zhí)行A自己的構(gòu)造函數(shù),析構(gòu)時(shí)也是調(diào)用基類的析構(gòu)函數(shù),也就是說構(gòu)造和析構(gòu)中調(diào)用虛函數(shù)并不能達(dá)到目的,應(yīng)該避免

《構(gòu)造函數(shù)或者析構(gòu)函數(shù)中調(diào)用虛函數(shù)會(huì)怎么樣?》:https://dwz.cn/TaJTJONX

84、智能指針的原理、常用的智能指針及實(shí)現(xiàn)

原理

智能指針是一個(gè)類,用來存儲指向動(dòng)態(tài)分配對象的指針,負(fù)責(zé)自動(dòng)釋放動(dòng)態(tài)分配的對象,防止堆內(nèi)存泄漏。動(dòng)態(tài)分配的資源,交給一個(gè)類對象去管理,當(dāng)類對象聲明周期結(jié)束時(shí),自動(dòng)調(diào)用析構(gòu)函數(shù)釋放資源

常用的智能指針

(1) shared_ptr

實(shí)現(xiàn)原理:采用引用計(jì)數(shù)器的方法,允許多個(gè)智能指針指向同一個(gè)對象,每當(dāng)多一個(gè)指針指向該對象時(shí),指向該對象的所有智能指針內(nèi)部的引用計(jì)數(shù)加1,每當(dāng)減少一個(gè)智能指針指向?qū)ο髸r(shí),引用計(jì)數(shù)會(huì)減1,當(dāng)計(jì)數(shù)為0的時(shí)候會(huì)自動(dòng)的釋放動(dòng)態(tài)分配的資源。

  • 智能指針將一個(gè)計(jì)數(shù)器與類指向的對象相關(guān)聯(lián),引用計(jì)數(shù)器跟蹤共有多少個(gè)類對象共享同一指針

  • 每次創(chuàng)建類的新對象時(shí),初始化指針并將引用計(jì)數(shù)置為1

  • 當(dāng)對象作為另一對象的副本而創(chuàng)建時(shí),拷貝構(gòu)造函數(shù)拷貝指針并增加與之相應(yīng)的引用計(jì)數(shù)

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

  • 調(diào)用析構(gòu)函數(shù)時(shí),構(gòu)造函數(shù)減少引用計(jì)數(shù)(如果引用計(jì)數(shù)減至0,則刪除基礎(chǔ)對象)

(2) unique_ptr

unique_ptr采用的是獨(dú)享所有權(quán)語義,一個(gè)非空的unique_ptr總是擁有它所指向的資源。轉(zhuǎn)移一個(gè)unique_ptr將會(huì)把所有權(quán)全部從源指針轉(zhuǎn)移給目標(biāo)指針,源指針被置空;所以unique_ptr不支持普通的拷貝和賦值操作,不能用在STL標(biāo)準(zhǔn)容器中;局部變量的返回值除外(因?yàn)榫幾g器知道要返回的對象將要被銷毀);如果你拷貝一個(gè)unique_ptr,那么拷貝結(jié)束后,這兩個(gè)unique_ptr都會(huì)指向相同的資源,造成在結(jié)束時(shí)對同一內(nèi)存指針多次釋放而導(dǎo)致程序崩潰。

(3) weak_ptr

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

(4) auto_ptr

主要是為了解決“有異常拋出時(shí)發(fā)生內(nèi)存泄漏”的問題 。因?yàn)榘l(fā)生異常而無法正常釋放內(nèi)存。

auto_ptr有拷貝語義,拷貝后源對象變得無效,這可能引發(fā)很嚴(yán)重的問題;而unique_ptr則無拷貝語義,但提供了移動(dòng)語義,這樣的錯(cuò)誤不再可能發(fā)生,因?yàn)楹苊黠@必須使用std::move()進(jìn)行轉(zhuǎn)移。

auto_ptr不支持拷貝和賦值操作,不能用在STL標(biāo)準(zhǔn)容器中。STL容器中的元素經(jīng)常要支持拷貝、賦值操作,在這過程中auto_ptr會(huì)傳遞所有權(quán),所以不能在STL中使用。

智能指針shared_ptr代碼實(shí)現(xiàn):

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& 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;//指向引用計(jì)數(shù)的指針 };

《智能指針的原理及實(shí)現(xiàn)》:https://blog.csdn.net/lizhentao0707/article/details/81156384

85、構(gòu)造函數(shù)的幾種關(guān)鍵字

default

default關(guān)鍵字可以顯式要求編譯器生成合成構(gòu)造函數(shù),防止在調(diào)用時(shí)相關(guān)構(gòu)造函數(shù)類型沒有定義而報(bào)錯(cuò)

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

如果沒有加語句1,語句2會(huì)報(bào)錯(cuò),表示找不到參數(shù)為空的構(gòu)造函數(shù),將其設(shè)置為default可以解決這個(gè)問題

delete

delete關(guān)鍵字可以刪除構(gòu)造函數(shù)、賦值運(yùn)算符函數(shù)等,這樣在使用的時(shí)候會(huì)得到友善的提示

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

在執(zhí)行語句1時(shí),會(huì)提示new方法已經(jīng)被刪除,如果將new設(shè)置為私有方法,則會(huì)報(bào)慘不忍睹的錯(cuò)誤,因此使用delete關(guān)鍵字可以更加人性化的刪除一些默認(rèn)方法

=0

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

《C++構(gòu)造函數(shù)的default和delete》:https://blog.csdn.net/u010591680/article/details/71101737

86、C++的四種強(qiáng)制轉(zhuǎn)換reinterpret_cast/const_cast/static_cast /dynamic_cast

reinterpret_cast

reinterpret_cast(expression)

type-id 必須是一個(gè)指針、引用、算術(shù)類型、函數(shù)指針或者成員指針。它可以用于類型之間進(jìn)行強(qiáng)制轉(zhuǎn)換。

const_cast

const_cast(expression)

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

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

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

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

static_cast

static_cast < type-id > (expression)

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

  • 用于類層次結(jié)構(gòu)中基類(父類)和派生類(子類)之間指針或引用引用的轉(zhuǎn)換

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

  • 進(jìn)行下行轉(zhuǎn)換(把基類指針或引用轉(zhuǎn)換成派生類表示)時(shí),由于沒有動(dòng)態(tài)類型檢查,所以是不安全的

  • 用于基本數(shù)據(jù)類型之間的轉(zhuǎn)換,如把int轉(zhuǎn)換成char,把int轉(zhuǎn)換成enum。這種轉(zhuǎn)換的安全性也要開發(fā)人員來保證。

  • 把空指針轉(zhuǎn)換成目標(biāo)類型的空指針

  • 把任何類型的表達(dá)式轉(zhuǎn)換成void類型

注意:static_cast不能轉(zhuǎn)換掉expression的const、volatile、或者_(dá)_unaligned屬性。

dynamic_cast

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

dynamic_cast(expression)

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

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

dynamic_cast運(yùn)算符可以在執(zhí)行期決定真正的類型,也就是說expression必須是多態(tài)類型。如果下行轉(zhuǎn)換是安全的(也就說,如果基類指針或者引用確實(shí)指向一個(gè)派生類對象)這個(gè)運(yùn)算符會(huì)傳回適當(dāng)轉(zhuǎn)型過的指針。如果 如果下行轉(zhuǎn)換不安全,這個(gè)運(yùn)算符會(huì)傳回空指針(也就是說,基類指針或者引用沒有指向一個(gè)派生類對象)

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

在類層次間進(jìn)行上行轉(zhuǎn)換時(shí),dynamic_cast和static_cast的效果是一樣的

在進(jìn)行下行轉(zhuǎn)換時(shí),dynamic_cast具有類型檢查的功能,比static_cast更安全

舉個(gè)例子:

#include  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_cast int *p = &n; //以下兩者效果相同 char *c = reinterpret_cast<char*> (p); char *c2 =  (char*)(p); cout << "reinterpret_cast輸出:"<< *c2 << endl; //const_cast const 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_cast Son* s1 = static_cast(b1); //同類型轉(zhuǎn)換 Son* s2 = static_cast(b2); //下行轉(zhuǎn)換,不安全 cout << "static_cast輸出:"<< endl; cout << s1->d << endl; cout << s2->d << endl; //下行轉(zhuǎn)換,原先父對象沒有d成員,輸出垃圾值 //dynamic_cast Son* s3 = dynamic_cast(b1); //同類型轉(zhuǎn)換 Son* s4 = dynamic_cast(b2); //下行轉(zhuǎn)換,安全 cout << "dynamic_cast輸出:" << endl; cout << s3->d << endl; if(s4 == nullptr) cout << "s4指針為nullptr" << endl; else cout << s4->d << endl; return 0;
} //輸出結(jié)果 //reinterpret_cast輸出:a //const_cast輸出:100 //static_cast輸出: //2 //-33686019 //dynamic_cast輸出: //2 //s4指針為nullptr 

從輸出結(jié)果可以看出,在進(jìn)行下行轉(zhuǎn)換時(shí),dynamic_cast安全的,如果下行轉(zhuǎn)換不安全的話其會(huì)返回空指針,這樣在進(jìn)行操作的時(shí)候可以預(yù)先判斷。而使用static_cast下行轉(zhuǎn)換存在不安全的情況也可以轉(zhuǎn)換成功,但是直接使用轉(zhuǎn)換后的對象進(jìn)行操作容易造成錯(cuò)誤。

87、C++函數(shù)調(diào)用的壓棧過程

從代碼入手,解釋這個(gè)過程:

#include  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;
} //輸出結(jié)果 //2 //1 //var1=1,var2=2 

當(dāng)函數(shù)從入口函數(shù)main函數(shù)開始執(zhí)行時(shí),編譯器會(huì)將我們操作系統(tǒng)的運(yùn)行狀態(tài),main函數(shù)的返回地址、main的參數(shù)、mian函數(shù)中的變量、進(jìn)行依次壓棧;

當(dāng)main函數(shù)開始調(diào)用func()函數(shù)時(shí),編譯器此時(shí)會(huì)將main函數(shù)的運(yùn)行狀態(tài)進(jìn)行壓棧,再將func()函數(shù)的返回地址、func()函數(shù)的參數(shù)從右到左、func()定義變量依次壓棧;

當(dāng)func()調(diào)用f()的時(shí)候,編譯器此時(shí)會(huì)將func()函數(shù)的運(yùn)行狀態(tài)進(jìn)行壓棧,再將的返回地址、f()函數(shù)的參數(shù)從右到左、f()定義變量依次壓棧

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

《C/C++函數(shù)調(diào)用過程分析》:https://www.cnblogs.com/biyeymyhjob/archive/2012/07/20/2601204.html

《C/C++函數(shù)調(diào)用的壓棧模型》:https://blog.csdn.net/m0_37717595/article/details/80368411

88、說說移動(dòng)構(gòu)造函數(shù)

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

2)  拷貝構(gòu)造函數(shù)中,對于指針,我們一定要采用深層復(fù)制,而移動(dòng)構(gòu)造函數(shù)中,對于指針,我們采用淺層復(fù)制。淺層復(fù)制之所以危險(xiǎn),是因?yàn)閮蓚€(gè)指針共同指向一片內(nèi)存空間,若第一個(gè)指針將其釋放,另一個(gè)指針的指向就不合法了。

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

3)  移動(dòng)構(gòu)造函數(shù)的參數(shù)和拷貝構(gòu)造函數(shù)不同,拷貝構(gòu)造函數(shù)的參數(shù)是一個(gè)左值引用,但是移動(dòng)構(gòu)造函數(shù)的初值是一個(gè)右值引用。意味著,移動(dòng)構(gòu)造函數(shù)的參數(shù)是一個(gè)右值或者將亡值的引用。也就是說,只用用一個(gè)右值,或者將亡值初始化另一個(gè)對象的時(shí)候,才會(huì)調(diào)用移動(dòng)構(gòu)造函數(shù)。而那個(gè)move語句,就是將一個(gè)左值變成一個(gè)將亡值。

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

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

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

由此可見,函數(shù)調(diào)用結(jié)束后,返回值被臨時(shí)存儲到寄存器中,并沒有放到堆或棧中,也就是說與內(nèi)存沒有關(guān)系了。當(dāng)退出函數(shù)的時(shí)候,臨時(shí)變量可能被銷毀,但是返回值卻被放到寄存器中與臨時(shí)變量的生命周期沒有關(guān)系

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

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

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

90、關(guān)于this指針你知道什么?全說出來

  • this指針是類的指針,指向?qū)ο蟮氖椎刂贰?/span>

  • this指針只能在成員函數(shù)中使用,在全局函數(shù)、靜態(tài)成員函數(shù)中都不能用this。

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

this指針的用處

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

this指針的使用

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

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

類的this指針有以下特點(diǎn)

(1)this只能在成員函數(shù)中使用,全局函數(shù)、靜態(tài)函數(shù)都不能使用this。實(shí)際上,成員函數(shù)默認(rèn)第一個(gè)參數(shù)T * const this

如:

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

其中,func的原型在編譯器看來應(yīng)該是:

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

(2)由此可見,this在成員函數(shù)的開始前構(gòu)造,在成員函數(shù)的結(jié)束后清除。這個(gè)生命周期同任何一個(gè)函數(shù)的參數(shù)是一樣的,沒有任何區(qū)別。當(dāng)調(diào)用一個(gè)類的成員函數(shù)時(shí),編譯器將類的指針作為函數(shù)的this參數(shù)傳遞進(jìn)去。如:

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

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

91、幾個(gè)this指針的易混問題

A. this指針是什么時(shí)候創(chuàng)建的?

this在成員函數(shù)的開始執(zhí)行前構(gòu)造,在成員的執(zhí)行結(jié)束后清除。

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

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

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

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

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

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

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

如果不是類,而是結(jié)構(gòu)體的話,那么,如何通過結(jié)構(gòu)指針來訪問結(jié)構(gòu)中的變量呢?如果你明白這一點(diǎn)的話,就很容易理解這個(gè)問題了。

在C++中,類和結(jié)構(gòu)是只有一個(gè)區(qū)別的:類的成員默認(rèn)是private,而結(jié)構(gòu)是public。

this是類的指針,如果換成結(jié)構(gòu)體,那this就是結(jié)構(gòu)的指針了。

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

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

F.每個(gè)類編譯后,是否創(chuàng)建一個(gè)類中函數(shù)表保存函數(shù)指針,以便用來調(diào)用函數(shù)?

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

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

92、構(gòu)造函數(shù)、拷貝構(gòu)造函數(shù)和賦值操作符的區(qū)別

構(gòu)造函數(shù)

對象不存在,沒用別的對象初始化,在創(chuàng)建一個(gè)新的對象時(shí)調(diào)用構(gòu)造函數(shù)

拷貝構(gòu)造函數(shù)

對象不存在,但是使用別的已經(jīng)存在的對象來進(jìn)行初始化

賦值運(yùn)算符

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

舉個(gè)例子:

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

93、拷貝構(gòu)造函數(shù)和賦值運(yùn)算符重載的區(qū)別?

  • 拷貝構(gòu)造函數(shù)是函數(shù),賦值運(yùn)算符是運(yùn)算符重載。

  • 拷貝構(gòu)造函數(shù)會(huì)生成新的類對象,賦值運(yùn)算符不能。

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

  • 形參傳遞是調(diào)用拷貝構(gòu)造函數(shù)(調(diào)用的被賦值對象的拷貝構(gòu)造函數(shù)),但并不是所有出現(xiàn)"="的地方都是使用賦值運(yùn)算符,如下:

    Student s;
    Student s1 = s;    // 調(diào)用拷貝構(gòu)造函數(shù)
    Student s2;
    s2 = s;    // 賦值運(yùn)算符操作

注:類中有指針變量時(shí)要重寫析構(gòu)函數(shù)、拷貝構(gòu)造函數(shù)和賦值運(yùn)算符

94、智能指針的作用;

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

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

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

拷貝和賦值??截愂沟脤ο蟮囊糜?jì)數(shù)增加1,賦值使得原對象引用計(jì)數(shù)減1,當(dāng)計(jì)數(shù)為0時(shí),自動(dòng)釋放內(nèi)存。后來指向的對象引用計(jì)數(shù)加1,指向后來的對象

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

5)  智能指針類將一個(gè)計(jì)數(shù)器與類指向的對象相關(guān)聯(lián),引用計(jì)數(shù)跟蹤該類有多少個(gè)對象共享同一指針。每次創(chuàng)建類的新對象時(shí),初始化指針并將引用計(jì)數(shù)置為1;當(dāng)對象作為另一對象的副本而創(chuàng)建時(shí),拷貝構(gòu)造函數(shù)拷貝指針并增加與之相應(yīng)的引用計(jì)數(shù);對一個(gè)對象進(jìn)行賦值時(shí),賦值操作符減少左操作數(shù)所指對象的引用計(jì)數(shù)(如果引用計(jì)數(shù)為減至0,則刪除對象),并增加右操作數(shù)所指對象的引用計(jì)數(shù);調(diào)用析構(gòu)函數(shù)時(shí),構(gòu)造函數(shù)減少引用計(jì)數(shù)(如果引用計(jì)數(shù)減至0,則刪除基礎(chǔ)對象)。

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

95、說說你了解的auto_ptr作用

1)  auto_ptr的出現(xiàn),主要是為了解決“有異常拋出時(shí)發(fā)生內(nèi)存泄漏”的問題;拋出異常,將導(dǎo)致指針p所指向的空間得不到釋放而導(dǎo)致內(nèi)存泄漏;

2)  auto_ptr構(gòu)造時(shí)取得某個(gè)對象的控制權(quán),在析構(gòu)時(shí)釋放該對象。我們實(shí)際上是創(chuàng)建一個(gè)auto_ptr類型的局部對象,該局部對象析構(gòu)時(shí),會(huì)將自身所擁有的指針空間釋放,所以不會(huì)有內(nèi)存泄漏;

3)  auto_ptr的構(gòu)造函數(shù)是explicit,阻止了一般指針隱式轉(zhuǎn)換為 auto_ptr的構(gòu)造,所以不能直接將一般類型的指針賦值給auto_ptr類型的對象,必須用auto_ptr的構(gòu)造函數(shù)創(chuàng)建對象;

4)  由于auto_ptr對象析構(gòu)時(shí)會(huì)刪除它所擁有的指針,所以使用時(shí)避免多個(gè)auto_ptr對象管理同一個(gè)指針;

5)  Auto_ptr內(nèi)部實(shí)現(xiàn),析構(gòu)函數(shù)中刪除對象用的是delete而不是delete[],所以auto_ptr不能管理數(shù)組;

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

7)  可以通過*和->運(yùn)算符對auto_ptr所有用的指針進(jìn)行提領(lǐng)操作;

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

96、智能指針的循環(huán)引用

循環(huán)引用是指使用多個(gè)智能指針share_ptr時(shí),出現(xiàn)了指針之間相互指向,從而形成環(huán)的情況,有點(diǎn)類似于死鎖的情況,這種情況下,智能指針往往不能正常調(diào)用對象的析構(gòu)函數(shù),從而造成內(nèi)存泄漏。舉個(gè)例子:

#include  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 _pPre; shared_ptr _pNext;
    T _value;
}; void Funtest() { shared_ptrint>> sp1(new Node<int>(1)); shared_ptrint>> 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的引用+1 sp2->_pPre = sp1; //sp2的引用+1 cout << "sp1.use_count:" << sp1.use_count() << endl; cout << "sp2.use_count:" << sp2.use_count() << endl;
} int main() {
    Funtest();
    system("pause"); return 0;
} //輸出結(jié)果 //Node() //Node() //sp1.use_count:1 //sp2.use_count:1 //sp1.use_count:2 //sp2.use_count:2 

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

在實(shí)際編程過程中,應(yīng)該盡量避免出現(xiàn)智能指針之間相互指向的情況,如果不可避免,可以使用弱指針—weak_ptr,它不增加引用計(jì)數(shù),只要出了作用域就會(huì)自動(dòng)析構(gòu)。

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

97、什么是虛擬繼承

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

#include  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,空對象,只有一個(gè)占位 cout << "sizeof(B):" << sizeof B <<endl; // 4,一個(gè)bptr指針,省去占位,不需要對齊 cout << "sizeof(C):" << sizeof C <<endl; // 4,一個(gè)bptr指針,省去占位,不需要對齊 cout << "sizeof(D):" << sizeof D <<endl; // 8,兩個(gè)bptr,省去占位,不需要對齊 }

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

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

98、如何獲得結(jié)構(gòu)成員相對于結(jié)構(gòu)開頭的字節(jié)偏移量

使用 offsetof() 函數(shù)

舉個(gè)例子:

#include  #include  using namespace std; struct S { int x; char y; int z; double a;
}; int main() { cout << offsetof(S, x) << endl; // 0 cout << offsetof(S, y) << endl; // 4 cout << offsetof(S, z) << endl; // 8 cout << offsetof(S, a) << endl; // 12 return 0;
}

在VS2019 + win下 并不是這樣的 cout << offsetof(S, x) << endl; // 0 cout << offsetof(S, y) << endl; // 4 cout << offsetof(S, z) << endl; // 8 cout << offsetof(S, a) << endl; // 16 這里是 16的位置,因?yàn)?nbsp;double是8字節(jié),需要找一個(gè)8的倍數(shù)對齊, 當(dāng)然了,如果加上 #pragma pack(4)指定 4字節(jié)對齊就可以了 #pragma pack(4) struct S { int x; char y; int z; double a;
}; void test02() { cout << offsetof(S, x) << endl; // 0 cout << offsetof(S, y) << endl; // 4 cout << offsetof(S, z) << endl; // 8 cout << offsetof(S, a) << endl; // 12

S結(jié)構(gòu)體中各個(gè)數(shù)據(jù)成員的內(nèi)存空間劃分如下所示,需要注意內(nèi)存對齊

99、靜態(tài)類型和動(dòng)態(tài)類型以及靜態(tài)綁定和動(dòng)態(tài)綁定的總結(jié)

  • 靜態(tài)類型:對象在聲明時(shí)采用的類型,在編譯期既已確定;

  • 動(dòng)態(tài)類型:通常是指一個(gè)指針或引用目前所指對象的類型,是在運(yùn)行期決定的;

  • 靜態(tài)綁定:綁定的是靜態(tài)類型,所對應(yīng)的函數(shù)或?qū)傩砸蕾囉趯ο蟮撵o態(tài)類型,發(fā)生在編譯期;

  • 動(dòng)態(tài)綁定:綁定的是動(dòng)態(tài)類型,所對應(yīng)的函數(shù)或?qū)傩砸蕾囉趯ο蟮膭?dòng)態(tài)類型,發(fā)生在運(yùn)行期;

從上面的定義也可以看出,非虛函數(shù)一般都是靜態(tài)綁定,而虛函數(shù)都是動(dòng)態(tài)綁定(如此才可實(shí)現(xiàn)多態(tài)性)。
舉個(gè)例子:

#include  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的靜態(tài)類型是它聲明的類型C*,動(dòng)態(tài)類型也是C*; B* pb = new B(); //pb的靜態(tài)類型和動(dòng)態(tài)類型也都是B*; A* pa = pc; //pa的靜態(tài)類型是它聲明的類型A*,動(dòng)態(tài)類型是pa所指向的對象pc的類型C*; pa = pb; //pa的動(dòng)態(tài)類型可以更改,現(xiàn)在它的動(dòng)態(tài)類型是B*,但其靜態(tài)類型仍是聲明時(shí)候的A*; C *pnull = NULL; //pnull的靜態(tài)類型是它聲明的類型C*,沒有動(dòng)態(tài)類型,因?yàn)樗赶蛄薔ULL; pa->func(); //A::func() pa的靜態(tài)類型永遠(yuǎn)都是A*,不管其指向的是哪個(gè)子類,都是直接調(diào)用A::func(); pc->func(); //C::func() pc的動(dòng)、靜態(tài)類型都是C*,因此調(diào)用C::func(); pnull->func(); //C::func() 不用奇怪為什么空指針也可以調(diào)用函數(shù),因?yàn)檫@在編譯期就確定了,和指針空不空沒關(guān)系; return 0;
}

如果將A類中的virtual注釋去掉,則運(yùn)行結(jié)果是:

pa->func(); //B::func() 因?yàn)橛辛藇irtual虛函數(shù)特性,pa的動(dòng)態(tài)類型指向B*,因此先在B中查找,找到后直接調(diào)用; pc->func(); //C::func() pc的動(dòng)、靜態(tài)類型都是C*,因此也是先在C中查找; pnull->func(); //空指針異常,因?yàn)槭莊unc是virtual函數(shù),因此對func的調(diào)用只能等到運(yùn)行期才能確定,然后才發(fā)現(xiàn)pnull是空指針; 

在上面的例子中,

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

  • 同樣的空指針也能夠直接調(diào)用no-virtual函數(shù)而不報(bào)錯(cuò)(這也說明一定要做空指針檢查?。。?,因此靜態(tài)綁定不能實(shí)現(xiàn)多態(tài);

  • 如果func是虛函數(shù),那所有的調(diào)用都要等到運(yùn)行時(shí)根據(jù)其指向?qū)ο蟮念愋筒拍艽_定,比起靜態(tài)綁定自然是要有性能損失的,但是卻能實(shí)現(xiàn)多態(tài)特性;

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

至此總結(jié)一下靜態(tài)綁定和動(dòng)態(tài)綁定的區(qū)別:

  • 靜態(tài)綁定發(fā)生在編譯期,動(dòng)態(tài)綁定發(fā)生在運(yùn)行期;

  • 對象的動(dòng)態(tài)類型可以更改,但是靜態(tài)類型無法更改;

  • 要想實(shí)現(xiàn)動(dòng)態(tài),必須使用動(dòng)態(tài)綁定;

  • 在繼承體系中只有虛函數(shù)使用的是動(dòng)態(tài)綁定,其他的全部是靜態(tài)綁定;

    建議:

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

#include  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  哇哦,這是什么情況,調(diào)用了子類的函數(shù),卻使用了基類中參數(shù)的默認(rèn)值! } int main() {
    test2(); return 0;
}

《C++中的靜態(tài)綁定和動(dòng)態(tài)綁定》:https://www.cnblogs.com/lizhenghn/p/3657717.html

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

  • nullptr替代 NULL

  • 引入了 auto 和 decltype 這兩個(gè)關(guān)鍵字實(shí)現(xiàn)了類型推導(dǎo)

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

  • 類和結(jié)構(gòu)體的中初始化列表

  • Lambda 表達(dá)式(匿名函數(shù))

  • std::forward_list(單向鏈表)

  • 右值引用和move語義

101、引用是否能實(shí)現(xiàn)動(dòng)態(tài)綁定,為什么可以實(shí)現(xiàn)?

可以。

引用在創(chuàng)建的時(shí)候必須初始化,在訪問虛函數(shù)時(shí),編譯器會(huì)根據(jù)其所綁定的對象類型決定要調(diào)用哪個(gè)函數(shù)。注意只能調(diào)用虛函數(shù)。

舉個(gè)例子:

#include  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; // 基類類型引用綁定已經(jīng)存在的Son對象,引用必須初始化 s.fun(); //son::fun() b.fun(); //son :: fun() return 0;
}

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

102、全局變量和局部變量有什么區(qū)別?

生命周期不同:全局變量隨主程序創(chuàng)建和創(chuàng)建,隨主程序銷毀而銷毀;局部變量在局部函數(shù)內(nèi)部,甚至局部循環(huán)體等內(nèi)部存在,退出就不存在;

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

操作系統(tǒng)和編譯器通過內(nèi)存分配的位置可以區(qū)分兩者,全局變量分配在全局?jǐn)?shù)據(jù)段并且在程序開始運(yùn)行的時(shí)候被加載,局部變量則分配在堆棧里面 。

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

103、指針加減計(jì)算要注意什么?

指針加減本質(zhì)是對其所指地址的移動(dòng),移動(dòng)的步長跟指針的類型是有關(guān)系的,因此在涉及到指針加減運(yùn)算需要十分小心,加多或者減多都會(huì)導(dǎo)致指針指向一塊未知的內(nèi)存地址,如果再進(jìn)行操作就會(huì)很危險(xiǎn)。

舉個(gè)例子:

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

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

a自增16進(jìn)制0x20之后,其實(shí)際地址變?yōu)?280 + 2*16*4 = 1408,(因?yàn)橐粋€(gè)int占4位,所以要乘4),這樣它們的差值就變成了1312 - 1280 = -96,所以c的值就變成了-96/4 = -24

遇到指針的計(jì)算,需要明確的是指針每移動(dòng)一位,它實(shí)際跨越的內(nèi)存間隔是指針類型的長度,建議都轉(zhuǎn)成10進(jìn)制計(jì)算,計(jì)算結(jié)果除以類型長度取得結(jié)果

104、 怎樣判斷兩個(gè)浮點(diǎn)數(shù)是否相等?

對兩個(gè)浮點(diǎn)數(shù)判斷大小和是否相等不能直接用==來判斷,會(huì)出錯(cuò)!明明相等的兩個(gè)數(shù)比較反而是不相等!對于兩個(gè)浮點(diǎn)數(shù)比較只能通過相減并與預(yù)先設(shè)定的精度比較,記得要取絕對值!浮點(diǎn)數(shù)與0的比較也應(yīng)該注意。與浮點(diǎn)數(shù)的表示方式有關(guān)。

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

1)  機(jī)器用棧來傳遞過程參數(shù)、存儲返回信息、保存寄存器用于以后恢復(fù),以及本地存儲。而為單個(gè)過程分配的那部分棧稱為幀棧;幀??梢哉J(rèn)為是程序棧的一段,它有兩個(gè)端點(diǎn),一個(gè)標(biāo)識起始地址,一個(gè)標(biāo)識著結(jié)束地址,兩個(gè)指針結(jié)束地址指針esp,開始地址指針ebp;

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

3)  過程實(shí)現(xiàn)

①   備份原來的幀指針,調(diào)整當(dāng)前的棧幀指針到棧指針位置;

②   建立起來的棧幀就是為被調(diào)用者準(zhǔn)備的,當(dāng)被調(diào)用者使用棧幀時(shí),需要給臨時(shí)變量分配預(yù)留內(nèi)存;

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

④   恢復(fù)被調(diào)用者寄存器當(dāng)中的值,這一過程其實(shí)是從棧幀中將備份的值再恢復(fù)到寄存器,不過此時(shí)這些值可能已經(jīng)不在棧頂了

⑤   恢復(fù)被調(diào)用者寄存器當(dāng)中的值,這一過程其實(shí)是從棧幀中將備份的值再恢復(fù)到寄存器,不過此時(shí)這些值可能已經(jīng)不在棧頂了。

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

⑦   恢復(fù)調(diào)用者的棧幀,恢復(fù)其實(shí)就是調(diào)整棧幀兩端,使得當(dāng)前棧幀的區(qū)域又回到了原始的位置。

⑧   彈出返回地址,跳出當(dāng)前過程,繼續(xù)執(zhí)行調(diào)用者的代碼。

4)  過程調(diào)用和返回指令

①   call指令

②   leave指令

③   ret指令

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

1) 指針參數(shù)傳遞本質(zhì)上是值傳遞,它所傳遞的是一個(gè)地址值。

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

值傳遞的特點(diǎn)是,被調(diào)函數(shù)對形式參數(shù)的任何操作都是作為局部變量進(jìn)行的,不會(huì)影響主調(diào)函數(shù)的實(shí)參變量的值(形參指針變了,實(shí)參指針不會(huì)變)。

2) 引用參數(shù)傳遞過程中,被調(diào)函數(shù)的形式參數(shù)也作為局部變量在棧中開辟了內(nèi)存空間,但是這時(shí)存放的是由主調(diào)函數(shù)放進(jìn)來的實(shí)參變量的地址。

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

因此,被調(diào)函數(shù)對形參的任何操作都會(huì)影響主調(diào)函數(shù)中的實(shí)參變量。

3) 引用傳遞和指針傳遞是不同的,雖然他們都是在被調(diào)函數(shù)??臻g上的一個(gè)局部變量,但是任何對于引用參數(shù)的處理都會(huì)通過一個(gè)間接尋址的方式操作到主調(diào)函數(shù)中的相關(guān)變量。

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

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

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

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

107、類如何實(shí)現(xiàn)只能靜態(tài)分配和只能動(dòng)態(tài)分配

1)  前者是把new、delete運(yùn)算符重載為private屬性。后者是把構(gòu)造、析構(gòu)函數(shù)設(shè)為protected屬性,再用子類來動(dòng)態(tài)創(chuàng)建

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

①   靜態(tài)建立,靜態(tài)建立一個(gè)類對象,就是由編譯器為對象在??臻g中分配內(nèi)存;

②   動(dòng)態(tài)建立,A *p = new A();動(dòng)態(tài)建立一個(gè)類對象,就是使用new運(yùn)算符為對象在堆空間中分配內(nèi)存。這個(gè)過程分為兩步,第一步執(zhí)行operator new()函數(shù),在堆中搜索一塊內(nèi)存并進(jìn)行分配;第二步調(diào)用類構(gòu)造函數(shù)構(gòu)造對象;

3)  只有使用new運(yùn)算符,對象才會(huì)被建立在堆上,因此只要限制new運(yùn)算符就可以實(shí)現(xiàn)類對象只能建立在棧上,可以將new運(yùn)算符設(shè)為私有。

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

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

結(jié)語

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

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

就醬,拜了個(gè)拜!下期再見!





免責(zé)聲明:本文內(nèi)容由21ic獲得授權(quán)后發(fā)布,版權(quán)歸原作者所有,本平臺僅提供信息存儲服務(wù)。文章僅代表作者個(gè)人觀點(diǎn),不代表本平臺立場,如有問題,請聯(lián)系我們,謝謝!

本站聲明: 本文章由作者或相關(guān)機(jī)構(gòu)授權(quán)發(fā)布,目的在于傳遞更多信息,并不代表本站贊同其觀點(diǎn),本站亦不保證或承諾內(nèi)容真實(shí)性等。需要轉(zhuǎn)載請聯(lián)系該專欄作者,如若文章內(nèi)容侵犯您的權(quán)益,請及時(shí)聯(lián)系本站刪除。
換一批
延伸閱讀

LED驅(qū)動(dòng)電源的輸入包括高壓工頻交流(即市電)、低壓直流、高壓直流、低壓高頻交流(如電子變壓器的輸出)等。

關(guān)鍵字: 驅(qū)動(dòng)電源

在工業(yè)自動(dòng)化蓬勃發(fā)展的當(dāng)下,工業(yè)電機(jī)作為核心動(dòng)力設(shè)備,其驅(qū)動(dòng)電源的性能直接關(guān)系到整個(gè)系統(tǒng)的穩(wěn)定性和可靠性。其中,反電動(dòng)勢抑制與過流保護(hù)是驅(qū)動(dòng)電源設(shè)計(jì)中至關(guān)重要的兩個(gè)環(huán)節(jié),集成化方案的設(shè)計(jì)成為提升電機(jī)驅(qū)動(dòng)性能的關(guān)鍵。

關(guān)鍵字: 工業(yè)電機(jī) 驅(qū)動(dòng)電源

LED 驅(qū)動(dòng)電源作為 LED 照明系統(tǒng)的 “心臟”,其穩(wěn)定性直接決定了整個(gè)照明設(shè)備的使用壽命。然而,在實(shí)際應(yīng)用中,LED 驅(qū)動(dòng)電源易損壞的問題卻十分常見,不僅增加了維護(hù)成本,還影響了用戶體驗(yàn)。要解決這一問題,需從設(shè)計(jì)、生...

關(guān)鍵字: 驅(qū)動(dòng)電源 照明系統(tǒng) 散熱

根據(jù)LED驅(qū)動(dòng)電源的公式,電感內(nèi)電流波動(dòng)大小和電感值成反比,輸出紋波和輸出電容值成反比。所以加大電感值和輸出電容值可以減小紋波。

關(guān)鍵字: LED 設(shè)計(jì) 驅(qū)動(dòng)電源

電動(dòng)汽車(EV)作為新能源汽車的重要代表,正逐漸成為全球汽車產(chǎn)業(yè)的重要發(fā)展方向。電動(dòng)汽車的核心技術(shù)之一是電機(jī)驅(qū)動(dòng)控制系統(tǒng),而絕緣柵雙極型晶體管(IGBT)作為電機(jī)驅(qū)動(dòng)系統(tǒng)中的關(guān)鍵元件,其性能直接影響到電動(dòng)汽車的動(dòng)力性能和...

關(guān)鍵字: 電動(dòng)汽車 新能源 驅(qū)動(dòng)電源

在現(xiàn)代城市建設(shè)中,街道及停車場照明作為基礎(chǔ)設(shè)施的重要組成部分,其質(zhì)量和效率直接關(guān)系到城市的公共安全、居民生活質(zhì)量和能源利用效率。隨著科技的進(jìn)步,高亮度白光發(fā)光二極管(LED)因其獨(dú)特的優(yōu)勢逐漸取代傳統(tǒng)光源,成為大功率區(qū)域...

關(guān)鍵字: 發(fā)光二極管 驅(qū)動(dòng)電源 LED

LED通用照明設(shè)計(jì)工程師會(huì)遇到許多挑戰(zhàn),如功率密度、功率因數(shù)校正(PFC)、空間受限和可靠性等。

關(guān)鍵字: LED 驅(qū)動(dòng)電源 功率因數(shù)校正

在LED照明技術(shù)日益普及的今天,LED驅(qū)動(dòng)電源的電磁干擾(EMI)問題成為了一個(gè)不可忽視的挑戰(zhàn)。電磁干擾不僅會(huì)影響LED燈具的正常工作,還可能對周圍電子設(shè)備造成不利影響,甚至引發(fā)系統(tǒng)故障。因此,采取有效的硬件措施來解決L...

關(guān)鍵字: LED照明技術(shù) 電磁干擾 驅(qū)動(dòng)電源

開關(guān)電源具有效率高的特性,而且開關(guān)電源的變壓器體積比串聯(lián)穩(wěn)壓型電源的要小得多,電源電路比較整潔,整機(jī)重量也有所下降,所以,現(xiàn)在的LED驅(qū)動(dòng)電源

關(guān)鍵字: LED 驅(qū)動(dòng)電源 開關(guān)電源

LED驅(qū)動(dòng)電源是把電源供應(yīng)轉(zhuǎn)換為特定的電壓電流以驅(qū)動(dòng)LED發(fā)光的電壓轉(zhuǎn)換器,通常情況下:LED驅(qū)動(dòng)電源的輸入包括高壓工頻交流(即市電)、低壓直流、高壓直流、低壓高頻交流(如電子變壓器的輸出)等。

關(guān)鍵字: LED 隧道燈 驅(qū)動(dòng)電源
關(guān)閉