程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> C++對象布局及多態實現探索之虛繼承

C++對象布局及多態實現探索之虛繼承

編輯:關於C++

下面我們來看虛繼承。首先看看這C020類,它從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

很明顯對象的起始處是一個指針,然後是子類的成員變量,接下來是父類的成員變量。和以前的討論不同的是由於使用了虛繼承,父類的成員變量被放到了最後面。

運行如下的代碼:

C020 c020;
c020.C010::c_ = 0x04;

由於子類中的變量和父類中的變量重名,所以我們必須用這種方式來訪問屬於父類的成員變量,普通情況下不需要這種寫法。我們看看後面這行代碼對應的匯編代碼:

0042387E mov eax,dword ptr [ebp+FFFFF82Ch]
00423884 mov ecx,dword ptr [eax+4]
00423887 mov byte ptr [ebp+ecx+FFFFF82Ch],4

前面說過對象的起始是一個指針,第1行指令取到這個指針的值,第2行把這個指針指向的地址後移4字節後的值(做為一個4字節的值)取出來。執行完這句我們看看ecx寄存器,可知取出來的值為5。最後一行是真正的賦值指令,它通過在對象的起始處(即[ebp+FFFFF32Ch])加上ecx中的值做偏移值(即5)來得到賦值的目的地址。接合前面的對象布局輸出,我們可以發現從對象起始地址開始加5字節的偏移值,剛好得到父類的成員變量的地址。這樣我們可以大致分析出直接虛繼承的子類的對象布局。

|子類5            |父類1    |

|偏移值指針4,5|子類成員變量1|父類成員變量1|

(注:第一個數字為所在區域的長度(字節數),偏移值指針後的第二個數字為該指針指向的偏移值。後同。)

通過查看內存可以發現偏移值指針指向的內存前4字節為0,我不知道它的具體的用途是什麼。接下來的4字節是一個32位的整數,也就是真正的偏移值。即從子類的起始位置到被虛繼承的父類的起始位置的偏移值,在我們前面的例子中這個值為5(一個指針加一個char成員變量)。

通過這個分析我們可以看到在虛承繼的情況下,通過子類的對象訪問父類的普通成員變量的效率是相當低的。如果必須用到虛繼承,也應該盡量不要在父類中放置普通成員變量(靜態成員變量不受影響)。

另外為什麼微軟不把偏移值直接放到子類中,而是采用偏移值指針。我想是因為采用指針的方式更為靈活,即使以後需要擴展也不影響類對象的布局。

按下來我們再看看這幾行代碼:

PRINT_OBJ_ADR(c020);
C010 * pt = &c020;
PRINT_PT(pt);
pt->c_ = 0x03;

第2行聲明了一個父類指針,並讓它指向一個子類的對象。第3行打印出這個指針的值。運行結果為:

c020's address is : 0012F708
pt's value is : 0012F70D

我們可以看到賦值後的指針的值並不等於賦給它的對象地址值。也就是說在這個賦值過程中編譯器進行了額外的工作,即調整了指針的值。我們看看第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

喔!比想象的要復雜的多。一行簡單的指針賦值語句卻產生了這麼多的匯編代碼。這行代碼本身的語義是取對象的地址賦給一個指針,對於編譯器來說它把這做為指針到指針的賦值來處理。由於牽涉到了向上的類型轉換,同時又有虛繼承存在。根據前面的布局分析,在虛繼承的情況下,父類位於對象布局的後部。因此在這裡要做一個指針位置的調整。由於調整要根據源指針來進行計算,所以先要對源指針的合法性進行檢查,以避免運行時的指針異常錯誤。前3行的匯編指令就是在做這件事,檢查源指針是否為NULL。如果為NULL則執行4、5、10、11行,最終給pt賦0。如果不為NULL跳至第6行執行到最後。重要的是第6、7、8行代碼,它們通過偏移值指針找到偏移值,並以此來調整指針的位置,讓目的指針最終指向對象中的父類部分的數據成員。

對比一下普通的指針賦值,我們可以對上面賦值的復雜性和低效有更深的認識。

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個賦值也很直接,只是通過eax做了一個中轉。這裡我們就可以看到前面的虛繼承下的子類指針到父類指針的賦值是我麼的低效。在程序中應盡量的避免這種代碼。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved