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

C++對象布局及多態實現之帶虛函數的類

編輯:關於C++

如果類中存在虛函數時,情況會怎樣呢?我們知道當一個類中有虛函數時,編譯器會為該類產生一個虛函數表,並在它的每一個對象中插入一個指向該虛函數表的指針,通常這個指針是插在對象的起始位置。所謂的虛函數表實際就是一個指針數組,其中的指針指向真正的函數起始地址。我們來驗證一下,定義一個無成員變量的類C040,內含一個虛函數。

struct C040
{
 virtual void foo() {}
};

運行如下代碼打印它的大小及對象中的內容。

PRINT_SIZE_DETAIL(C040)

結果為:

The size of C040 is 4
The detail of C040 is 40 b4 45 00

果然它的大小為4字節,即含有一個指針,指針指向的地址為0x0045b440。

同樣再定義一個空類C050,派生自類C040。

struct C050 : C040
{};

由於虛函數會被繼承,且維持為虛函數。那麼類C050的對象中同樣應該含有一個指向C050的虛函數表的指針。

運行如下代碼打印它的大小及對象中的內容。

PRINT_SIZE_DETAIL(C050)

結果為:

The size of C050 is 4
The detail of C050 is 44 b4 45 00

果然它的大小也為4字節,即含有一個指向虛函數表(後稱虛表)的指針(後稱虛表指針)。

虛表是類級別的,類的所有對象共享同一個虛表。我們可以生成類C040的兩個對象,然後通過觀察對象的地址、虛表指針地址、虛表地址、及虛表中的條目的值(即所指向的函數地址)來進行驗證。

運行如下代碼:

C040 obj1, obj2;
PRINT_VTABLE_ITEM(obj1, 0, 0)
PRINT_VTABLE_ITEM(obj2, 0, 0)

結果如下:

obj1 : objadr:0012FDC4 vpadr:0012FDC4 vtadr:0045B440 vtival(0):0041D834
obj2 : objadr:0012FDB8 vpadr:0012FDB8 vtadr:0045B440 vtival(0):0041D834

(注:第一列為對象名,第二列(objadr)為對象的內存地址,第三列(vpadr)為虛表指針地址,第四列(vtadr)為虛表的地址,第五列(vtival(n))為虛表中的條目的值,n為條目的索引,從0開始。後同)

果然對象地址不同,虛表指針(vpadr)位於對象的起始位置,所以它的地址和對象相同。兩個對象的虛表指針指向的是同一個虛表,因此(vtadr)的值相同,虛表中的第一條目(vtival(0))的值當然也一樣。

接下來,我們再觀察類C040和從它派生的類C050的對象,這兩個類各有自己的虛表,但由於C050沒有重寫繼承自C040的虛函數,所以它們的虛表中的條目的值,即指向的虛函數的地址應該是一樣的。

運行如下代碼:

C040 c040;
C050 c050;
PRINT_VTABLE_ITEM(c040, 0, 0)
PRINT_VTABLE_ITEM(c050, 0, 0)

結果為:

c040 : objadr:0012FD4C vpadr:0012FD4C vtadr:0045B448 vtival(0):0041D834
c050 : objadr:0012FD40 vpadr:0012FD40 vtadr:0045B44C vtival(0):0041D834

果然這次我們可以看到雖然前幾列皆不相同,但最後一列的值相同。即它們共享同一個虛函數。

定義一個C043類,包含兩個虛函數。再定義一個C071類,從C043派生,並重寫繼承的第一個虛函數。

struct C043
{
 virtual void foo1() {}
 virtual void foo2() {}
};
struct C071 : C043
{
 virtual void foo1() {}
};

我們可以預料到,C043和C071各有一個包含兩個條目的虛表,由於C071派生自C043,並且重寫了第一個虛函數。那麼這兩個類的虛表的第一個條目值是不同的,而第二項應該是相同的。運行如下代碼。

C043 c043;
C071 c071;
PRINT_SIZE_DETAIL(C071)
PRINT_VTABLE_ITEM(c043, 0, 0)
PRINT_VTABLE_ITEM(c071, 0, 0)
PRINT_VTABLE_ITEM(c043, 0, 1)
PRINT_VTABLE_ITEM(c071, 0, 1)

結果為:

The size of C071 is 4
The detail of C071 is 5c b4 45 00
c043 : objadr:0012FCD4 vpadr:0012FCD4 vtadr:0045B450 vtival(0):0041D4F1
c071 : objadr:0012FCC8 vpadr:0012FCC8 vtadr:0045B45C vtival(0):0041D811
c043 : objadr:0012FCD4 vpadr:0012FCD4 vtadr:0045B450 vtival(1):0041DFE1
c071 : objadr:0012FCC8 vpadr:0012FCC8 vtadr:0045B45C vtival(1):0041DFE1

觀察第1、2行的最後一列,即兩個類的虛表的第一個條目,由於C071重寫了foo1函數,所以這個值不一樣。而第3、4行的最後一列為兩個類的虛表的第二個條目,由於C071並沒有重寫它,所以這兩個值是相同的。和我們之間的猜測是一致的。

接下來我們看看多重繼承。定義兩個類,各含一個虛函數,及一個數據成員。再從這兩個類派生一個空子類。

struct C041
{
 C041() : c_(0x01) {}
 virtual void foo() { c_ = 0x02; }
 char c_;
};
struct C042
{
 C042() : c_(0x02) {}
 virtual void foo2() {}
 char c_;
};
struct C051 : public C041, public C042
{};

運行如下代碼:

