VC++,掀起你的蓋頭來(lái)
——談VC++對(duì)象模型
(美)簡(jiǎn) ? 格雷
程化 譯
譯者前言
一個(gè)C++程序員,想要進(jìn)一步提升技術(shù)水平的話,應(yīng)該多了解一些語(yǔ)言的語(yǔ)意細(xì)節(jié)。對(duì)于使用VC++的程序員來(lái)說(shuō),還應(yīng)該了解一些VC++對(duì)于C++的詮釋。Inside the C++ Object Model雖然是一本好書,然而,書的篇幅多一些,又和具體的VC++關(guān)系小一些。因此,從篇幅和內(nèi)容來(lái)看,譯者認(rèn)為本文是深入理解C++對(duì)象模型比較好的一個(gè)出發(fā)點(diǎn)。
這篇文章以前看到時(shí)就覺(jué)得很好,舊文重讀,感覺(jué)理解得更多一些了,于是產(chǎn)生了翻譯出來(lái),與大家共享的想法。雖然文章不長(zhǎng),但時(shí)間有限,又若干次在翻譯時(shí)打盹睡著,拖拖拉拉用了小一個(gè)月。
一方面因本人水平所限,另一方面因翻譯時(shí)經(jīng)常打盹,錯(cuò)誤之處恐怕不少,歡迎大家批評(píng)指正。
1 前言
了解你所使用的編程語(yǔ)言究竟是如何實(shí)現(xiàn)的,對(duì)于C++程序員可能特別有意義。首先,它可以去除我們對(duì)于所使用語(yǔ)言的神秘感,使我們不至于對(duì)于編譯器干的活感到完全不可思議;尤其重要的是,它使我們?cè)贒ebug和使用語(yǔ)言高級(jí)特性的時(shí)候,有更多的把握。當(dāng)需要提高代碼效率的時(shí)候,這些知識(shí)也能夠很好地幫助我們。
本文著重回答這樣一些問(wèn)題:
? * 類如何布局?
? * 成員變量如何訪問(wèn)?
? * 成員函數(shù)如何訪問(wèn)?
? * 所謂的“調(diào)整塊”(adjuster thunk)是怎么回事?
? * 使用如下機(jī)制時(shí),開(kāi)銷如何:
? * 單繼承、多重繼承、虛繼承
? * 虛函數(shù)調(diào)用
? * 強(qiáng)制轉(zhuǎn)換到基類,或者強(qiáng)制轉(zhuǎn)換到虛基類
? * 異常處理
首先,我們順次考察C兼容的結(jié)構(gòu)(struct)的布局,單繼承,多重繼承,以及虛繼承;
接著,我們講成員變量和成員函數(shù)的訪問(wèn),當(dāng)然,這里面包含虛函數(shù)的情況;
再接下來(lái),我們考察構(gòu)造函數(shù),析構(gòu)函數(shù),以及特殊的賦值操作符成員函數(shù)是如何工作的,數(shù)組是如何動(dòng)態(tài)構(gòu)造和銷毀的;
最后,簡(jiǎn)單地介紹對(duì)異常處理的支持。
對(duì)每個(gè)語(yǔ)言特性,我們將簡(jiǎn)要介紹該特性背后的動(dòng)機(jī),該特性自身的語(yǔ)意(當(dāng)然,本文決不是“C++入門”,大家對(duì)此要有充分認(rèn)識(shí)),以及該特性在微軟的VC++中是如何實(shí)現(xiàn)的。這里要注意區(qū)分抽象的C++語(yǔ)言語(yǔ)意與其特定實(shí)現(xiàn)。微軟之外的其他C++廠商可能提供一個(gè)完全不同的實(shí)現(xiàn),我們偶爾也會(huì)將VC++的實(shí)現(xiàn)與其他實(shí)現(xiàn)進(jìn)行比較。
2 類布局
本節(jié)討論不同的繼承方式造成的不同內(nèi)存布局。
2.1 C結(jié)構(gòu)(struct)
由于C++基于C,所以C++也“基本上”兼容C。特別地,C++規(guī)范在“結(jié)構(gòu)”上使用了和C相同的,簡(jiǎn)單的內(nèi)存布局原則:成員變量按其被聲明的順序排列,按具體實(shí)現(xiàn)所規(guī)定的對(duì)齊原則在內(nèi)存地址上對(duì)齊。所有的C/C++廠商都保證他們的C/C++編譯器對(duì)于有效的C結(jié)構(gòu)采用完全相同的布局。這里,A是一個(gè)簡(jiǎn)單的C結(jié)構(gòu),其成員布局和對(duì)齊方式都一目了然。

struct A {
char c;
int i;
};
譯者注:從上圖可見(jiàn),A在內(nèi)存中占有8個(gè)字節(jié),按照聲明成員的順序,前4個(gè)字節(jié)包含一個(gè)字符(實(shí)際占用1個(gè)字節(jié),3個(gè)字節(jié)空著,補(bǔ)對(duì)齊),后4個(gè)字節(jié)包含一個(gè)整數(shù)。A的指針就指向字符開(kāi)始字節(jié)處。
2.2 有C++特征的C結(jié)構(gòu)
當(dāng)然了,C++不是復(fù)雜的C,C++本質(zhì)上是面向?qū)ο蟮恼Z(yǔ)言:包含繼承、封裝,以及多態(tài)。原始的C結(jié)構(gòu)經(jīng)過(guò)改造,成了面向?qū)ο笫澜绲幕悺3顺蓡T變量外,C++類還可以封裝成員函數(shù)和其他東西。然而,有趣的是,除非為了實(shí)現(xiàn)虛函數(shù)和虛繼承引入的隱藏成員變量外,C++類實(shí)例的大小完全取決于一個(gè)類及其基類的成員變量!成員函數(shù)基本上不影響類實(shí)例的大小。
這里提供的B是一個(gè)C結(jié)構(gòu),然而,該結(jié)構(gòu)有一些C++特征:控制成員可見(jiàn)性的“public/protected/private”關(guān)鍵字、成員函數(shù)、靜態(tài)成員,以及嵌套的類型聲明。雖然看著琳瑯滿目,實(shí)際上只有成員變量才占用類實(shí)例的空間。要注意的是,C++標(biāo)準(zhǔn)委員會(huì)不限制由“public/protected/private”關(guān)鍵字分開(kāi)的各段在實(shí)現(xiàn)時(shí)的先后順序,因此,不同的編譯器實(shí)現(xiàn)的內(nèi)存布局可能并不相同。(在VC++中,成員變量總是按照聲明時(shí)的順序排列)。

struct B {
public:
int bm1;
protected:
int bm2;
private:
int bm3;
static int bsm;
void bf();
static void bsf();
typedef void* bpv;
struct N { };
};
譯者注:B中,為何static int bsm不占用內(nèi)存空間?因?yàn)樗庆o態(tài)成員,該數(shù)據(jù)存放在程序的數(shù)據(jù)段中,不在類實(shí)例中。
2.3 單繼承
C++提供繼承的目的是在不同的類型之間提取共性。比如,科學(xué)家對(duì)物種進(jìn)行分類,從而有種、屬、綱等說(shuō)法。有了這種層次結(jié)構(gòu),我們才可能將某些具備特定性質(zhì)的東西歸入到最合適的分類層次上,如“懷孩子的是哺乳動(dòng)物”。由于這些屬性可以被子類繼承,所以,我們只要知道“鯨魚、人”是哺乳動(dòng)物,就可以方便地指出“鯨魚、人都可以懷孩子”。那些特例,如鴨嘴獸(生蛋的哺乳動(dòng)物),則要求我們對(duì)缺省的屬性或行為進(jìn)行覆蓋。
C++中的繼承語(yǔ)法很簡(jiǎn)單,在子類后加上“:base”就可以了。下面的D繼承自基類C。

