《深度探索c++對(duì)象模型》(二)

前述

本章的的主題是構(gòu)造函數(shù)語(yǔ)意學(xué)溺欧,主要是挖掘編譯器對(duì)于“對(duì)象構(gòu)造過程”的干涉砸泛,以及對(duì)于“程序形式”和“程序效率”上的沖擊筷弦。


參考書籍及鏈接:《深度探索c++對(duì)象模型》


一恨豁、Default Constructor的構(gòu)造操作

1. 什么時(shí)候才會(huì)合成一個(gè)default construct呢围段?

答案是當(dāng)編譯器需要的時(shí)候呼盆,default constructor會(huì)被合成出來诲祸,只執(zhí)行編譯器所需要的任務(wù)氧急。另外要注意程序的需要和編譯器的需要之間的區(qū)別尸曼,如果程序有需要拐叉,那是程序員的責(zé)任岩遗,就需要自己實(shí)現(xiàn)constructor。
對(duì)于class X凤瘦,如果沒有任何user-declared constructor宿礁,那么會(huì)有一個(gè)default constructor被隱式(implicitly)聲明出來...一個(gè)被隱式聲明出來的default constructor將是一個(gè)trivial(淺薄而無能,沒啥用的)constructor...
一個(gè)nontrivial default constructor在ARM的術(shù)語(yǔ)中就是編譯器需要的那種蔬芥,必要的話由編譯器合成出來梆靖。下面4小節(jié)分別討論nontrivial default constructor的4種情況

2. 幾種對(duì)象構(gòu)建時(shí)的區(qū)別控汉。

Global objects的內(nèi)存保證會(huì)在程序啟動(dòng)的時(shí)候被清0。Local objects配置于程序的堆棧中返吻,heap objects配置于自由空間姑子,都不一定會(huì)被清零,它們的內(nèi)容將是內(nèi)存上次被使用的遺跡测僵。

3. 第一種情況:“帶有Default Constructor”的member class object

如果一個(gè)class沒有任何constructor街佑,但它內(nèi)含一個(gè)member object,而后者有default constructor捍靠,那么這個(gè)class的implicit default constructor就是“nontrivial”沐旨,編譯器為該class合成出一個(gè)default constructor。不過這個(gè)合成操作只有在constructor真正需要被調(diào)用時(shí)才會(huì)發(fā)生榨婆。

4. 多成員對(duì)象的情況磁携。

編譯器的處理是:如果一個(gè)class A內(nèi)含一個(gè)或者一個(gè)以上member class objects,那么class A的每一個(gè)constructor必須調(diào)用每一個(gè)member classes 的default constructor纲辽。編譯器會(huì)擴(kuò)張已存在的constructors,在其中安插一些代碼颜武,使得user code在被執(zhí)行之前,先調(diào)用必要的default constructors拖吼。調(diào)用順序與member objects在class中的聲明次序一致鳞上。

5. 第二種情況:“帶有Default constructor”的base class。

如果一個(gè)沒有任何constructors的class派生自一個(gè)“帶有default constructor”的base class吊档,那么這個(gè)derived class的default constructor會(huì)被視為nontrivial篙议,并因此需要被合成出來。對(duì)于一個(gè)后繼派生的class而言怠硼,這個(gè)合成的constructor和一個(gè)“被顯式提供的default constructor”并沒有差異鬼贱。

注意一點(diǎn),如果有constructor,但沒有default constructor,那就會(huì)對(duì)每一個(gè)constructors進(jìn)行擴(kuò)充香璃。如果亦存在Member Class Object这难,那些default constructor也會(huì)在base class constructor都被調(diào)用之后調(diào)用。

6. 第三種情況:“帶有一個(gè)Virtual Funtion”的class葡秒。

如果class聲明(或繼承)一個(gè)virtual function姻乓,編譯器也需要合成出default constructor或擴(kuò)充construtor。下面兩個(gè)擴(kuò)張行動(dòng)會(huì)在編譯期間發(fā)生:

  • 一個(gè)virtual function table(在cfront中被稱為vtbl)會(huì)被編譯期產(chǎn)生出來眯牧,內(nèi)放class的virtual functions地址蹋岩。
  • 在每一個(gè)class object中,一個(gè)額外的pointer member(也就是vptr)會(huì)被編譯期合成出來学少,內(nèi)含相關(guān)之class vtbl的地址剪个。

編譯器會(huì)為每一個(gè)含有virtual function的class objects的vptr進(jìn)行適當(dāng)?shù)某跏蓟苑胖眠m當(dāng)?shù)膙irtual table地址版确。

7. 第四種情況:“帶有一個(gè)virtual base class”的class扣囊。

如果class派生自一個(gè)繼承串鏈乎折,其中有一個(gè)或更多的virtual base classes編譯器也需要合成出default constructor或擴(kuò)充construtor。其目的在于必須使 virtual base class 在其每一個(gè)derived class object中的位置能夠在執(zhí)行期準(zhǔn)備妥當(dāng)如暖。對(duì)于class所定義的每一個(gè)constructor笆檀。編譯器都會(huì)安插那些“允許每一個(gè)virtual base class 的執(zhí)行期存取操作”的代碼忌堂。

8. 總結(jié)盒至。

除以上四種情況外,在沒有聲明constructor時(shí)就默認(rèn)其是無用的士修, 其default constructor也就不會(huì)被合成出來的枷遂。
在合成的default constructor中,只有base class subobjects和member class objects會(huì)被初始化棋嘲。所有其他的nonstatic data member 酒唉,如整數(shù),整數(shù)指針沸移,整數(shù)數(shù)組等是不會(huì)被初始化的痪伦,這些初始化操作對(duì)程序是必須的,但對(duì)編譯器則并非需要的雹锣。
C++新手一般有兩個(gè)誤解:

  • 任何class 如果沒有定義default constructor 网沾,就會(huì)被合成出來一個(gè)。
  • 編譯器合成出來的default constructor 會(huì)明確設(shè)定 class 內(nèi)每一個(gè)data member的默認(rèn)值蕊爵。

二辉哥、Copy Constructor的構(gòu)造操作

1. 哪些情況需要有copy constructor?

有三種情況攒射,會(huì)以一個(gè)object的內(nèi)容作為另一class object的初值醋旦,即需要有 copy constructor。

    1. 把一個(gè)object直接賦值給另一個(gè)object進(jìn)行初值会放。
    1. 當(dāng)object被當(dāng)做參數(shù)交給某個(gè)函數(shù)
    1. 當(dāng)函數(shù)返回一個(gè)class object饲齐。

一個(gè)class object可用兩種方式復(fù)制得到,一種是被初始化咧最,另一種是賦值捂人。從概念上看,這兩種操作分別是以copy constructor和copy assignment operator完成的窗市。
Default constructors和copy constructor在必要的時(shí)候才由編譯器 產(chǎn)生先慷,這里的“必要”意指當(dāng)class不展現(xiàn)bitwise copy sematics時(shí)。

2. Default Memberwise Initialization

當(dāng)class object以“相同的另一個(gè)object作為初值是咨察,其內(nèi)部是以所謂的default memberwise initialization方式完成的论熙。也就是把每一個(gè)內(nèi)建的或派生的data member(例如一個(gè)數(shù)組或指針)的值,從某個(gè)object拷貝一份到另一個(gè)object上摄狱,但不拷貝其具體內(nèi)容脓诡。例如只拷貝指針地址无午,不拷貝一份新的指針指向的對(duì)象,這也就是淺拷貝祝谚,不過它并不會(huì)拷貝其中member class object宪迟,而是以遞歸的方式實(shí)行memberwise initialization。

3. 遞歸的memberwise initialization是如何實(shí)現(xiàn)的呢交惯?

答案就是Bitwise Copy Semantics和default copy constructor次泽。如果class展現(xiàn)了Bitwise Copy Semantics,則使用bitwise copy(bitwise copy semantics編譯器生成的偽代碼是memcpy函數(shù))席爽,否則編譯器會(huì)生成default copy constructor意荤。

4. Memberwise copy(深拷貝)與Bitwise copy(淺拷貝)的區(qū)別

