關於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 }
使用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