部落格已遷移至:http://kulv.sinaapp.com/
關於基類建構函式調用虛函數實際調用的不是衍生類別的問題的原因
我們知道,類的建構函式裡面編譯器插入了很多代碼,比如異常安全,虛函數表指標的設定,基類構造,等等。
而且,關鍵是這些代碼時在任何使用者的代碼(非初始化)的地方之前插入的,問題就來了···
如果在基類建構函式裡面調用基類的虛函數,那麼,實際調用的卻不像我們當初認為的多態效果,為什麼呢?
下面看看編譯怎麼實現的就知道了····
下面是測試類別:
class A {<br />public:<br />A() {<br />vfunc() ;<br />}<br />virtual vfunc() {<br />;<br />}<br />} ;</p><p>class CC : public A {<br />public:<br />virtual vfunc() {<br />;<br />}<br />};</p><p>int main()<br />{<br />CC c ;<br />return 0 ;<br />}<br />
原因:
004012F8 lea ecx,[ebp-4]//裝載this指標到ECX,這是thiscall的調研約定<br />004012FB call @ILT+5(CC::CC) (0040100a) //調用CC的建構函式<br />30: return 0 ;<br />00401300 xor eax,eax<br />31: }<br />///////////////////////////////////////////////////////////////////////</p><p>CC::CC:<br />····<br />00401339 pop ecx//this指標到ECX<br />0040133A mov dword ptr [ebp-4],ecx<br />0040133D mov ecx,dword ptr [ebp-4]<br />00401340 call @ILT+15(A::A) (00401014)//衍生類別建構函式進入後,完全沒幹什麼事就“馬上”調用了基類的建構函式,那麼,其vptr虛函數表指標也沒有賦值!<br />00401345 mov eax,dword ptr [ebp-4]<br />00401348 mov dword ptr [eax],offset CC::`vftable' (00431068)//直到這才對自己的虛函數表指標賦值,因此,此前的調用,絕不會涉及到衍生類別CC的虛函數··<br />0040134E mov eax,dword ptr [ebp-4] //此後如果有任何虛函數調用,訪問虛函數表時訪問的就正常的是衍生類別的虛函數表了,調用的也正確了<br />00401351 pop edi<br />00401352 pop esi<br />00401353 pop ebx<br />00401354 add esp,44h<br />00401357 cmp ebp,esp<br />00401359 call __chkesp (00401590)<br />0040135E mov esp,ebp<br />00401360 pop ebp<br />00401361 ret<br />///////////////////////////////////////////////////////////////////////</p><p>10: A() {<br />····<br />00401399 pop ecx<br />0040139A mov dword ptr [ebp-4],ecx<br />0040139D mov eax,dword ptr [ebp-4]<br />004013A0 mov dword ptr [eax],offset A::`vftable' (0043106c)//注意到,基類第一件事情是:為虛函數表指標地址賦值,<br />//此指標傳下來其實是CC衍生類別的this指標的地址,也就是:類的第一個位元組<br />11: vfunc() ;//此時調用的時候,理所當然會傳入this指標,此this的vptr此時還不是衍生類別的,而是上面一句設定的基類的虛函數表地址!<br />004013A6 mov ecx,dword ptr [ebp-4]//傳遞這個很多人都會“錯誤的以為的”this<br />004013A9 call @ILT+10(A::vfunc) (0040100f) //調用這個“虛函數”,這裡沒有訪問虛函數表是因為編譯器最佳化的原因吧,因為此時肯定為A的vfunc,<br />//感興趣可以試一下:看下面的例子<br />12: }<br />·····<br />004013C1 ret
上面沒有訪問虛函數表,是因為編譯器最佳化了一下,如果我們的A類建構函式是這樣的,那麼···
class A {<br />public:<br />A() {<br />init() ;//先調用A的非虛函數,裡面再調用vfunc虛函數就可以看出要訪問虛函數表了,不過此時的虛函數表指標指向A的虛函數表<br />}<br />virtual vir() { ;} ;//故意加了一項,待會會看到+4的字樣<br />virtual vfunc() {//應該在虛函數表的第二項<br />;<br />}<br />init() {<br />vfunc() ;<br />}<br />} ;</p><p>10: A() {<br />····<br />004013A9 pop ecx<br />004013AA mov dword ptr [ebp-4],ecx<br />004013AD mov eax,dword ptr [ebp-4]<br />004013B0 mov dword ptr [eax],offset A::`vftable' (00431074)//設定此時的虛函數表指標為基類的!!不是衍生類別的,調用完成後會被衍生類別覆蓋<br />11: init() ;<br />004013B6 mov ecx,dword ptr [ebp-4]<br />004013B9 call @ILT+40(A::init) (0040102d)//不是虛函數,直接調用,不用訪問虛函數表<br />12: }<br />····<br />004013D1 ret</p><p>///////////////////////////////////////////////////////////////////////<br />18: init() {<br />····<br />00401469 pop ecx<br />0040146A mov dword ptr [ebp-4],ecx //this指標放到棧頂<br />19: vfunc() ;<br />0040146D mov eax,dword ptr [ebp-4] //下面準備虛函數表的查詢,可以看出,虛函數調用機制還是會多用幾次定址操作的,訪問記憶體。<br />00401470 mov edx,dword ptr [eax] //this-》edx。其實其內容是A的虛函數表的指標<br />00401472 mov esi,esp<br />00401474 mov ecx,dword ptr [ebp-4]<br />00401477 call dword ptr [edx+4]//vfunc在第二項,所以+4,這裡<br />0040147A cmp esi,esp<br />0040147C call __chkesp (00401610)<br />20: }<br />····<br />00401491 ret</p><p>