PRINT_SIZE_DETAIL(C041)
PRINT_SIZE_DETAIL(C042)
PRINT_SIZE_DETAIL(C051)

結果為:

The size of C041 is 5
The detail of C041 is 64 b3 45 00 01
The size of C042 is 5
The detail of C042 is 68 b3 45 00 02
The size of C051 is 10
The detail of C051 is 6c b4 45 00 01 68 b4 45 00 02

注意,首先我們觀察C051的對象輸出,發現它的大小為10字節,這說明它有兩個虛表指針,從導出的內存數據我們可以推斷,首先是一個虛表指針,然後是從C041繼承的成員變量,值也是我們在C041的構造函數中賦的值0x01,然後又是一個虛表指針,再是從C042繼承的成員變量,值為0x02。

為了驗證,我們再運行如下代碼:

C041 c041;
C042 c042;
C051 c051;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_VTABLE_ITEM(c042, 0, 0)
PRINT_VTABLE_ITEM(c051, 0, 0)
PRINT_VTABLE_ITEM(c051, 5, 0)

注意最後一行的第二個參數,5。它是從對象起始地址開始到虛表指針的偏移值(按字節計算),從上面的對象內存輸出我們看到C041的大小為5字節,因此C051中第二個虛表指針的起始位置距對象地址的偏移為5字節。輸出的結果為:

(注:這個偏移值是通過觀察而判斷出來的,並不通用,而且它依賴於我們前面所說的編譯器在生成代碼時所用的結構成員對齊方式,我們將這個值設為1。如果設為其他值會影響對象的大小及這個偏移值。參見第一篇起始處的說明。下同。)

c041 : objadr:0012FB88 vpadr:0012FB88 vtadr:0045B364 vtival(0):0041DF1E
c042 : objadr:0012FB78 vpadr:0012FB78 vtadr:0045B368 vtival(0):0041D43D
c051 : objadr:0012FB64 vpadr:0012FB64 vtadr:0045B46C vtival(0):0041DF1E
c051 : objadr:0012FB64 vpadr:0012FB69 vtadr:0045B468 vtival(0):0041D43D

這下我們可以看到C051的兩個虛表指針指向兩個不現的虛表(第3、4行的vtadr列),而虛表中的條目的值分別等於C041和C042(即它的兩個父類)的虛表條目的值(第1、3行和2、4行的vtival列的值相同)。

為什麼子類要有兩個虛表,而不是將它們合並為一個。主要是在處理類型的動態轉換時這種對象布局更方便調整指針,後面我們看到這樣的例子。

如果子類重寫父類的虛函數會怎麼樣?前面的類C071我們已經看到過一次了。我們再定義一個從C041和C042派生的類C082,並重寫這兩個父類中的虛函數,同時再增加一個虛函數。

struct C041
{
 C041() : c_(0x01) {}
 virtual void foo() { c_ = 0x02; }
 char c_;
};
struct C042
{
 C042() : c_(0x02) {}
 virtual void foo2() {}
 char c_;
};
struct C082 : public C041, public C042
{
 C082() : c_(0x03) {}
 virtual void foo() {}
 virtual void foo2() {}
 virtual void foo3() {}
 char c_;
};

運行和上面類似的代碼:

PRINT_SIZE_DETAIL(C082)
C041 c041;
C042 c042;
C082 c082;
PRINT_VTABLE_ITEM(c041, 0, 0)
PRINT_VTABLE_ITEM(c042, 0, 0)
PRINT_VTABLE_ITEM(c082, 0, 0)
PRINT_VTABLE_ITEM(c082, 5, 0)

結果為:

The size of C082 is 11
The detail of C082 is 70 b3 45 00 01 6c b3 45 00 02 03
c041 : objadr:0012FA74 vpadr:0012FA74 vtadr:0045B364 vtival(0):0041DF1E
c042 : objadr:0012FA64 vpadr:0012FA64 vtadr:0045B368 vtival(0):0041D43D
c082 : objadr:0012FA50 vpadr:0012FA50 vtadr:0045B370 vtival(0):0041D87A
c082 : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(0):0041D483

果然C082的兩個虛表中的條目值都和父類的不一樣了(vtival列),指向了重寫後的新函數地址。觀察C082的大小和對象內存,我們可以知道它並沒有為新定義的虛函數foo3生成新的虛表。那麼foo3的函數地址到底是加到了類的第一個虛表,還是第二個虛表中?在調試狀態下,我們在“局部變量”窗口中展開c082對象。我們可以看到兩個虛表及其中的條目,但兩個虛表都只能看到第一個條目。這應該是VC7.1IDE的一個小BUG。看來我們只有另想辦法來驗證。我們先把兩個虛表中的第二個條目位置上的值打印出來。運行如下代碼。

PRINT_VTABLE_ITEM(c082, 0, 1)
PRINT_VTABLE_ITEM(c082, 5, 1)

結果如下:

c082 : objadr:0012FA50 vpadr:0012FA50 vtadr:0045B370 vtival(1):0041D32F
c082 : objadr:0012FA50 vpadr:0012FA55 vtadr:0045B36C vtival(1):0041D87A

然後我們調用一下foo3函數:

c082.foo3();

查看它的匯編代碼:

004225F3 lea ecx,[ebp+FFFFFB74h]
004225F9 call 0041D32F

第2條call指令後的地址就是foo3的函數地址了(實際上是一個跳轉指令),對照前面的輸出我們就可以知道,子類新定義的虛函數對應的虛表條目加入到了子類的第一個虛表中,並位於繼承自父類的虛表條目之後。

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