struct C {
int c1;
void cf();
};

struct D : C {
int d1;
void df();
};
既然派生類要保留基類的所有屬性和行為,自然地,每個(gè)派生類的實(shí)例都包含了一份完整的基類實(shí)例數(shù)據(jù)。在D中,并不是說(shuō)基類C的數(shù)據(jù)一定要放在D的數(shù)據(jù)之前,只不過(guò)這樣放的話,能夠保證D中的C對(duì)象地址,恰好是D對(duì)象地址的第一個(gè)字節(jié)。這種安排之下,有了派生類D的指針,要獲得基類C的指針,就不必要計(jì)算偏移量了。幾乎所有知名的C++廠商都采用這種內(nèi)存安排。在單繼承類層次下,每一個(gè)新的派生類都簡(jiǎn)單地把自己的成員變量添加到基類的成員變量之后。看看上圖,C對(duì)象指針和D對(duì)象指針指向同一地址。
2.4 多重繼承
大多數(shù)情況下,其實(shí)單繼承就足夠了。但是,C++為了我們的方便,還提供了多重繼承。
比如,我們有一個(gè)組織模型,其中有經(jīng)理類(分任務(wù)),工人類(干活)。那么,對(duì)于一線經(jīng)理類,即既要從上級(jí)經(jīng)理那里領(lǐng)取任務(wù)干活,又要向下級(jí)工人分任務(wù)的角色來(lái)說(shuō),如何在類層次中表達(dá)呢?單繼承在此就有點(diǎn)力不勝任。我們可以安排經(jīng)理類先繼承工人類,一線經(jīng)理類再繼承經(jīng)理類,但這種層次結(jié)構(gòu)錯(cuò)誤地讓經(jīng)理類繼承了工人類的屬性和行為。反之亦然。當(dāng)然,一線經(jīng)理類也可以僅僅從一個(gè)類(經(jīng)理類或工人類)繼承,或者一個(gè)都不繼承,重新聲明一個(gè)或兩個(gè)接口,但這樣的實(shí)現(xiàn)弊處太多:多態(tài)不可能了;未能重用現(xiàn)有的接口;最嚴(yán)重的是,當(dāng)接口變化時(shí),必須多處維護(hù)。最合理的情況似乎是一線經(jīng)理從兩個(gè)地方繼承屬性和行為——經(jīng)理類、工人類。
C++就允許用多重繼承來(lái)解決這樣的問(wèn)題:
struct Manager ... { ... };
struct Worker ... { ... };
struct MiddleManager : Manager, Worker { ... };
這樣的繼承將造成怎樣的類布局呢?下面我們還是用“字母類”來(lái)舉例:

struct E {
int e1;
void ef();
};

struct F : C, E {
int f1;
void ff();
};
結(jié)構(gòu)F從C和E多重繼承得來(lái)。與單繼承相同的是,F(xiàn)實(shí)例拷貝了每個(gè)基類的所有數(shù)據(jù)。與單繼承不同的是,在多重繼承下,內(nèi)嵌的兩個(gè)基類的對(duì)象指針不可能全都與派生類對(duì)象指針相同:
F f;
// (void*)&f == (void*)(C*)&f;
// (void*)&f < (void*)(E*)&f;
譯者注:上面那行說(shuō)明C對(duì)象指針與F對(duì)象指針相同,下面那行說(shuō)明E對(duì)象指針與F對(duì)象指針不同。
觀察類布局,可以看到F中內(nèi)嵌的E對(duì)象,其指針與F指針并不相同。正如后文討論強(qiáng)制轉(zhuǎn)化和成員函數(shù)時(shí)指出的,這個(gè)偏移量會(huì)造成少量的調(diào)用開(kāi)銷。
具體的編譯器實(shí)現(xiàn)可以自由地選擇內(nèi)嵌基類和派生類的布局。VC++按照基類的聲明順序先排列基類實(shí)例數(shù)據(jù),最后才排列派生類數(shù)據(jù)。當(dāng)然,派生類數(shù)據(jù)本身也是按照聲明順序布局的(本規(guī)則并非一成不變,我們會(huì)看到,當(dāng)一些基類有虛函數(shù)而另一些基類沒(méi)有時(shí),內(nèi)存布局并非如此)。
2.5 虛繼承
回到我們討論的一線經(jīng)理類例子。讓我們考慮這種情況:如果經(jīng)理類和工人類都繼承自“雇員類”,將會(huì)發(fā)生什么?
struct Employee { ... };
struct Manager : Employee { ... };
struct Worker : Employee { ... };
struct MiddleManager : Manager, Worker { ... };
如果經(jīng)理類和工人類都繼承自雇員類,很自然地,它們每個(gè)類都會(huì)從雇員類獲得一份數(shù)據(jù)拷貝。如果不作特殊處理,一線經(jīng)理類的實(shí)例將含有兩個(gè)雇員類實(shí)例,它們分別來(lái)自兩個(gè)雇員基類。如果雇員類成員變量不多,問(wèn)題不嚴(yán)重;如果成員變量眾多,則那份多余的拷貝將造成實(shí)例生成時(shí)的嚴(yán)重開(kāi)銷。更糟的是,這兩份不同的雇員實(shí)例可能分別被修改,造成數(shù)據(jù)的不一致。因此,我們需要讓經(jīng)理類和工人類進(jìn)行特殊的聲明,說(shuō)明它們?cè)敢夤蚕硪环莨蛦T基類實(shí)例數(shù)據(jù)。
很不幸,在C++中,這種“共享繼承”被稱為“虛繼承”,把問(wèn)題搞得似乎很抽象。虛繼承的語(yǔ)法很簡(jiǎn)單,在指定基類時(shí)加上virtual關(guān)鍵字即可。
struct Employee { ... };
struct Manager : virtual Employee { ... };
struct Worker : virtual Employee { ... };
struct MiddleManager : Manager, Worker { ... };
使用虛繼承,比起單繼承和多重繼承有更大的實(shí)現(xiàn)開(kāi)銷、調(diào)用開(kāi)銷。回憶一下,在單繼承和多重繼承的情況下,內(nèi)嵌的基類實(shí)例地址比起派生類實(shí)例地址來(lái),要么地址相同(單繼承,以及多重繼承的最靠左基類),要么地址相差一個(gè)固定偏移量(多重繼承的非最靠左基類)。然而,當(dāng)虛繼承時(shí),一般說(shuō)來(lái),派生類地址和其虛基類地址之間的偏移量是不固定的,因?yàn)槿绻@個(gè)派生類又被進(jìn)一步繼承的話,最終派生類會(huì)把共享的虛基類實(shí)例數(shù)據(jù)放到一個(gè)與上一層派生類不同的偏移量處。請(qǐng)看下例:

struct G : virtual C {
int g1;
void gf();
};
譯者注:GdGvbptrG(In G, the displacement of G’s virtual base pointer to G)意思是:在G中,G對(duì)象的指針與G的虛基類表指針之間的偏移量,在此可見(jiàn)為0,因?yàn)镚對(duì)象內(nèi)存布局第一項(xiàng)就是虛基類表指針; GdGvbptrC(In G, the displacement of G’s virtual base pointer to C)意思是:在G中,C對(duì)象的指針與G的虛基類表指針之間的偏移量,在此可見(jiàn)為4。

struct H : virtual C {
int h1;
void hf();
};
struct I : G, H {
int i1;
void _if();
};
暫時(shí)不追究vbptr成員變量從何而來(lái)。從上面這些圖可以直觀地看到,在G對(duì)象中,內(nèi)嵌的C基類對(duì)象的數(shù)據(jù)緊跟在G的數(shù)據(jù)之后,在H對(duì)象中,內(nèi)嵌的C基類對(duì)象的數(shù)據(jù)也緊跟在H的數(shù)據(jù)之后。但是,在I對(duì)象中,內(nèi)存布局就并非如此了。VC++實(shí)現(xiàn)的內(nèi)存布局中,G對(duì)象實(shí)例中G對(duì)象和C對(duì)象之間的偏移,不同于I對(duì)象實(shí)例中G對(duì)象和C對(duì)象之間的偏移。當(dāng)使用指針訪問(wèn)虛基類成員變量時(shí),由于指針可以是指向派生類實(shí)例的基類指針,所以,編譯器不能根據(jù)聲明的指針類型計(jì)算偏移,而必須找到另一種間接的方法,從派生類指針計(jì)算虛基類的位置。
在VC++中,對(duì)每個(gè)繼承自虛基類的類實(shí)例,將增加一個(gè)隱藏的“虛基類表指針”(vbptr)成員變量,從而達(dá)到間接計(jì)算虛基類位置的目的。該變量指向一個(gè)全類共享的偏移量表,表中項(xiàng)目記錄了對(duì)于該類而言,“虛基類表指針”與虛基類之間的偏移量。
其它的實(shí)現(xiàn)方式中,有一種是在派生類中使用指針成員變量。這些指針成員變量指向派生類的虛基類,每個(gè)虛基類一個(gè)指針。這種方式的優(yōu)點(diǎn)是:獲取虛基類地址時(shí),所用代碼比較少。然而,編譯器優(yōu)化代碼時(shí)通常都可以采取措施避免重復(fù)計(jì)算虛基類地址。況且,這種實(shí)現(xiàn)方式還有一個(gè)大弊端:從多個(gè)虛基類派生時(shí),類實(shí)例將占用更多的內(nèi)存空間;獲取虛基類的虛基類的地址時(shí),需要多次使用指針,從而效率較低等等。
在VC++中,G擁有一個(gè)隱藏的“虛基類表指針”成員,指向一個(gè)虛基類表,該表的第二項(xiàng)是GdGvbptrC。(在G中,虛基類對(duì)象C的地址與G的“虛基類表指針”之間的偏移量(當(dāng)對(duì)于所有的派生類來(lái)說(shuō)偏移量不變時(shí),省略“d”前的前綴))。比如,在32位平臺(tái)上,GdGvptrC是8個(gè)字節(jié)。同樣,在I實(shí)例中的G對(duì)象實(shí)例也有“虛基類表指針”,不過(guò)該指針指向一個(gè)適用于“G處于I之中”的虛基類表,表中一項(xiàng)為IdGvbptrC,值為20。
觀察前面的G、H和I,我們可以得到如下關(guān)于VC++虛繼承下內(nèi)存布局的結(jié)論:
? 首先排列非虛繼承的基類實(shí)例;
? 有虛基類時(shí),為每個(gè)基類增加一個(gè)隱藏的vbptr,除非已經(jīng)從非虛繼承的類那里繼承了一個(gè)vbptr;
? 排列派生類的新數(shù)據(jù)成員;
? 在實(shí)例最后,排列每個(gè)虛基類的一個(gè)實(shí)例。
該布局安排使得虛基類的位置隨著派生類的不同而“浮動(dòng)不定”,但是,非虛基類因此也就湊在一起,彼此的偏移量固定不變。
3 成員變量
介紹了類布局之后,我們接著考慮對(duì)不同的繼承方式,訪問(wèn)成員變量的開(kāi)銷究竟如何。
沒(méi)有繼承。沒(méi)有任何繼承關(guān)系時(shí),訪問(wèn)成員變量和C語(yǔ)言的情況完全一樣:從指向?qū)ο蟮闹羔槪紤]一定的偏移量即可。
C* pc;
pc->c1; // *(pc + dCc1);
譯者注:pc是指向C的指針。
? 訪問(wèn)C的成員變量c1,只需要在pc上加上固定的偏移量dCc1(在C中,C指針地址與其c1成員變量之間的偏移量值),再獲取該指針的內(nèi)容即可。
單繼承。由于派生類實(shí)例與其基類實(shí)例之間的偏移量是常數(shù)0,所以,可以直接利用基類指針和基類成員之間的偏移量關(guān)系,如此計(jì)算得以簡(jiǎn)化。
D* pd;
pd->c1; // *(pd + dDC + dCc1); // *(pd + dDc1);
pd->d1; // *(pd + dDd1);
譯者注:D從C單繼承,pd為指向D的指針。
? 當(dāng)訪問(wèn)基類成員c1時(shí),計(jì)算步驟本來(lái)應(yīng)該為“pd+dDC+dCc1”,即為先計(jì)算D對(duì)象和C對(duì)象之間的偏移,再在此基礎(chǔ)上加上C對(duì)象指針與成員變量c1之間的偏移量。然而,由于dDC恒定為0,所以直接計(jì)算C對(duì)象地址與c1之間的偏移就可以了。
? 當(dāng)訪問(wèn)派生類成員d1時(shí),直接計(jì)算偏移量。
多重繼承。雖然派生類與某個(gè)基類之間的偏移量可能不為0,然而,該偏移量總是一個(gè)常數(shù)。只要是個(gè)常數(shù),訪問(wèn)成員變量,計(jì)算成員變量偏移時(shí)的計(jì)算就可以被簡(jiǎn)化。可見(jiàn)即使對(duì)于多重繼承來(lái)說(shuō),訪問(wèn)成員變量開(kāi)銷仍然不大。
F* pf;
pf->c1; // *(pf + dFC + dCc1); // *(pf + dFc1);
pf->e1; // *(pf + dFE + dEe1); // *(pf + dFe1);
pf->f1; // *(pf + dFf1);
譯者注:F繼承自C和E,pf是指向F對(duì)象的指針。
? 訪問(wèn)C類成員c1時(shí),F(xiàn)對(duì)象與內(nèi)嵌C對(duì)象的相對(duì)偏移為0,可以直接計(jì)算F和c1的偏移;
? 訪問(wèn)E類成員e1時(shí),F(xiàn)對(duì)象與內(nèi)嵌E對(duì)象的相對(duì)偏移是一個(gè)常數(shù),F(xiàn)和e1之間的偏移計(jì)算也可以被簡(jiǎn)化;
? 訪問(wèn)F自己的成員f1時(shí),直接計(jì)算偏移量。
虛繼承。當(dāng)類有虛基類時(shí),訪問(wèn)非虛基類的成員仍然是計(jì)算固定偏移量的問(wèn)題。然而,訪問(wèn)虛基類的成員變量,開(kāi)銷就增大了,因?yàn)楸仨毥?jīng)過(guò)如下步驟才能獲得成員變量的地址:獲取“虛基類表指針”;獲取虛基類表中某一表項(xiàng)的內(nèi)容;把內(nèi)容中指出的偏移量加到“虛基類表指針”的地址上。然而,事情并非永遠(yuǎn)如此。正如下面訪問(wèn)I對(duì)象的c1成員那樣,如果不是通過(guò)指針訪問(wèn),而是直接通過(guò)對(duì)象實(shí)例,則派生類的布局可以在編譯期間靜態(tài)獲得,偏移量也可以在編譯時(shí)計(jì)算,因此也就不必要根據(jù)虛基類表的表項(xiàng)來(lái)間接計(jì)算了。
I* pi;
pi->c1; // *(pi + dIGvbptr + (*(pi+dIGvbptr))[1] + dCc1);
pi->g1; // *(pi + dIG + dGg1); // *(pi + dIg1);
pi->h1; // *(pi + dIH + dHh1); // *(pi + dIh1);
pi->i1; // *(pi + dIi1);
I i;
i.c1; // *(&i + IdIC + dCc1); // *(&i + IdIc1);
譯者注:I繼承自G和H,G和H的虛基類是C,pi是指向I對(duì)象的指針。
? 訪問(wèn)虛基類C的成員c1時(shí),dIGvbptr是“在I中,I對(duì)象指針與G的“虛基類表指針”之間的偏移”,*(pi + dIGvbptr)是虛基類表的開(kāi)始地址,*(pi + dIGvbptr)[1]是虛基類表的第二項(xiàng)的內(nèi)容(在I對(duì)象中,G對(duì)象的“虛基類表指針”與虛基類之間的偏移),dCc1是C對(duì)象指針與成員變量c1之間的偏移;
? 訪問(wèn)非虛基類G的成員g1時(shí),直接計(jì)算偏移量;
? 訪問(wèn)非虛基類H的成員h1時(shí),直接計(jì)算偏移量;
? 訪問(wèn)自身成員i1時(shí),直接使用偏移量;
? 當(dāng)聲明了一個(gè)對(duì)象實(shí)例,用點(diǎn)“.”操作符訪問(wèn)虛基類成員c1時(shí),由于編譯時(shí)就完全知道對(duì)象的布局情況,所以可以直接計(jì)算偏移量。
當(dāng)訪問(wèn)類繼承層次中,多層虛基類的成員變量時(shí),情況又如何呢?比如,訪問(wèn)虛基類的虛基類的成員變量時(shí)?一些實(shí)現(xiàn)方式為:保存一個(gè)指向直接虛基類的指針,然后就可以從直接虛基類找到它的虛基類,逐級(jí)上推。VC++優(yōu)化了這個(gè)過(guò)程。VC++在虛基類表中增加了一些額外的項(xiàng),這些項(xiàng)保存了從派生類到其各層虛基類的偏移量。
4 強(qiáng)制轉(zhuǎn)化
如果沒(méi)有虛基類的問(wèn)題,將一個(gè)指針強(qiáng)制轉(zhuǎn)化為另一個(gè)類型的指針代價(jià)并不高昂。如果在要求轉(zhuǎn)化的兩個(gè)指針之間有“基類-派生類”關(guān)系,編譯器只需要簡(jiǎn)單地在兩者之間加上或者減去一個(gè)偏移量即可(并且該量還往往為0)。
F* pf;
(C*)pf; // (C*)(pf ? pf + dFC : 0); // (C*)pf;
(E*)pf; // (E*)(pf ? pf + dFE : 0);
C和E是F的基類,將F的指針pf轉(zhuǎn)化為C*或E*,只需要將pf加上一個(gè)相應(yīng)的偏移量。轉(zhuǎn)化為C類型指針C*時(shí),不需要計(jì)算,因?yàn)镕和C之間的偏移量為0。轉(zhuǎn)化為E類型指針E*時(shí),必須在指針上加一個(gè)非0的偏移常量dFE。C++規(guī)范要求NULL指針在強(qiáng)制轉(zhuǎn)化后依然為NULL,因此在做強(qiáng)制轉(zhuǎn)化需要的運(yùn)算之前,VC++會(huì)檢查指針是否為NULL。當(dāng)然,這個(gè)檢查只有當(dāng)指針被顯示或者隱式轉(zhuǎn)化為相關(guān)類型指針時(shí)才進(jìn)行;當(dāng)在派生類對(duì)象中調(diào)用基類的方法,從而派生類指針被在后臺(tái)轉(zhuǎn)化為一個(gè)基類的Const “this” 指針時(shí),這個(gè)檢查就不需要進(jìn)行了,因?yàn)樵诖藭r(shí),該指針一定不為NULL。
正如你猜想的,當(dāng)繼承關(guān)系中存在虛基類時(shí),強(qiáng)制轉(zhuǎn)化的開(kāi)銷會(huì)比較大。具體說(shuō)來(lái),和訪問(wèn)虛基類成員變量的開(kāi)銷相當(dāng)。
I* pi;
(G*)pi; // (G*)pi;
(H*)pi; // (H*)(pi ? pi + dIH : 0);
(C*)pi; // (C*)(pi ? (pi+dIGvbptr + (*(pi+dIGvbptr))[1]) : 0);
譯者注:pi是指向I對(duì)象的指針,G,H是I的基類,C是G,H的虛基類。
? 強(qiáng)制轉(zhuǎn)化pi為G*時(shí),由于G*和I*的地址相同,不需要計(jì)算;
? 強(qiáng)制轉(zhuǎn)化pi為H*時(shí),只需要考慮一個(gè)常量偏移;
? 強(qiáng)制轉(zhuǎn)化pi為C*時(shí),所作的計(jì)算和訪問(wèn)虛基類成員變量的開(kāi)銷相同,首先得到G的虛基類表指針,再?gòu)奶摶惐淼牡诙?xiàng)中取出G到虛基類C的偏移量,最后根據(jù)pi、虛基類表偏移和虛基類C與虛基類表指針之間的偏移計(jì)算出C*。
一般說(shuō)來(lái),當(dāng)從派生類中訪問(wèn)虛基類成員時(shí),應(yīng)該先強(qiáng)制轉(zhuǎn)化派生類指針為虛基類指針,然后一直使用虛基類指針來(lái)訪問(wèn)虛基類成員變量。這樣做,可以避免每次都要計(jì)算虛基類地址的開(kāi)銷。見(jiàn)下例。
/* before: */ ... pi->c1 ... pi->c1 ...
/* faster: */ C* pc = pi; ... pc->c1 ... pc->c1 ...
譯者注:前者一直使用派生類指針pi,故每次訪問(wèn)c1都有計(jì)算虛基類地址的較大開(kāi)銷;后者先將pi轉(zhuǎn)化為虛基類指針pc,故后續(xù)調(diào)用可以省去計(jì)算虛基類地址的開(kāi)銷。
5 成員函數(shù)
一個(gè)C++成員函數(shù)只是類范圍內(nèi)的又一個(gè)成員。X類每一個(gè)非靜態(tài)的成員函數(shù)都會(huì)接受一個(gè)特殊的隱藏參數(shù)——this指針,類型為X* const。該指針在后臺(tái)初始化為指向成員函數(shù)工作于其上的對(duì)象。同樣,在成員函數(shù)體內(nèi),成員變量的訪問(wèn)是通過(guò)在后臺(tái)計(jì)算與this指針的偏移來(lái)進(jìn)行。

struct P {
int p1;
void pf(); // new
virtual void pvf(); // new
};
P有一個(gè)非虛成員函數(shù)pf(),以及一個(gè)虛成員函數(shù)pvf()。很明顯,虛成員函數(shù)造成對(duì)象實(shí)例占用更多內(nèi)存空間,因?yàn)樘摮蓡T函數(shù)需要虛函數(shù)表指針。這一點(diǎn)以后還會(huì)談到。這里要特別指出的是,聲明非虛成員函數(shù)不會(huì)造成任何對(duì)象實(shí)例的內(nèi)存開(kāi)銷。現(xiàn)在,考慮P::pf()的定義。
void P::pf() { // void P::pf([P *const this])
++p1; // ++(this->p1);
}
這里P:pf()接受了一個(gè)隱藏的this指針參數(shù),對(duì)于每個(gè)成員函數(shù)調(diào)用,編譯器都會(huì)自動(dòng)加上這個(gè)參數(shù)。同時(shí),注意成員變量訪問(wèn)也許比看起來(lái)要代價(jià)高昂一些,因?yàn)槌蓡T變量訪問(wèn)通過(guò)this指針進(jìn)行,在有的繼承層次下,this指針需要調(diào)整,所以訪問(wèn)的開(kāi)銷可能會(huì)比較大。然而,從另一方面來(lái)說(shuō),編譯器通常會(huì)把this指針緩存到寄存器中,所以,成員變量訪問(wèn)的代價(jià)不會(huì)比訪問(wèn)局部變量的效率更差。
譯者注:訪問(wèn)局部變量,需要到SP寄存器中得到棧指針,再加上局部變量與棧頂?shù)钠啤T跊](méi)有虛基類的情況下,如果編譯器把this指針緩存到了寄存器中,訪問(wèn)成員變量的過(guò)程將與訪問(wèn)局部變量的開(kāi)銷相似。
5.1 覆蓋成員函數(shù)
和成員變量一樣,成員函數(shù)也會(huì)被繼承。與成員變量不同的是,通過(guò)在派生類中重新定義基類函數(shù),一個(gè)派生類可以覆蓋,或者說(shuō)替換掉基類的函數(shù)定義。覆蓋是靜態(tài)(根據(jù)成員函數(shù)的靜態(tài)類型在編譯時(shí)決定)還是動(dòng)態(tài)(通過(guò)對(duì)象指針在運(yùn)行時(shí)動(dòng)態(tài)決定),依賴于成員函數(shù)是否被聲明為“虛函數(shù)”。
Q從P繼承了成員變量和成員函數(shù)。Q聲明了pf(),覆蓋了P::pf()。Q還聲明了pvf(),覆蓋了P::pvf()虛函數(shù)。Q還聲明了新的非虛成員函數(shù)qf(),以及新的虛成員函數(shù)qvf()。

