前述
本章的的主題是構(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。
- 把一個(gè)object直接賦值給另一個(gè)object進(jìn)行初值会放。
- 當(dāng)object被當(dāng)做參數(shù)交給某個(gè)函數(shù)
- 當(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”呢就珠?
有四種情況:
- 當(dāng)class內(nèi)含有一個(gè)member class object,而這個(gè)member class內(nèi)有一個(gè)默認(rèn)的copy構(gòu)造函數(shù)(不論是class設(shè)計(jì)者明確聲明醒颖,或者被編譯器合成)
- 當(dāng)class繼承自一個(gè)base class妻怎,而base class有copy構(gòu)造函數(shù)(不論顯式聲明或是被編譯器合成]
- 當(dāng)一個(gè)類聲明了一個(gè)或多個(gè)virtual 函數(shù)
- 當(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)化:
- 首先加上一個(gè)額外參數(shù),其類型是class object的一個(gè)reference朱庆,這個(gè)參數(shù)將被用來放置被“拷貝建構(gòu)”而得的返回值盛泡。
- 在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中的排列順序決定的魂奥。