Memberwise copy: 在初始化一個(gè)對(duì)象期間,基類的構(gòu)造函數(shù)被調(diào)用,成員變量被調(diào)用,如果它們有構(gòu)造函數(shù)的時(shí)候,它們的構(gòu)造函數(shù)被調(diào)用,這個(gè)過程是一個(gè)遞歸的過程。
Bitwise copy: 原內(nèi)存拷貝只锻。例子,給定一個(gè)對(duì)象object,它的類型是class Base玖像。對(duì)象object占用10字節(jié)的內(nèi)存,地址從0x0到0x9.如果還有一個(gè)對(duì)象objectTwo,類型也是class Base。那么執(zhí)行objectTwo = object;如果使用Bitwise拷貝語(yǔ)義,那么將會(huì)拷貝從0x0到0x9的數(shù)據(jù)到objectTwo的內(nèi)存地址齐饮,也就是說Bitwise是字節(jié)到字節(jié)的拷貝捐寥。

對(duì)于默認(rèn)的拷貝構(gòu)造函數(shù)不會(huì)使用深拷貝,它只是使用淺拷貝。這意味著類的所有的成員是一層深度的拷貝而已祖驱。如果你的類或結(jié)構(gòu)體成員中只是包含基本的數(shù)據(jù)類型例如int, float, char,那么Memberwise copy與Bitwise copy基本是相同的握恳。但如果類中有指針存在,那么你可能會(huì)遇到問題。
例如下面的例子:

class A
{
   int m;
   double d;
   char *Str;
};

如果你創(chuàng)建兩個(gè)這樣的類對(duì)象,class A  a, b;并且你給a賦值,      
a.m = 6;   
a.d = 10.123;   
a.Str = new char[10];   
astrcpy(a.Str, "test");//這里是淺拷貝   
如果執(zhí)行b = a;那么會(huì)把對(duì)象a的每一個(gè)成員的值賦值給b的每個(gè)成員羹膳。   
b.m = a.m;    
b.d = a.d;   
b.Str = a.Str;//現(xiàn)在對(duì)象a和b的成員Str都執(zhí)向相同的內(nèi)存,刪除任一個(gè)內(nèi)存都會(huì)析放另一個(gè)對(duì)象的內(nèi)存睡互。   

所以你需要深拷貝,它不是拷貝的內(nèi)存地址而是拷貝內(nèi)存地址的內(nèi)容。一個(gè)默認(rèn)的拷貝構(gòu)造函數(shù)經(jīng)常執(zhí)行淺拷貝,只有擁有自己的拷貝函數(shù)才可以實(shí)現(xiàn)深拷貝陵像。

5. 什么時(shí)候一個(gè)class不展現(xiàn)出“bitwise copy semantics”呢就珠?

有四種情況:

    1. 當(dāng)class內(nèi)含有一個(gè)member class object,而這個(gè)member class內(nèi)有一個(gè)默認(rèn)的copy構(gòu)造函數(shù)(不論是class設(shè)計(jì)者明確聲明醒颖,或者被編譯器合成)
    1. 當(dāng)class繼承自一個(gè)base class妻怎,而base class有copy構(gòu)造函數(shù)(不論顯式聲明或是被編譯器合成]
    1. 當(dāng)一個(gè)類聲明了一個(gè)或多個(gè)virtual 函數(shù)
    1. 當(dāng)class派生自一個(gè)繼承串鏈,其中一個(gè)或者多個(gè)virtual base class

6. 重新設(shè)定Virtual Table的指針(virtual funtion的情況)

當(dāng)編譯器導(dǎo)入一個(gè)vptr到class之中時(shí)泞歉,該class就不再展現(xiàn)bitwise semantics了逼侦。編譯器需要合成出一個(gè)copy constructor,以求將vptr適當(dāng)?shù)爻跏蓟?br> 當(dāng)一個(gè)base class object以其derived class的object內(nèi)容做初始化操作時(shí)腰耙,其vptr復(fù)制操作也必須要保證安全(非pointer和reference)榛丢。也就是說,合成出來的基類構(gòu)造函數(shù)會(huì)顯式設(shè)定object的vptr指向基類對(duì)應(yīng)的virtual table挺庞,而不是直接將右手邊的class object中將其vptr現(xiàn)值拷貝過來晰赞。

7. 如何處理virtual base class subobject的情況?

virtual base class的存在需要特別處理。一個(gè)class object如果以另一個(gè)object作為初值掖鱼,而后者有一個(gè)virtual base class subobject然走,那么也會(huì)使“bitwise copy semantics”失效。
這時(shí)需要合成一個(gè)copy constructor,從而安插一些代碼以設(shè)定virtualbase class pointer/offset的初值戏挡,對(duì)每一個(gè)members執(zhí)行必要的memberwise初始化操作芍瑞,以及執(zhí)行其他的內(nèi)存相關(guān)工作。

三褐墅、程序轉(zhuǎn)化語(yǔ)意學(xué)(Program Transformation Semantics)

1. class object的顯式初始化操作拆檬。

初始化object時(shí),必要的程序轉(zhuǎn)化有以下兩個(gè)階段:

  • 重寫每一個(gè)定義掌栅,其中的初始化操作會(huì)被剝除秩仆,在c++中,“定義”指占用內(nèi)存的行為猾封。
  • class的copy constructor調(diào)用操作會(huì)被安插進(jìn)去。

2. 參數(shù)的初始化所做的程序轉(zhuǎn)換噪珊。

C++ Standard說晌缘,把一個(gè)class object當(dāng)做參數(shù)傳給一個(gè)函數(shù)(或是作為一個(gè)函數(shù)的返回值),相當(dāng)于以下形式的初始化操作:

X xx = arg;//其中xx代表形式參數(shù)(或返回值)而arg代表真正的參數(shù)值

//因此痢站,若已知如下函數(shù):
void foo(X xo);
 
//轉(zhuǎn)換的結(jié)果為:
X xx;
//xo以memberwise的方式將xx當(dāng)作初值...
foo(xx);

有一種策略是導(dǎo)入所謂的臨時(shí)性object磷箕,并調(diào)用copy constructor將它初始化,然后將此臨時(shí)性object交給函數(shù)阵难,臨時(shí)性object會(huì)在函數(shù)結(jié)束處被析構(gòu)岳枷。

3. 返回值的初始化所做的程序轉(zhuǎn)換。

函數(shù)bar()的返回值為一個(gè)對(duì)象呜叫,那該怎么把局部對(duì)象xx拷貝過來空繁? Stroustrup在cfront中的解決辦法是一個(gè)雙階段的轉(zhuǎn)化:

    1. 首先加上一個(gè)額外參數(shù),其類型是class object的一個(gè)reference朱庆,這個(gè)參數(shù)將被用來放置被“拷貝建構(gòu)”而得的返回值盛泡。
    1. 在return指令之前安插一個(gè)copy constructor調(diào)用操作,以便將欲傳回之object的內(nèi)容當(dāng)做上述新增參數(shù)的初值娱颊。函數(shù)也對(duì)應(yīng)變?yōu)関oid類型傲诵。

4. 在編譯器層面所做的優(yōu)化。

編譯器會(huì)以result參數(shù)取代name return val箱硕。這樣的編譯器優(yōu)化操作拴竹,有時(shí)被稱為Named Return Value(NRV)優(yōu)化。NRV優(yōu)化如今被視為是標(biāo)準(zhǔn)C++編譯器的一個(gè)義不容辭的優(yōu)化操作剧罩。NRV需要一定的條件栓拜,即對(duì)應(yīng)的類要有copy constructor
一般而言,面對(duì)“以一個(gè)class object作為另一個(gè)class object的初值”的情形菱属,語(yǔ)言允許編譯器有大量的自由發(fā)揮空間钳榨。其優(yōu)點(diǎn)當(dāng)然是導(dǎo)致機(jī)器碼產(chǎn)生時(shí)有明顯的效率提升。缺點(diǎn)則是你不能安全地規(guī)劃你的copy constructor的副作用纽门,必須視其執(zhí)行而定薛耻。

NRV與返回值初始化的區(qū)別在于:NRV中不產(chǎn)生local object,直接以_result帶入其中進(jìn)行各種處理赏陵,減少調(diào)用copy constructor饼齿。而返回值初始化則是在最后用copy constructor將local object的值拷貝給_result, 中間不處理_result。一個(gè)是優(yōu)化蝙搔,一個(gè)是程序轉(zhuǎn)換缕溉。

5. 那Copy Constructor要還是不要?