struct Q : P {
int q1;
void pf(); // overrides P::pf
void qf(); // new
void pvf(); // overrides P::pvf
virtual void qvf(); // new
};
對(duì)于非虛的成員函數(shù)來(lái)說(shuō),調(diào)用哪個(gè)成員函數(shù)是在編譯時(shí),根據(jù)“->”操作符左邊指針表達(dá)式的類型靜態(tài)決定的。特別地,即使ppq指向Q的實(shí)例,ppq->pf()仍然調(diào)用的是P::pf(),因?yàn)閜pq被聲明為“P*”。(注意,“->”操作符左邊的指針類型決定隱藏的this參數(shù)的類型。)
P p; P* pp = &p; Q q; P* ppq = &q; Q* pq = &q;
pp->pf(); // pp->P::pf(); // P::pf(pp);
ppq->pf(); // ppq->P::pf(); // P::pf(ppq);
pq->pf(); // pq->Q::pf(); // Q::pf((P*)pq); (錯(cuò)誤!)
pq->qf(); // pq->Q::qf(); // Q::qf(pq);
譯者注:標(biāo)記“錯(cuò)誤”處,P*似應(yīng)為Q*。因?yàn)閜f非虛函數(shù),而pq的類型為Q*,故應(yīng)該調(diào)用到Q的pf函數(shù)上,從而該函數(shù)應(yīng)該要求一個(gè)Q* const類型的this指針。
對(duì)于虛函數(shù)調(diào)用來(lái)說(shuō),調(diào)用哪個(gè)成員函數(shù)在運(yùn)行時(shí)決定。不管“->”操作符左邊的指針表達(dá)式的類型如何,調(diào)用的虛函數(shù)都是由指針實(shí)際指向的實(shí)例類型所決定。比如,盡管ppq的類型是P*,當(dāng)ppq指向Q的實(shí)例時(shí),調(diào)用的仍然是Q::pvf()。
pp->pvf(); // pp->P::pvf(); // P::pvf(pp);
ppq->pvf(); // ppq->Q::pvf(); // Q::pvf((Q*)ppq);
pq->pvf(); // pq->Q::pvf(); // Q::pvf((P*)pq); (錯(cuò)誤!)
譯者注:標(biāo)記“錯(cuò)誤”處,P*似應(yīng)為Q*。因?yàn)閜vf是虛函數(shù),pq本來(lái)就是Q*,又指向Q的實(shí)例,從哪個(gè)方面來(lái)看都不應(yīng)該是P*。
為了實(shí)現(xiàn)這種機(jī)制,引入了隱藏的vfptr成員變量。一個(gè)vfptr被加入到類中(如果類中沒(méi)有的話),該vfptr指向類的虛函數(shù)表(vftable)。類中每個(gè)虛函數(shù)在該類的虛函數(shù)表中都占據(jù)一項(xiàng)。每項(xiàng)保存一個(gè)對(duì)于該類適用的虛函數(shù)的地址。因此,調(diào)用虛函數(shù)的過(guò)程如下:取得實(shí)例的vfptr;通過(guò)vfptr得到虛函數(shù)表的一項(xiàng);通過(guò)虛函數(shù)表該項(xiàng)的函數(shù)地址間接調(diào)用虛函數(shù)。也就是說(shuō),在普通函數(shù)調(diào)用的參數(shù)傳遞、調(diào)用、返回指令開(kāi)銷外,虛函數(shù)調(diào)用還需要額外的開(kāi)銷。
回頭再看看P和Q的內(nèi)存布局,可以發(fā)現(xiàn),VC++編譯器把隱藏的vfptr成員變量放在P和Q實(shí)例的開(kāi)始處。這就使虛函數(shù)的調(diào)用能夠盡量快一些。實(shí)際上,VC++的實(shí)現(xiàn)方式是,保證任何有虛函數(shù)的類的第一項(xiàng)永遠(yuǎn)是vfptr。這就可能要求在實(shí)例布局時(shí),在基類前插入新的vfptr,或者要求在多重繼承時(shí),雖然在右邊,然而有vfptr的基類放到左邊沒(méi)有vfptr的基類的前面。
許多C++的實(shí)現(xiàn)會(huì)共享或者重用從基類繼承來(lái)的vfptr。比如,Q并不會(huì)有一個(gè)額外的vfptr,指向一個(gè)專門存放新的虛函數(shù)qvf()的虛函數(shù)表。Qvf項(xiàng)只是簡(jiǎn)單地追加到P的虛函數(shù)表的末尾。如此一來(lái),單繼承的代價(jià)就不算高昂。一旦一個(gè)實(shí)例有vfptr了,它就不需要更多的vfptr。新的派生類可以引入更多的虛函數(shù),這些新的虛函數(shù)只是簡(jiǎn)單地在已存在的,“每類一個(gè)”的虛函數(shù)表的末尾追加新項(xiàng)。
5.2 多重繼承下的虛函數(shù)
如果從多個(gè)有虛函數(shù)的基類繼承,一個(gè)實(shí)例就有可能包含多個(gè)vfptr。考慮如下的R和S類:

