普通的虛繼承
下面我們來(lái)看虛繼承。首先看看這C020類(lèi),它從C010虛繼承:
struct C010
{
C010() : c_(0x01) {}
void foo() { c_ = 0x02; }
char c_;
};
struct C020 : public virtual C010
{
C020() : c_(0x02) {}
char c_;
};
運行如下代碼,查看對象的內存布局:
PRINT_SIZE_DETAIL(C020)
結果為:
The size of C020 is 6
The detail of C020 is c0 c2 45 00 02 01
很明顯對象的起始處是一個(gè)指針,然后是子類(lèi)的成員變量,接下來(lái)是父類(lèi)的成員變量。和以前的討論不同的是由于使用了虛繼承,父類(lèi)的成員變量被放到了最后面。
運行如下的代碼:
C020 c020;
c020.C010::c_ = 0x04;
由于子類(lèi)中的變量和父類(lèi)中的變量重名,所以我們必須用這種方式來(lái)訪(fǎng)問(wèn)屬于父類(lèi)的成員變量,普通情況下不需要這種寫(xiě)法。我們看看后面這行代碼對應的匯編代碼:
0042387E mov eax,dword ptr [ebp+FFFFF82Ch]
00423884 mov ecx,dword ptr [eax+4]
00423887 mov byte ptr [ebp+ecx+FFFFF82Ch],4
前面說(shuō)過(guò)對象的起始是一個(gè)指針,第1行指令取到這個(gè)指針的值,第2行把這個(gè)指針指向的地址后移4字節后的值(做為一個(gè)4字節的值)取出來(lái)。執行完這句我們看看ecx寄存器,可知取出來(lái)的值為5。最后一行是真正的賦值指令,它通過(guò)在對象的起始處(即[ebp+FFFFF32Ch])加上ecx中的值做偏移值(即5)來(lái)得到賦值的目的地址。接合前面的對象布局輸出,我們可以發(fā)現從對象起始地址開(kāi)始加5字節的偏移值,剛好得到父類(lèi)的成員變量的地址。這樣我們可以大致分析出直接虛繼承的子類(lèi)的對象布局。
|子類(lèi)5 |父類(lèi)1 ?。?br>|偏移值指針4,5|子類(lèi)成員變量1|父類(lèi)成員變量1|
(注:第一個(gè)數字為所在區域的長(cháng)度(字節數),偏移值指針后的第二個(gè)數字為該指針指向的偏移值。后同。)
通過(guò)查看內存可以發(fā)現偏移值指針指向的內存前4字節為0,我不知道它的具體的用途是什么。接下來(lái)的4字節是一個(gè)32位的整數,也就是真正的偏移值。即從子類(lèi)的起始位置到被虛繼承的父類(lèi)的起始位置的偏移值,在我們前面的例子中這個(gè)值為5(一個(gè)指針加一個(gè)char成員變量)。
通過(guò)這個(gè)分析我們可以看到在虛承繼的情況下,通過(guò)子類(lèi)的對象訪(fǎng)問(wèn)父類(lèi)的普通成員變量的效率是相當低的。如果必須用到虛繼承,也應該盡量不要在父類(lèi)中放置普通成員變量(靜態(tài)成員變量不受影響)。
另外為什么微軟不把偏移值直接放到子類(lèi)中,而是采用偏移值指針。我想是因為采用指針的方式更為靈活,即使以后需要擴展也不影響類(lèi)對象的布局。
按下來(lái)我們再看看這幾行代碼:
PRINT_OBJ_ADR(c020);
C010 * pt = &c020;
PRINT_PT(pt);
pt->c_ = 0x03;
第2行聲明了一個(gè)父類(lèi)指針,并讓它指向一個(gè)子類(lèi)的對象。第3行打印出這個(gè)指針的值。運行結果為:
c020's address is : 0012F708
pt's value is : 0012F70D
我們可以看到賦值后的指針的值并不等于賦給它的對象地址值。也就是說(shuō)在這個(gè)賦值過(guò)程中編譯器進(jìn)行了額外的工作,即調整了指針的值。我們看看第2行對應的匯編代碼,看看編譯器究竟做了些什么?
01 004238EA lea eax,[ebp+FFFFF82Ch]
02 004238F0 test eax,eax
03 004238F2 jne 00423900
04 004238F4 mov dword ptr [ebp+FFFFF014h],0
05 004238FE jmp 00423916
06 00423900 mov ecx,dword ptr [ebp+FFFFF82Ch]
07 00423906 mov edx,dword ptr [ecx+4]
08 00423909 lea eax,[ebp+edx+FFFFF82Ch]
09 00423910 mov dword ptr [ebp+FFFFF014h],eax
10 00423916 mov ecx,dword ptr [ebp+FFFFF014h]
11 0042391C mov dword ptr [ebp+FFFFF820h],ecx
喔!比想象的要復雜的多。一行簡(jiǎn)單的指針賦值語(yǔ)句卻產(chǎn)生了這么多的匯編代碼。這行代碼本身的語(yǔ)義是取對象的地址賦給一個(gè)指針,對于編譯器來(lái)說(shuō)它把這做為指針到指針的賦值來(lái)處理。由于牽涉到了向上的類(lèi)型轉換,同時(shí)又有虛繼承存在。根據前面的布局分析,在虛繼承的情況下,父類(lèi)位于對象布局的后部。因此在這里要做一個(gè)指針位置的調整。由于調整要根據源指針來(lái)進(jìn)行計算,所以先要對源指針的合法性進(jìn)行檢查,以避免運行時(shí)的指針異常錯誤。前3行的匯編指令就是在做這件事,檢查源指針是否為NULL。如果為NULL則執行4、5、10、11行,最終給pt賦0。如果不為NULL跳至第6行執行到最后。重要的是第6、7、8行代碼,它們通過(guò)偏移值指針找到偏移值,并以此來(lái)調整指針的位置,讓目的指針最終指向對象中的父類(lèi)部分的數據成員。
對比一下普通的指針賦值,我們可以對上面賦值的復雜性和低效有更深的認識。
C010 * pt1 = NULL;
C010 * pt2 = pt1;
這兩行相應的匯編代碼為:
0042397D mov dword ptr [ebp+FFFFF814h],0
00423987 mov eax,dword ptr [ebp+FFFFF814h]
0042398D mov dword ptr [ebp+FFFFF808h],eax
第1行是普通的賦值,編譯器并不做任何的檢查,即使源指針為NULL。因為它不需要根據源指針(本處為NULL)做任何計算。第2個(gè)賦值也很直接,只是通過(guò)eax做了一個(gè)中轉。這里我們就可以看到前面的虛繼承下的子類(lèi)指針到父類(lèi)指針的賦值是我么的低效。在程序中應盡量的避免這種代碼。
(未完待繼)