copy constructor的應(yīng)用吃型,迫使編譯器多多少少對(duì)你的程序代碼做部分優(yōu)化证鸥。尤其當(dāng)一個(gè)函數(shù)以傳值(by value)的方式傳回一個(gè)class object,而該class有一個(gè)copy constructor(不論是明確定義出來的勤晚,或是合成的)時(shí)枉层。這將導(dǎo)致深?yuàn)W的程序轉(zhuǎn)化——不論在函數(shù)的定義或使用上,此外編譯器也將copy constructor的調(diào)用操作優(yōu)化赐写,以一個(gè)額外的第一參數(shù)(數(shù)值被直接存放在其中)取代NRV鸟蜡。

  • 如果編譯器能自動(dòng)為你實(shí)施了最好的行為,那就沒有必要實(shí)現(xiàn)一個(gè)自己的copy constructor。
  • 如果class需要大量的memberwise初始化操作挺邀,例如以傳值的方式傳回object揉忘,此時(shí)提供一個(gè)explicit inline copy constructor就是非常合理的(在有NRV的前提下)。

四端铛、成員們的初始化隊(duì)伍(Memeber Initialization List)

1. 在下列情況下泣矛,為了讓你的程序能夠順利編譯,你必須使用member initialization list:

  • 當(dāng)初始化一個(gè)reference member時(shí)
  • 當(dāng)初始化一個(gè)const member時(shí)
  • 當(dāng)調(diào)用一個(gè)base class的constructor沦补,而它擁有一組參數(shù)時(shí)
  • 當(dāng)調(diào)用一個(gè)member class的constructor乳蓄,而它擁有一組參數(shù)時(shí)

2.member initialization list中到底會(huì)發(fā)生什么事情?

編譯器會(huì)一一操作initialization list夕膀,以適當(dāng)順序在constructor之內(nèi)安插初始化操作虚倒,并且在任何explicit user code之前。
initialization list中的項(xiàng)目順序是由class中的members聲明順序決定的产舞,不是由initialization list中的排列順序決定的魂奥。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市易猫,隨后出現(xiàn)的幾起案子耻煤,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,997評(píng)論 6 502
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件哈蝇,死亡現(xiàn)場(chǎng)離奇詭異棺妓,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)炮赦,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,603評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門怜跑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人吠勘,你說我怎么就攤上這事性芬。” “怎么了剧防?”我有些...
    開封第一講書人閱讀 163,359評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵植锉,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我峭拘,道長(zhǎng)俊庇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,309評(píng)論 1 292
  • 正文 為了忘掉前任棚唆,我火速辦了婚禮暇赤,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘宵凌。我一直安慰自己,他們只是感情好止后,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,346評(píng)論 6 390
  • 文/花漫 我一把揭開白布瞎惫。 她就那樣靜靜地躺著,像睡著了一般译株。 火紅的嫁衣襯著肌膚如雪瓜喇。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,258評(píng)論 1 300
  • 那天歉糜,我揣著相機(jī)與錄音乘寒,去河邊找鬼。 笑死匪补,一個(gè)胖子當(dāng)著我的面吹牛伞辛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播夯缺,決...
    沈念sama閱讀 40,122評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼蚤氏,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了踊兜?” 一聲冷哼從身側(cè)響起竿滨,我...
    開封第一講書人閱讀 38,970評(píng)論 0 275
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后于游,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體毁葱,經(jīng)...
    沈念sama閱讀 45,403評(píng)論 1 313
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,596評(píng)論 3 334
  • 正文 我和宋清朗相戀三年贰剥,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了倾剿。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,769評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡鸠澈,死狀恐怖柱告,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情笑陈,我是刑警寧澤际度,帶...
    沈念sama閱讀 35,464評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站涵妥,受9級(jí)特大地震影響乖菱,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜蓬网,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,075評(píng)論 3 327
  • 文/蒙蒙 一窒所、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧帆锋,春花似錦吵取、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,705評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至实辑,卻和暖如春捺氢,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背剪撬。 一陣腳步聲響...
    開封第一講書人閱讀 32,848評(píng)論 1 269
  • 我被黑心中介騙來泰國(guó)打工摄乒, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人残黑。 一個(gè)月前我還...
    沈念sama閱讀 47,831評(píng)論 2 370
  • 正文 我出身青樓馍佑,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親萍摊。 傳聞我的和親對(duì)象是個(gè)殘疾皇子挤茄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,678評(píng)論 2 354

推薦閱讀更多精彩內(nèi)容