struct R {
int r1;
virtual void pvf(); // new
virtual void rvf(); // new
};

struct S : P, R {
int s1;
void pvf(); // overrides P::pvf and R::pvf
void rvf(); // overrides R::rvf
void svf(); // new
};
這里R是另一個(gè)包含虛函數(shù)的類。因?yàn)镾從P和R多重繼承,S的實(shí)例內(nèi)嵌P和R的實(shí)例,以及S自身的數(shù)據(jù)成員S::s1。注意,在多重繼承下,靠右的基類R,其實(shí)例的地址和P與S不同。S::pvf覆蓋了P::pvf()和R::pvf(),S::rvf()覆蓋了R::rvf()。
S s; S* ps = &s;
((P*)ps)->pvf(); // (*(P*)ps)->P::vfptr[0])((S*)(P*)ps)
((R*)ps)->pvf(); // (*(R*)ps)->R::vfptr[0])((S*)(R*)ps)
ps->pvf(); // one of the above; calls S::pvf()
譯者注:
? 調(diào)用((P*)ps)->pvf()時(shí),先到P的虛函數(shù)表中取出第一項(xiàng),然后把ps轉(zhuǎn)化為S*作為this指針傳遞進(jìn)去;
? 調(diào)用((R*)ps)->pvf()時(shí),先到R的虛函數(shù)表中取出第一項(xiàng),然后把ps轉(zhuǎn)化為S*作為this指針傳遞進(jìn)去;
因?yàn)镾::pvf()覆蓋了P::pvf()和R::pvf(),在S的虛函數(shù)表中,相應(yīng)的項(xiàng)也應(yīng)該被覆蓋。然而,我們很快注意到,不光可以用P*,還可以用R*來(lái)調(diào)用pvf()。問(wèn)題出現(xiàn)了:R的地址與P和S的地址不同。表達(dá)式(R*)ps與表達(dá)式(P*)ps指向類布局中不同的位置。因?yàn)楹瘮?shù)S::pvf希望獲得一個(gè)S*作為隱藏的this指針參數(shù),虛函數(shù)必須把R*轉(zhuǎn)化為S*。因此,在S對(duì)R虛函數(shù)表的拷貝中,pvf函數(shù)對(duì)應(yīng)的項(xiàng),指向的是一個(gè)“調(diào)整塊”的地址,該調(diào)整塊使用必要的計(jì)算,把R*轉(zhuǎn)換為需要的S*。
譯者注:這就是“thunk1: this-= sdPR; goto S::pvf”干的事。先根據(jù)P和R在S中的偏移,調(diào)整this為P*,也就是S*,然后跳轉(zhuǎn)到相應(yīng)的虛函數(shù)處執(zhí)行。
在微軟VC++實(shí)現(xiàn)中,對(duì)于有虛函數(shù)的多重繼承,只有當(dāng)派生類虛函數(shù)覆蓋了多個(gè)基類的虛函數(shù)時(shí),才使用調(diào)整塊。
5.3 地址點(diǎn)與“邏輯this調(diào)整”
考慮下一個(gè)虛函數(shù)S::rvf(),該函數(shù)覆蓋了R::rvf()。我們都知道S::rvf()必須有一個(gè)隱藏的S*類型的this參數(shù)。但是,因?yàn)橐部梢杂肦*來(lái)調(diào)用rvf(),也就是說(shuō),R的rvf虛函數(shù)槽可能以如下方式被用到:
((R*)ps)->rvf(); // (*((R*)ps)->R::vfptr[1])((R*)ps)
所以,大多數(shù)實(shí)現(xiàn)用另一個(gè)調(diào)整塊將傳遞給rvf的R*轉(zhuǎn)換為S*。還有一些實(shí)現(xiàn)在S的虛函數(shù)表末尾添加一個(gè)特別的虛函數(shù)項(xiàng),該虛函數(shù)項(xiàng)提供方法,從而可以直接調(diào)用ps->rvf(),而不用先轉(zhuǎn)換R*。MSC++的實(shí)現(xiàn)不是這樣,MSC++有意將S::rvf編譯為接受一個(gè)指向S中嵌套的R實(shí)例,而非指向S實(shí)例的指針(我們稱這種行為是“給派生類的指針類型與該虛函數(shù)第一次被引入時(shí)接受的指針類型相同”)。所有這些在后臺(tái)透明發(fā)生,對(duì)成員變量的存取,成員函數(shù)的this指針,都進(jìn)行“邏輯this調(diào)整”。
當(dāng)然,在debugger中,必須對(duì)這種this調(diào)整進(jìn)行補(bǔ)償。
ps->rvf(); // ((R*)ps)->rvf(); // S::rvf((R*)ps)
譯者注:調(diào)用rvf虛函數(shù)時(shí),直接給入R*作為this指針。
所以,當(dāng)覆蓋非最左邊的基類的虛函數(shù)時(shí),MSC++一般不創(chuàng)建調(diào)整塊,也不增加額外的虛函數(shù)項(xiàng)。
5.4 調(diào)整塊
正如已經(jīng)描述的,有時(shí)需要調(diào)整塊來(lái)調(diào)整this指針的值(this指針通常位于棧上返回地址之下,或者在寄存器中),在this指針上加或減去一個(gè)常量偏移,再調(diào)用虛函數(shù)。某些實(shí)現(xiàn)(尤其是基于cfront的)并不使用調(diào)整塊機(jī)制。它們?cè)诿總€(gè)虛函數(shù)表項(xiàng)中增加額外的偏移數(shù)據(jù)。每當(dāng)虛函數(shù)被調(diào)用時(shí),該偏移數(shù)據(jù)(通常為0),被加到對(duì)象的地址上,然后對(duì)象的地址再作為this指針傳入。
ps->rvf();
// struct { void (*pfn)(void*); size_t disp; };
// (*ps->vfptr[i].pfn)(ps + ps->vfptr[i].disp);
譯者注:當(dāng)調(diào)用rvf虛函數(shù)時(shí),前一句表示虛函數(shù)表每一項(xiàng)是一個(gè)結(jié)構(gòu),結(jié)構(gòu)中包含偏移量;后一句表示調(diào)用第i個(gè)虛函數(shù)時(shí),this指針使用保存在虛函數(shù)表中第i項(xiàng)的偏移量來(lái)進(jìn)行調(diào)整。
這種方法的缺點(diǎn)是虛函數(shù)表增大了,虛函數(shù)的調(diào)用也更加復(fù)雜。
現(xiàn)代基于PC的實(shí)現(xiàn)一般采用“調(diào)整—跳轉(zhuǎn)”技術(shù):
S::pvf-adjust: // MSC++
this -= SdPR;
goto S::pvf()
當(dāng)然,下面的代碼序列更好(然而,當(dāng)前沒(méi)有任何實(shí)現(xiàn)采用該方法):
S::pvf-adjust:
this -= SdPR; // fall into S::pvf()
S::pvf() { ... }
譯者注:IBM的C++編譯器使用該方法。
5.5 虛繼承下的虛函數(shù)
T虛繼承P,覆蓋P的虛成員函數(shù),聲明了新的虛函數(shù)。如果采用在基類虛函數(shù)表末尾添加新項(xiàng)的方式,則訪問(wèn)虛函數(shù)總要求訪問(wèn)虛基類。在VC++中,為了避免獲取虛函數(shù)表時(shí),轉(zhuǎn)換到虛基類P的高昂代價(jià),T中的新虛函數(shù)通過(guò)一個(gè)新的虛函數(shù)表獲取,從而帶來(lái)了一個(gè)新的虛函數(shù)表指針。該指針?lè)旁赥實(shí)例的頂端。
struct T : virtual P {
int t1;
void pvf(); // overrides P::pvf
virtual void tvf(); // new
};
void T::pvf() {
++p1; // ((P*)this)->p1++; // vbtable lookup!
++t1; // this->t1++;
}
如上所示,即使是在虛函數(shù)中,訪問(wèn)虛基類的成員變量也要通過(guò)獲取虛基類表的偏移,實(shí)行計(jì)算來(lái)進(jìn)行。這樣做之所以必要,是因?yàn)樘摵瘮?shù)可能被進(jìn)一步繼承的類所覆蓋,而進(jìn)一步繼承的類的布局中,虛基類的位置變化了。下面就是這樣的一個(gè)類:
struct U : T {
int u1;
};
在此U增加了一個(gè)成員變量,從而改變了P的偏移。因?yàn)閂C++實(shí)現(xiàn)中,T::pvf()接受的是嵌套在T中的P的指針,所以,需要提供一個(gè)調(diào)整塊,把this指針調(diào)整到T::t1之后(該處即是P在T中的位置)。
5.6 特殊成員函數(shù)
本節(jié)討論編譯器合成到特殊成員函數(shù)中的隱藏代碼。
5.6.1 構(gòu)造函數(shù)和析構(gòu)函數(shù)
正如我們所見(jiàn),在構(gòu)造和析構(gòu)過(guò)程中,有時(shí)需要初始化一些隱藏的成員變量。最壞的情況下,一個(gè)構(gòu)造函數(shù)要執(zhí)行如下操作:
? * 如果是“最終派生類”,初始化vbptr成員變量,調(diào)用虛基類的構(gòu)造函數(shù);
? * 調(diào)用非虛基類的構(gòu)造函數(shù)
? * 調(diào)用成員變量的構(gòu)造函數(shù)
? * 初始化虛函數(shù)表成員變量
? * 執(zhí)行構(gòu)造函數(shù)體中,程序所定義的其他初始化代碼
(注意:一個(gè)“最終派生類”的實(shí)例,一定不是嵌套在其他派生類實(shí)例中的基類實(shí)例)
所以,如果你有一個(gè)包含虛函數(shù)的很深的繼承層次,即使該繼承層次由單繼承構(gòu)成,對(duì)象的構(gòu)造可能也需要很多針對(duì)虛函數(shù)表的初始化。
反之,析構(gòu)函數(shù)必須按照與構(gòu)造時(shí)嚴(yán)格相反的順序來(lái)“肢解”一個(gè)對(duì)象。
? * 合成并初始化虛函數(shù)表成員變量
? * 執(zhí)行析構(gòu)函數(shù)體中,程序定義的其他析構(gòu)代碼
? * 調(diào)用成員變量的析構(gòu)函數(shù)(按照相反的順序)
? * 調(diào)用直接非虛基類的析構(gòu)函數(shù)(按照相反的順序)
? * 如果是“最終派生類”,調(diào)用虛基類的析構(gòu)函數(shù)(按照相反順序)
在VC++中,有虛基類的類的構(gòu)造函數(shù)接受一個(gè)隱藏的“最終派生類標(biāo)志”,標(biāo)示虛基類是否需要初始化。對(duì)于析構(gòu)函數(shù),VC++采用“分層析構(gòu)模型”,代碼中加入一個(gè)隱藏的析構(gòu)函數(shù),該函數(shù)被用于析構(gòu)包含虛基類的類(對(duì)于“最終派生類”實(shí)例而言);代碼中再加入另一個(gè)析構(gòu)函數(shù),析構(gòu)不包含虛基類的類。前一個(gè)析構(gòu)函數(shù)調(diào)用后一個(gè)。
5.6.2 虛析構(gòu)函數(shù)與delete操作符
考慮結(jié)構(gòu)V和W。

struct V {
virtual ~V();
};

struct W : V {
operator delete();
};
析構(gòu)函數(shù)可以為虛。一個(gè)類如果有虛析構(gòu)函數(shù)的話,將會(huì)象有其他虛函數(shù)一樣,擁有一個(gè)虛函數(shù)表指針,虛函數(shù)表中包含一項(xiàng),其內(nèi)容為指向?qū)υ擃愡m用的虛析構(gòu)函數(shù)的地址。這些機(jī)制和普通虛函數(shù)相同。虛析構(gòu)函數(shù)的特別之處在于:當(dāng)類實(shí)例被銷毀時(shí),虛析構(gòu)函數(shù)被隱含地調(diào)用。調(diào)用地(delete發(fā)生的地方)雖然不知道銷毀的動(dòng)態(tài)類型,然而,要保證調(diào)用對(duì)該類型合適的delete操作符。例如,當(dāng)pv指向W的實(shí)例時(shí),當(dāng)W::~W被調(diào)用之后,W實(shí)例將由W類的delete操作符來(lái)銷毀。
V* pv = new V;
delete pv; // pv->~V::V(); // use ::operator delete()
pv = new W;
delete pv; // pv->~W::W(); // use W::operator delete()
pv = new W;
::delete pv; // pv->~W::W(); // use ::operator delete()
譯者注:
? V沒(méi)有定義delete操作符,delete時(shí)使用函數(shù)庫(kù)的delete操作符;
? W定義了delete操作符,delete時(shí)使用自己的delete操作符;
? 可以用全局范圍標(biāo)示符顯示地調(diào)用函數(shù)庫(kù)的delete操作符。
為了實(shí)現(xiàn)上述語(yǔ)意,VC++擴(kuò)展了其“分層析構(gòu)模型”,從而自動(dòng)創(chuàng)建另一個(gè)隱藏的析構(gòu)幫助函數(shù)——“deleting析構(gòu)函數(shù)”,然后,用該函數(shù)的地址來(lái)替換虛函數(shù)表中“實(shí)際”虛析構(gòu)函數(shù)的地址。析構(gòu)幫助函數(shù)調(diào)用對(duì)該類合適的析構(gòu)函數(shù),然后為該類有選擇性地調(diào)用合適的delete操作符。
6 數(shù)組
堆上分配空間的數(shù)組使虛析構(gòu)函數(shù)進(jìn)一步復(fù)雜化。問(wèn)題變復(fù)雜的原因有兩個(gè):
1、 堆上分配空間的數(shù)組,由于數(shù)組可大可小,所以,數(shù)組大小值應(yīng)該和數(shù)組一起保存。因此,堆上分配空間的數(shù)組會(huì)分配額外的空間來(lái)存儲(chǔ)數(shù)組元素的個(gè)數(shù);
2、 當(dāng)數(shù)組被刪除時(shí),數(shù)組中每個(gè)元素都要被正確地釋放,即使當(dāng)數(shù)組大小不確定時(shí)也必須成功完成該操作。然而,派生類可能比基類占用更多的內(nèi)存空間,從而使正確釋放比較困難。
struct WW : W { int w1; };
pv = new W[m];
delete [] pv; // delete m W's (sizeof(W) == sizeof(V))
pv = new WW[n];
delete [] pv; // delete n WW's (sizeof(WW) > sizeof(V))
譯者注:WW從W繼承,增加了一個(gè)成員變量,因此,WW占用的內(nèi)存空間比W大。然而,不管指針pv指向W的數(shù)組還是WW的數(shù)組,delete[]都必須正確地釋放WW或W對(duì)象占用的內(nèi)存空間。
雖然從嚴(yán)格意義上來(lái)說(shuō),數(shù)組delete的多態(tài)行為C++標(biāo)準(zhǔn)并未定義,然而,微軟有一些客戶要求實(shí)現(xiàn)該行為。因此,在MSC++中,該行為是用另一個(gè)編譯器生成的虛析構(gòu)幫助函數(shù)來(lái)完成。該函數(shù)被稱為“向量delete析構(gòu)函數(shù)”(因其針對(duì)特定的類定制,比如WW,所以,它能夠遍歷數(shù)組的每個(gè)元素,調(diào)用對(duì)每個(gè)元素適用的析構(gòu)函數(shù))。
7 異常處理
簡(jiǎn)單說(shuō)來(lái),異常處理是C++標(biāo)準(zhǔn)委員會(huì)工作文件提供的一種機(jī)制,通過(guò)該機(jī)制,一個(gè)函數(shù)可以通知其調(diào)用者“異常”情況的發(fā)生,調(diào)用者則能據(jù)此選擇合適的代碼來(lái)處理異常。該機(jī)制在傳統(tǒng)的“函數(shù)調(diào)用返回,檢查錯(cuò)誤狀態(tài)代碼”方法之外,給程序提供了另一種處理錯(cuò)誤的手段。
因?yàn)镃++是面向?qū)ο蟮恼Z(yǔ)言,很自然地,C++中用對(duì)象來(lái)表達(dá)異常狀態(tài)。并且,使用何種異常處理也是基于“拋出的”異常對(duì)象的靜態(tài)或動(dòng)態(tài)類型來(lái)決定的。不光如此,既然C++總是保證超出范圍的對(duì)象能夠被正確地銷毀,異常實(shí)現(xiàn)也必須保證當(dāng)控制從異常拋出點(diǎn)轉(zhuǎn)換到異常“捕獲”點(diǎn)時(shí)(棧展開(kāi)),超出范圍的對(duì)象能夠被自動(dòng)、正確地銷毀。
考慮如下例子:
struct X { X(); }; // exception object class
struct Z { Z(); ~Z(); }; // class with a destructor
extern void recover(const X&);
void f(int), g(int);
int main() {
try {
f(0);
} catch (const X& rx) {
recover(rx);
}
return 0;
}
void f(int i) {
Z z1;
g(i);
Z z2;
g(i-1);
}
void g(int j) {
if (j < 0)
throw X();
}
譯者注:X是異常類,Z是帶析構(gòu)函數(shù)的工作類,recover是錯(cuò)誤處理函數(shù),f和g一起產(chǎn)生異常條件,g實(shí)際拋出異常。
這段程序會(huì)拋出異常。在main中,加入了處理異常的try & catch框架,當(dāng)調(diào)用f(0)時(shí),f構(gòu)造z1,調(diào)用g(0)后,再構(gòu)造z2,再調(diào)用g(-1),此時(shí)g發(fā)現(xiàn)參數(shù)為負(fù),拋出X異常對(duì)象。我們希望在某個(gè)調(diào)用層次上,該異常能夠得到處理。既然g和f都沒(méi)有建立處理異常的框架,我們就只能希望main函數(shù)建立的異常處理框架能夠處理X異常對(duì)象。實(shí)際上,確實(shí)如此。當(dāng)控制被轉(zhuǎn)移到main中異常捕獲點(diǎn)時(shí),從g中的異常拋出點(diǎn)到main中的異常捕獲點(diǎn)之間,該范圍內(nèi)的對(duì)象都必須被銷毀。在本例中,z2和z1應(yīng)該被銷毀。
談到異常處理的具體實(shí)現(xiàn)方式,一般情況下,在拋出點(diǎn)和捕獲點(diǎn)都使用“表”來(lái)表述能夠捕獲異常對(duì)象的類型;并且,實(shí)現(xiàn)要保證能夠在特定的捕獲點(diǎn)真正捕獲特定的異常對(duì)象;一般地,還要運(yùn)用拋出的對(duì)象來(lái)初始化捕獲語(yǔ)句的“實(shí)參”。通過(guò)合理地選擇編碼方案,可以保證這些表格不會(huì)占用過(guò)多的內(nèi)存空間。
異常處理的開(kāi)銷到底如何?讓我們?cè)倏紤]一下函數(shù)f。看起來(lái)f沒(méi)有做異常處理。f確實(shí)沒(méi)有包含try,catch,或者是throw關(guān)鍵字,因此,我們會(huì)猜異常處理應(yīng)該對(duì)f沒(méi)有什么影響。錯(cuò)!編譯器必須保證一旦z1被構(gòu)造,而后續(xù)調(diào)用的任何函數(shù)向f拋回了異常,異常又出了f的范圍時(shí),z1對(duì)象能被正確地銷毀。同樣,一旦z2被構(gòu)造,編譯器也必須保證后續(xù)拋出異常時(shí),能夠正確地銷毀z2和z1。
要實(shí)現(xiàn)這些“展開(kāi)”語(yǔ)意,編譯器必須在后臺(tái)提供一種機(jī)制,該機(jī)制在調(diào)用者函數(shù)中,針對(duì)調(diào)用的函數(shù)拋出的異常動(dòng)態(tài)決定異常環(huán)境(處理點(diǎn))。這可能包括在每個(gè)函數(shù)的準(zhǔn)備工作和善后工作中增加額外的代碼,在最糟糕的情況下,要針對(duì)每一套對(duì)象初始化的情況更新?tīng)顟B(tài)變量。例如,上述例子中,z1應(yīng)被銷毀的異常環(huán)境當(dāng)然與z2和z1都應(yīng)該被銷毀的異常環(huán)境不同,因此,不管是在構(gòu)造z1后,還是繼而在構(gòu)造z2后,VC++都要分別在狀態(tài)變量中更新(存儲(chǔ))新的值。
所有這些表,函數(shù)調(diào)用的準(zhǔn)備和善后工作,狀態(tài)變量的更新,都會(huì)使異常處理功能造成可觀的內(nèi)存空間和運(yùn)行速度開(kāi)銷。正如我們所見(jiàn),即使在沒(méi)有使用異常處理的函數(shù)中,該開(kāi)銷也會(huì)發(fā)生。
幸運(yùn)的是,一些編譯器可以提供編譯選項(xiàng),關(guān)閉異常處理機(jī)制。那些不需要異常處理機(jī)制的代碼,就可以避免這些額外的開(kāi)銷了。
8 小結(jié)
好了,現(xiàn)在你可以寫C++編譯器了(開(kāi)個(gè)玩笑)。
在本文中,我們討論了許多重要的C++運(yùn)行實(shí)現(xiàn)問(wèn)題。我們發(fā)現(xiàn),很多美妙的C++語(yǔ)言特性的開(kāi)銷很低,同時(shí),其他一些美妙的特性(譯者注:主要是和“虛”字相關(guān)的東西)將造成較大的開(kāi)銷。C++很多實(shí)現(xiàn)機(jī)制都是在后臺(tái)默默地為你工作。一般說(shuō)來(lái),單獨(dú)看一段代碼時(shí),很難衡量這段代碼造成的運(yùn)行時(shí)開(kāi)銷,必須把這段代碼放到一個(gè)更大的環(huán)境中來(lái)考察,運(yùn)行時(shí)開(kāi)銷問(wèn)題才能得到比較明確的答案。