第二章 構(gòu)造函數(shù)語意學(xué)
2.1 Default Constructor的構(gòu)造操作
默認構(gòu)造函數(shù)在需要時被編譯器合成出來。
這里的需要分為:程序的需要 和 編譯器的需要塞蹭。
- 程序的需要:即如果編譯器不合成出來,不會發(fā)生錯誤隐圾,但程序語意不是自己想要的必搞。
- 編譯器的需要:即如果編譯器不合成出來,將發(fā)生錯誤的時候禽笑。
當(dāng)編譯器需要的時候,默認構(gòu)造函數(shù)會被合成出來蛤奥,并且只執(zhí)行編譯器所需要的任務(wù)佳镜。(也不會幫你賦初始值)
1.類中帶有 Default Constructor 的類對象成員
class Foo{ public: Foo(), Foo( int ) ... };
class Bar{ public: Foo foo; char *str; };
void foo_bar(){
Bar bar;
if( str ){ }....
}
如果一個class沒有任何構(gòu)造函數(shù),但它內(nèi)含一個對象成員凡桥,而后者有默認構(gòu)造函數(shù)蟀伸,那編譯器需要為該class合成一個默認構(gòu)造函數(shù)(如下)。
Bar::Bar(){
//被合成的默認構(gòu)造函數(shù)只會滿足編譯器的需要缅刽,因此不會為你初始化str
foo.Foo::Foo();
}
若程序員定義了一個默認構(gòu)造函數(shù):
Bar::Bar(){
str = 0;
}
現(xiàn)在程序的需求滿足了啊掏,但是編譯器還需要初始化foo。由于默認構(gòu)造函數(shù)已經(jīng)被顯式定義衰猛,編譯器沒法合成第二個迟蜜。
那么編譯器的行動是:
- 如果一個class 內(nèi)含一個或者一個以上的類對象成員 ,那么class的每一個構(gòu)造函數(shù)必須調(diào)用每一個類成員的默認構(gòu)造函數(shù) 啡省。
- 編譯器會擴張已存在的構(gòu)造函數(shù)們娜睛,在其中安插一些代碼髓霞,使得在執(zhí)行用戶代碼之前,先調(diào)用(調(diào)用順序與對象成員在class 的聲明次序一致)必要的默認構(gòu)造函數(shù)們微姊。
2.類繼承于帶有 Default Constructor 的基類
與前面道理相同:
- 若沒有酸茴,則在類中沒有默認構(gòu)造函數(shù)分预,便合成兢交;
- 若有,則在類中的默認構(gòu)造函數(shù)們在執(zhí)行用戶代碼之前笼痹,先調(diào)用(調(diào)用次序根據(jù)他們的聲明次序)所有基類的默認構(gòu)造函數(shù)配喳。
3.“帶有一個虛函數(shù)”的類
下面兩種情況,同樣需要合成默認構(gòu)造函數(shù):
- class 聲明(或繼承)一個虛函數(shù)坡氯;
- class派生自一個繼承串鏈摊灭,其中一個或者更多的 virtual base class(虛基類)田盈;
因為一個類中若存在虛函數(shù),那就少不了vptr與vtbl涧团。
因此在編譯期間發(fā)生:
一個虛函數(shù)表會被編譯器產(chǎn)生出來,內(nèi)放class 的虛函數(shù)們的地址经磅;
-
在每一個類對象中泌绣,一個額外的pointer member(vptr)會被編譯器合成出來,內(nèi)含相關(guān)的虛函數(shù)表的地址预厌;
【注】每一個類對象的意思是每一個有虛函數(shù)的類阿迈,就是說若基類與子類中都有虛函數(shù),那么在構(gòu)造函數(shù)中轧叽,都會執(zhí)行這兩步苗沧。
在合成的構(gòu)造函數(shù)中,編譯器會為每一個“帶有一個虛函數(shù)”的類(或者其派生類)的對象實例設(shè)定vptr的初值炭晒,并在其中放置虛函數(shù)表的地址待逞。
4.“帶有一個虛基類”的類
這一小節(jié)有點籠統(tǒng)。
Virtual base class的實現(xiàn)法在不同編譯器之間有很大差異网严。然而飒焦,<u>每一個實現(xiàn)的共同點在于必須使 虛基類 在其每一個 派生類 中的位置,能夠在執(zhí)行期準(zhǔn)備妥當(dāng)屿笼。</u>
對于class所定義的每一個constructor 牺荠,編譯器都會安插那些“允許每一個virtual base class 的執(zhí)行期存取操作”的代碼。
意思是:因為沒有辦法在編譯期確定 “虛基類中成員” 的實際偏移位置驴一,因此只能在構(gòu)造函數(shù)中加一些代碼休雌,合法化 “每一個虛基類的執(zhí)行期存取操作”。
總結(jié)
以上4種情況肝断,會導(dǎo)致“編譯器必須為未聲明構(gòu)造函數(shù)的類合成一個默認構(gòu)造函數(shù) ”杈曲,這只是編譯器(而非程序)的需要驰凛。
至于沒有存在這4種情況,而又沒有聲明構(gòu)造函數(shù)的class ,默認構(gòu)造函數(shù)不會被合成出來的担扑。
所有其他的非靜態(tài)變量 恰响,如整數(shù),整數(shù)指針涌献,整數(shù)數(shù)組等是不會被初始化的胚宦,這些初始化操作對程序是必須的,但對編譯器則并非需要的燕垃。
C++新手一般有兩個誤解:
- 任何class 如果沒有定義默認構(gòu)造函數(shù)枢劝,就會被合成出來一個;
- 編譯器合成出來的默認構(gòu)造函數(shù)會明確設(shè)定 class 內(nèi)每一個data member的默認值卜壕;
2.2 Copy Constructor的構(gòu)造操作
有三種情況您旁,會以一個類的內(nèi)容作為另一類對象的初值。
- 最明顯的當(dāng)然是對一個object做明確的初始化操作轴捎;(
X xx = x;
) - 當(dāng)class object被當(dāng)做參數(shù)交給某個函數(shù)鹤盒;
- 當(dāng)函數(shù)返回一個class object;
1.Default Memberwise Initialization(逐個成員初始化)
如果class 沒有提供一個顯式拷貝構(gòu)造函數(shù)時侦副,當(dāng)class object以 “相同class的另一個object” 作為初值時侦锯,其內(nèi)部是以所謂的default memberwise initialization(逐個成員初始化)方式完成的。
【注】這里沒提拷貝構(gòu)造函數(shù)跃洛。
逐個成員初始化:也就是把每一個內(nèi)建的或派生的數(shù)據(jù)成員(例如一個數(shù)組或指針)的值率触,從某個object拷貝一份到另一個object上,但不拷貝其具體內(nèi)容汇竭。例如只拷貝指針地址葱蝗,不拷貝一份新的指針指向的對象,這也就是淺拷貝细燎,不過它并不會拷貝其中member class object两曼,而是以遞歸的方式實行memberwise initialization(就是再到這個member中,進行淺拷貝)玻驻。
memberwise initialization是如何實現(xiàn)的呢悼凑?
答案就是Bitwise Copy Semantics和default copy constructor。如果class展現(xiàn)了Bitwise Copy Semantics璧瞬,則使用bitwise copy户辫,否則編譯器會生成默認拷貝函數(shù)。
也就是說:判斷是否要合成拷貝構(gòu)造函數(shù)的標(biāo)準(zhǔn)嗤锉,是在于class是否展現(xiàn)出所謂的“bitwise copy semantics”(逐位拷貝語意)渔欢。
2.bitwise copy semantics(逐位初始化)
那什么情況下class不展現(xiàn)Bitwise Copy Semantics呢?有四種情況:
(等價于什么時候要用默認拷貝構(gòu)造函數(shù)瘟忱?)
當(dāng)class內(nèi)含有一個類對象成員奥额,而這個類成員內(nèi)有一個默認的copy 構(gòu)造函數(shù)(不論是class設(shè)計者明確聲明苫幢,或者被編譯器合成);
當(dāng)class 繼承自一個基類垫挨,而基類內(nèi)有copy構(gòu)造函數(shù)(不論是class設(shè)計者明確聲明韩肝,或者被編譯器合成);
當(dāng)一個類聲明了一個或多個virtual 函數(shù)
當(dāng)class派生自一個繼承串鏈九榔,其中一個或者多個virtual base class
在前2種情況下哀峻,編譯器必須將“類對象成員”或者“基類”的 copy constructor的調(diào)用操作 安插到 被合成的copy constructor中。
后兩種單獨拿出來講帚屉。
3.重新設(shè)定Virtual Table 的指針
第一章也提到谜诫,因為class 包含virtual function漾峡, 編譯時需要做擴張操作:
- 增加virtual function table攻旦,內(nèi)含有一個有作用的virtual function的地址;
- 創(chuàng)建一個指向virtual function table的指針生逸,安插在class object內(nèi)牢屋。
編譯器對于每一個新產(chǎn)生的class object的vptr都必須被正確地賦值,否則將跑去執(zhí)行其他對象的function了槽袄,其后果是很嚴重的(如圖2)烙无。因此,編譯器導(dǎo)入一個vptr到class之中時遍尺,該class 就不在展現(xiàn)bitwise copy semantics截酷,必須合成copy Constructor并將vptr適當(dāng)?shù)爻跏蓟?/p>
下圖這種同個類,若按照bitwise copy乾戏,沒有錯誤:
但如下圖這種迂苛,若按照bitwise copy,vptr有錯誤:
【注】圖中鼓择,雖然Bear對draw()和animate()函數(shù)重寫三幻,但虛函數(shù)重寫后也是虛函數(shù),因此在虛函數(shù)表里呐能。
4.處理Virtual Base Class Subobject
virtual base class的存在需要特別處理念搬。一個class object 如果以另一個 virtual base class subobject那么也會使“bitwise copy semantics”失效。
每一個編譯器對于虛擬繼承的支持承諾摆出,都是表示必須讓 “derived class object 中的virtual base class subobject 位置” 在執(zhí)行期就準(zhǔn)備妥當(dāng)朗徊,維護 “位置的完整性” 是編譯器的責(zé)任。Bitwise copy semantics 可能會破壞這個位置偎漫,所以編譯器必須自己合成出copy constructor爷恳。
這也就是說,拷貝構(gòu)造函數(shù)和默認構(gòu)造函數(shù)一樣骑丸,需要的時候會進行構(gòu)建舌仍,而并非程序員不寫編譯器就幫著構(gòu)建妒貌。
2.4 初始化列表
下面四種情況必須使用初始化列表來初始化class 的成員:
- 當(dāng)初始化一個reference member時;
- 當(dāng)初始化一個const member時铸豁;
- 當(dāng)調(diào)用一個base class 的 constructor 灌曙,而它擁有一組參數(shù)(其實就是自定義的構(gòu)造函數(shù))時;
- 當(dāng)調(diào)用一個 member class 的 constructor节芥,而它擁有一組參數(shù)時在刺。
編譯器會一一操作初始化列表,以適當(dāng)順序在構(gòu)造函數(shù)內(nèi)安插舒適化操作头镊,并且在任何用戶代碼之前蚣驼。
不過,初始化的順序是class members聲明次序決定的相艇,不是由初始化列表決定的颖杏。