在構(gòu)造函數(shù)中調(diào)用虛成員函數(shù),雖然這是個(gè)不很常用的技術(shù),但研究一下可以加深對(duì)虛函數(shù)機(jī)制及對(duì)象構(gòu)造過程的理解。這個(gè)問題也和一般直觀上的認(rèn)識(shí)有所差異。先看看下面的兩個(gè)類定義。
struct C180 {
C180() {
foo();
this->foo();
}
virtual foo() {
cout << "<< C180.foo this: " << this
<< " vtadr: " << *(void**)this
<< endl;
}
};
struct C190 : public C180 {
C190() {}
virtual foo() {
cout << "<< C190.foo this: " << this
<< " vtadr: " << *(void**)this
<< endl;
}
};
父類中有一個(gè)虛函數(shù),并且父類在它的構(gòu)造函數(shù)中調(diào)用了這個(gè)虛函數(shù),調(diào)用時(shí)它采用了兩種方法一種是直接調(diào)用,一種是通過this指針調(diào)用。同時(shí)子類又重寫了這個(gè)虛函數(shù)。
我們可以來預(yù)測一下如果構(gòu)造一個(gè)C190的對(duì)象會(huì)發(fā)生什么情況。
我們知道,在構(gòu)造一個(gè)對(duì)象時(shí),首先會(huì)按對(duì)象的大小得到一塊內(nèi)存(在heap上或在stack上),然后會(huì)把指向這塊內(nèi)存的指針做為this指針來調(diào)用類的構(gòu)造函數(shù),對(duì)這塊內(nèi)存進(jìn)行初始化。如果對(duì)象有父類就會(huì)先調(diào)用父類的構(gòu)造函數(shù)(并依次遞歸),如果有多個(gè)父類(多重繼承)會(huì)依次對(duì)父類的構(gòu)造函數(shù)進(jìn)行調(diào)用,并會(huì)適當(dāng)?shù)恼{(diào)整this指針的位置。在調(diào)用完所有的父類的構(gòu)造函數(shù)后,再執(zhí)行自己的代碼。
照上面的分析構(gòu)造C190時(shí)也會(huì)調(diào)用C180的構(gòu)造函數(shù),這時(shí)在C180構(gòu)造函數(shù)中的第一個(gè)foo調(diào)用為靜態(tài)綁定,會(huì)調(diào)用到C180::foo()函數(shù)。第二個(gè)foo調(diào)用是通過指針調(diào)用的,這時(shí)多態(tài)行為會(huì)發(fā)生,應(yīng)該調(diào)用的是C190::foo()函數(shù)。
執(zhí)行如下代碼:
C190 obj;
obj.foo();
結(jié)果為:
<< C180.foo this: 0012F7A4 vtadr: 0045C404
<< C180.foo this: 0012F7A4 vtadr: 0045C404
<< C190.foo this: 0012F7A4 vtadr: 0045C400
和我們的分析大相徑庭。前2行是構(gòu)造C190時(shí)的輸出,后1行是我們用靜態(tài)綁定方式調(diào)用的C190::foo()函數(shù)。第2行的輸出說明多態(tài)行為并沒有象預(yù)期的那樣發(fā)生。而且比較輸出的最后一列,發(fā)現(xiàn)在調(diào)用C180的構(gòu)造函數(shù)時(shí)對(duì)象對(duì)應(yīng)的虛表和構(gòu)造后對(duì)象對(duì)應(yīng)的虛表不是同一個(gè)。其實(shí)這正是奧秘的所在。
為此我查了一下C++標(biāo)準(zhǔn)規(guī)范。在12.7.3條中有明確的規(guī)定。這是一種特例,在這種情況下,即在構(gòu)造子類時(shí)調(diào)用父類的構(gòu)造函數(shù),而父類的構(gòu)造函數(shù)中又調(diào)用了虛成員函數(shù),這個(gè)虛成員函數(shù)即使被子類重寫,也不允許發(fā)生多態(tài)的行為。即,這時(shí)必須要調(diào)用父類的虛函數(shù),而不子類重寫后的虛函數(shù)。
我想這樣做的原因是因?yàn)樵谡{(diào)用父類的構(gòu)造函數(shù)時(shí),對(duì)象中屬于子類部分的成員變量是肯定還沒有初始化的,因?yàn)樽宇悩?gòu)造函數(shù)中的代碼還沒有被執(zhí)行。如果這時(shí)允許多態(tài)的行為,即通過父類的構(gòu)造函數(shù)調(diào)用到了子類的虛函數(shù),而這個(gè)虛函數(shù)要訪問屬于子類的數(shù)據(jù)成員時(shí)就有可能出錯(cuò)。
我們看看VC7.1生成的匯編代碼就可以很容易的理解這個(gè)行為了。
這是C190的構(gòu)造函數(shù):
01 00426FE0 push ebp
02 00426FE1 mov ebp,esp
03 00426FE3 sub esp,0CCh
04 00426FE9 push ebx
05 00426FEA push esi
06 00426FEB push edi
07 00426FEC push ecx
08 00426FED lea edi,[ebp+FFFFFF34h]
09 00426FF3 mov ecx,33h
10 00426FF8 mov eax,0CCCCCCCCh
11 00426FFD rep stos dword ptr [edi]
12 00426FFF pop ecx
13 00427000 mov dword ptr [ebp-8],ecx
14 00427003 mov ecx,dword ptr [ebp-8]
15 00427006 call 0041D451
16 0042700B mov eax,dword ptr [ebp-8]
17 0042700E mov dword ptr [eax],45C400h
18 00427014 mov eax,dword ptr [ebp-8]
19 00427017 pop edi
20 00427018 pop esi
21 00427019 pop ebx
22 0042701A add esp,0CCh
23 00427020 cmp ebp,esp
24 00427022 call 0041DDF2
25 00427027 mov esp,ebp
26 00427029 pop ebp
27 0042702A ret
開始部分的指令在前面幾篇中陸續(xù)解釋過,這里不再詳述。我們看看第15是對(duì)父類的構(gòu)造函數(shù)C180::C180()的調(diào)用,根據(jù)前文的說明,我們知道此時(shí)ecx中放的是this指針,也就是C190對(duì)象的地址。這時(shí)如果跳到this指針批向的地址看看會(huì)發(fā)現(xiàn)值為0xcccccccc即沒有初始化,虛表指針也沒有被初始化。那么我們跟著跳到C180的構(gòu)造函數(shù)看看。
01 00427040 push ebp
02 00427041 mov ebp,esp
03 00427043 sub esp,0CCh
04 00427049 push ebx
05 0042704A push esi
06 0042704B push edi
07 0042704C push ecx
08 0042704D lea edi,[ebp+FFFFFF34h]
09 00427053 mov ecx,33h
10 00427058 mov eax,0CCCCCCCCh
11 0042705D rep stos dword ptr [edi]
12 0042705F pop ecx
13 00427060 mov dword ptr [ebp-8],ecx
14 00427063 mov eax,dword ptr [ebp-8]
15 00427066 mov dword ptr [eax],45C404h
16 0042706C mov ecx,dword ptr [ebp-8]
17 0042706F call 0041DA8C
18 00427074 mov ecx,dword ptr [ebp-8]
19 00427077 call 0041DA8C
20 0042707C mov eax,dword ptr [ebp-8]
21 0042707F pop edi
22 00427080 pop esi
23 00427081 pop ebx
24 00427082 add esp,0CCh
25 00427088 cmp ebp,esp
26 0042708A call 0041DDF2
27 0042708F mov esp,ebp
28 00427091 pop ebp
29 00427092 ret
看看第15行,在this指針的位置也就是對(duì)象的起始處,填入了一個(gè)4字節(jié)的值0x0045C404,其實(shí)這就是我們前面的打印過的C180的虛表地址。第16、17行和18、19行分別調(diào)用了兩次foo()函數(shù),用的都是靜態(tài)綁定。這個(gè)就有點(diǎn)奇怪,因?yàn)閷?duì)后一個(gè)調(diào)用我們使用了this指針,照理應(yīng)該是動(dòng)態(tài)綁定才對(duì)。可這里卻是靜態(tài)綁定,為什么編譯器要做這個(gè)優(yōu)化?我們繼承往后看。
這個(gè)函數(shù)執(zhí)行完后,我們再回到C190構(gòu)造函數(shù)中,我們接著看C190構(gòu)造函數(shù)匯編代碼的第17行,這里又在對(duì)象的起始處重新填入了0x0045C400,覆蓋了原來的值,而這個(gè)值就是我們前面打印過的真正的C190的虛表地址。
也就是說VC7.1是通過在調(diào)用構(gòu)造函數(shù)的真正代碼前把對(duì)象的虛指針值設(shè)置為指向?qū)?yīng)類的虛表來實(shí)現(xiàn)C++規(guī)范的相應(yīng)語義。C++標(biāo)準(zhǔn)中只規(guī)定了行為,并不規(guī)定具體編譯器在實(shí)現(xiàn)這一行為時(shí)所用的方法。象我們上面看到的,即使是通過this指針調(diào)用,編譯器也把它優(yōu)化為靜態(tài)綁定,也就是說即使不做這個(gè)虛指針的調(diào)整也不會(huì)有錯(cuò)。之所以要調(diào)整我想可能是防止在被調(diào)用的虛成員中又通過this指針來調(diào)用其他的虛函數(shù),不過誰會(huì)這么變態(tài)呢?
還有值得一提的是,VC7.1中有一個(gè)擴(kuò)展屬性可以用來抑制編譯器產(chǎn)生對(duì)虛指針進(jìn)行調(diào)整的代碼。我們可以在C180類的聲明中加入這個(gè)屬性。
struct __declspec(novtable) C180 {
C180() {
foo();
this->foo();
}
virtual foo() {
cout << "<< C180.foo this: " << this
<< " vtadr: " << *(void**)this
<< endl;
}
};
這樣再執(zhí)行前面的代碼,輸出就會(huì)變成:
<< C180.foo this: 0012F7A4 vtadr: CCCCCCCC
<< C180.foo this: 0012F7A4 vtadr: CCCCCCCC
<< C190.foo this: 0012F7A4 vtadr: 0045C400
由于編譯器抑制了對(duì)虛指針的調(diào)整所以在調(diào)C180的構(gòu)造函數(shù)時(shí)虛指針的值沒有初始化,這時(shí)我們才看到多虧編譯器把第二個(gè)通過this指針對(duì)foo的調(diào)用優(yōu)化成了靜態(tài)綁定,否則由于虛指針沒有初始化一定會(huì)出現(xiàn)一個(gè)指針異常的錯(cuò)誤,這就回答我們上面的那個(gè)問題。
在這種情況下產(chǎn)生的匯編代碼我就不列了,有興趣的朋友可以自己去看一看。另外對(duì)于析構(gòu)函數(shù)的調(diào)用,也請(qǐng)有興趣的朋友自行分析一下。
另外這個(gè)屬性在ATL的代碼中大量的使用。在ATL中接口一般為純虛基類,如果不用這個(gè)優(yōu)化屬性,由于在子類即實(shí)現(xiàn)類的構(gòu)造函數(shù)中要調(diào)用父類的構(gòu)造函數(shù),而編譯器產(chǎn)生的父類構(gòu)造函數(shù)又要設(shè)置虛指針的值。所以編譯器必須要把父類的虛表構(gòu)建出來。而實(shí)際上這個(gè)虛表是沒有任何意義的,因?yàn)锳TL的純虛接口類的虛函數(shù)都是無實(shí)現(xiàn)的。這樣不僅僅是多了幾行無用的設(shè)值指令,同時(shí)也浪費(fèi)了空間。有興趣的朋友可以自行驗(yàn)證一下。