鑽石型虛擬多重繼承的C++對象內存模型


關於C++對象內存布局的資料和書籍也有很多,比如陳皓老師的博客:

1、C++對象的內存布局(上)

2、C++對象的內存布局(下)

白楊:

RTTI、虛函數和虛基類的實現方式、開銷分析及使用指導

左手為你畫猜:

C++類對象內存模型與成員函數調用分析(上、中、下)

關於講解C++對象內存模型最好的書應該是侯捷老師翻譯的《深度探索C++對象內存模型》。

這兩天在看其他書籍時,對C++中虛擬繼承的實現機制不太理解,於是又重新翻回《深度探索C++對象內存模型》一書,並結合C++對象的內存布局(下)一文。在Visual Studio 2010下用“cl”編譯器進行測試,查看虛擬多重繼承下的C++對象內存模型。總結如下:

一、重復繼承

所謂重復繼承,即某個基類被間接地重復繼承了多次。為方便對比說明,下面的代碼采用了陳皓老師博客中C++類例子。

UML類圖如下:

類繼承的源代碼如下,直接采用C++對象的內存布局(下)中的例子,相關解釋已在原博客中詳細說明,故在此不再贅述:

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class B
 5 {
 6 public:
 7     int ib;
 8     char cb;
 9 public:
10     B():ib(0),cb('B')
11     {}
12     virtual void f()
13     {
14         cout<<"B::f()"<<endl;
15     }
16     virtual void Bf()
17     {
18         cout<<"B::Bf()"<<endl;
19     }
20 };
21 
22 class B1:public B
23 {
24 public:
25     int ib1;
26     char cb1;
27 public:
28     B1():ib1(01),cb1('1'){}
29 
30     virtual void f()
31     {
32         cout<<"B1::f()"<<endl;
33     }
34     virtual void f1()
35     {
36         cout<<"B1::f1()"<<endl;
37     }
38     virtual void Bf1()
39     {
40         cout<<"B1::Bf1()"<<endl;
41     }
42 };
43 
44 class B2:public B
45 {
46 public:
47     int ib2;
48     char cb2;
49 public:
50     B2():ib2(10),cb2('2'){}
51     virtual void f()
52     {
53         cout<<"B2::f()"<<endl;
54     }
55     virtual void f2()
56     {
57         cout<<"B2::f2()"<<endl;
58     }
59     virtual void Bf2()
60     {
61         cout<<"B2::Bf2()"<<endl;
62     }
63 };
64 
65 class D: public B1, public B2
66 {
67 public:
68     int id;
69     char cd;
70 public:
71     D():id(100),cd('D'){}
72 
73     virtual void f()
74     {
75         cout<<"D::f()"<<endl;
76     }
77     virtual void f1()
78     {
79         cout<<"D::f1()"<<endl;
80     }
81     virtual void f2()
82     {
83         cout<<"D::f2()"<<endl;
84     }
85     virtual void Df()
86     {
87         cout<<"D::Df()"<<endl;
88     }
89 
90 };
91 int main(int argc, char *argv[])
92 {
93     D d;
94     system("pause");
95     return 0;
96 }

在陳皓老師博客中,直接利用函數指針調用C++對象起始位置處虛函數表指針指向的虛函數表中的虛函數,以查看C++對象的內存模型。下面我們主要采用Visual Studio 2010 和 Visual C++下的“cl”編譯器查看C++對象內存模型。

Visual Studio 2010 IDE開發環境中,我們查看派生類D對象的內存模型。如下圖所示:

  

從上兩圖我們可以基本看出:

1、派生類D對象d的內存布局中,由其基類依次組裝而成,再加上派生類自己的成員變量。

2、其中基類布局依次按照在派生類中的聲明順序排列。

3、每個基類都有自己的虛函數表,指向虛函數表的指針_vfptr放置在最前面的位置。

為了再進一步了解重復繼承中的C++對象內存模型,我們采用Visual C++下的“cl”編譯器進行查看。

在“Microsoft Visual C++”的編譯環境中,我們可以利用編譯器“cl”、鏈接器“link”、可執行文件查看器“dumpbin”來查看Windows下可執行文件(COFF格式)的變量、函數怎么存儲。

“cl”即Visual C++ 的編譯器,即“Compiler”的縮寫。在Visual Studio 2010安裝完后,會有一個批處理文件用來建立運行這些工具所需要的環境。它位於開始/程序/Microsoft Visual Studio 2010/Visual Studio Tools/Viusual Studio 2010 Command Prompt,這樣我們就可以利用命令行使用VC++的編譯器了。

在“cl”編譯器中有個編譯選項可以查看C++類的內存布局,使用如下:打開Visual Studio的命令行提示符即Viusual Studio 2010 Command Prompt,按如下格式輸入:

>cl [.cpp] /d1reportSingleClassLayout[classname]

d1reportSingleClassLayout可以查看源文件中所有類及結構體的內存布局,classname為類名,/d1reportSingleClassLayout[classname]之間沒有空格。使用如下圖所示:

使用cl編譯器查看重復繼承中的C++對象內存模型結果如下圖所示:

 從上圖可以看出,編譯器在實現時使用了字節對齊(Alignment),以實現在對象內存中存取更有效率。字節對齊就是將數值調整到某數的整數倍,在32位計算機中,通常Alignment為4bytes,以使bus的“運輸量”達到最高效率。

可以看出,派生類D對象在內存中占有44個字節。

 重復繼承中的C++對象內部模型用圖片表示如下:

從圖中可以看出,在派生類D中,存在着兩份基類B的成員實例,分別為ib和cb,所以在C++對象的內存布局(下)指出這樣可能會出現二義性編譯錯誤。我們可以指定類作用域符::進行限定來消除二義性,也可以在語言層面利用虛擬繼承機制來解決。

二、鑽石型多重虛擬繼承

 在《深度探索C++對象模型》中提到:一個virtual base class subobject只會在derived class中存在一份實體,不管它在class繼承體系中出現多少次!

因此,虛擬繼承的就是為了解決重復繼承中多個間接父類的問題。鑽石型的結構就是最經典的虛擬多重繼承結構。

UML類圖如下:

 

 

如上圖,讓B1和B2各自維護的一個B子對象,折疊成一個由D維護的單一的B子對象,並且還可以保存基類和派生類的指針之間的多態指定操作,這對於編譯器實現來說,難度非常高。《深度探索C++對象模型》提到一般的實現方法如下所述:將D對象分割為兩部分,一個不變局部和一個共享局部。不變局部中的數據,不管后繼如何衍化,總是擁有固定的偏移量,所以這一部分數據可以被直接存取,至於共享局部,所表現的就是虛擬繼承的基類子對象,這一部分的數據,其位置會因為每次的派生操作而有變化,所以它們是間接存取。

所以,一般的布局策略是安排好派生類對象的不變部分,然后再建立其共享部分。在接下來的分析可以看出,VC++編譯器實現中,在每一個派生類對象中插入一些指針vbptr,每個指針指向一個虛擬繼承的基類子對象。要存取繼承得來的基類子對象,可以使用相關指針間接完成。

要實現虛擬繼承,我們只需要在B1和B2繼承B的語法中加入virtual關鍵字即可。實現代碼如下:

 

 1 #include <iostream>
 2 using namespace std;
 3 
 4 class B
 5 {
 6 public:
 7     int ib;
 8     char cb;
 9 public:
10     B():ib(0),cb('B')
11     {}
12     virtual void f()
13     {
14         cout<<"B::f()"<<endl;
15     }
16     virtual void Bf()
17     {
18         cout<<"B::Bf()"<<endl;
19     }
20 };
21 
22 class B1:virtual public B
23 {
24 public:
25     int ib1;
26     char cb1;
27 public:
28     B1():ib1(01),cb1('1'){}
29 
30     virtual void f()
31     {
32         cout<<"B1::f()"<<endl;
33     }
34     virtual void f1()
35     {
36         cout<<"B1::f1()"<<endl;
37     }
38     virtual void Bf1()
39     {
40         cout<<"B1::Bf1()"<<endl;
41     }
42 };
43 
44 class B2:virtual public B
45 {
46 public:
47     int ib2;
48     char cb2;
49 public:
50     B2():ib2(10),cb2('2'){}
51     virtual void f()
52     {
53         cout<<"B2::f()"<<endl;
54     }
55     virtual void f2()
56     {
57         cout<<"B2::f2()"<<endl;
58     }
59     virtual void Bf2()
60     {
61         cout<<"B2::Bf2()"<<endl;
62     }
63 };
64 
65 class D: public B1, public B2
66 {
67 public:
68     int id;
69     char cd;
70 public:
71     D():id(100),cd('D'){}
72 
73     virtual void f()
74     {
75         cout<<"D::f()"<<endl;
76     }
77     virtual void f1()
78     {
79         cout<<"D::f1()"<<endl;
80     }
81     virtual void f2()
82     {
83         cout<<"D::f2()"<<endl;
84     }
85     virtual void Df()
86     {
87         cout<<"D::Df()"<<endl;
88     }
89 
90 };
91 int main(int argc, char *argv[])
92 {
93     D d;
94     system("pause");
95     return 0;
96 }
View Code

 

使用cl編譯器查看鑽石型虛擬重復繼承中的C++對象內存模型結果如下圖所示:

 

 

從上圖可以看出,虛擬重復繼承中的派生類D對象在內存中占有52字節,比之前多了8個字節。

 虛擬重復繼承中的C++對象內部模型用圖片表示如下:

 

從圖中可以看出,VC++編譯器在實現虛擬繼承時,在派生類的對象中安插了兩個vbptr指針。因此,對每個繼承自虛基類的類實例,將增加一個隱藏的“虛基類表指針”(vbptr)成員變量,從而達到間接計算虛基類位置的目的。該變量指向一個全類共享的偏移量表,表中項目記錄了對於該類而言,“虛基類表指針”與虛基類之間的偏移量。由上可以看出,B1虛基類表指針vbptr與虛基類B之間的偏移量是40字節,B2虛基類表指針vbptr與虛基類B之間的偏移量是24字節。第一項中-4的含義:表示的是vptr和vbptr的距離,如果B1中沒有虛函數的定義,這個地方就會是0。vbptr就是存放在vptr下面的位置。

我們注意到在虛擬繼承的C++對象內存布局中,還有一個4個字節的vtordisp字段,vtordisp在MSDN中這樣解釋

Enables the addition of the hidden vtordisp construction/destruction displacement member. The vtordisp pragma is applicable only to code that uses virtual bases. If a derived class overrides a virtual function that it inherits from a virtual base class, and if a constructor or destructor for the derived class calls that function using a pointer to the virtual base class, the compiler may introduce additional hidden “vtordisp” fields into classes with virtual bases.

也就是說如果虛擬繼承中派生類重寫了基類的虛函數,並且在構造函數或者析構函數中使用指向基類的指針調用了該函數,編譯器會為虛基類添加vtordisp域。

但在本例中,vtordisp為什么存在於C++派生類對象中,對象如何使用它,我卻不得而知,希望向大家請教。

至此,我們已經分析完在VC++編譯器實現中的重復繼承和鑽石型虛擬重復繼承的C++對象內存模型。這篇博客也花了大概4個小時,時間挺久,但非常值得,希望大家多多指教。

 轉載和分享請注明出處。http://www.cnblogs.com/liu-jun/archive/2013/05/17/3083736.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM