本文主要簡述一下在Visual Studio中C++對象的內存布局,這裡沒有什麼測試代碼,只是以圖文的形式來描述一下內存分布,關於測試的代碼以及C++對象模型的其他內容大家可以參考一下陳皓先生的幾篇博文以及網上的其他一些文章:
《C++虛函數表解析》:http://blog.csdn.net/haoel/article/details/1948051
《C++對象的內存布局(上)》:http://blog.csdn.net/haoel/article/details/3081328
《C++對象的內存布局(下)》:http://blog.csdn.net/haoel/article/details/3081385
根據我自己調試出來的結果來看(Release版本),VS處理對象的原則大致可以分為以下幾點:
1、 對於普通成員變量,按照聲明次序以及內存的對齊原則存放。
例如對於類A,在32位程序中占據8個字節:
class A { public: void test() { cout << "A::test\n"; } private: int nA; char chA; };
在內存中布局如下:
2、 如果對象含有虛函數,則在對象的開頭處添加一個指針,該指針指向一個虛函數表,表中依序存放著該類中虛函數的地址。
例如對於類A,在32位程序中占據12個字節:
class A { public: virtual void f() { cout << "A::f()\n"; } virtual void fA() { cout << "A::fA()\n"; } private: int nA; char chA; };
在內存中布局如下,注意的是虛表的最後一項未必是0:
3、 如果對象只有直接繼承(非虛擬繼承)而來的父類,按照子類以及父類有沒有虛函數可以分為以下幾種情況:
<1>:子類和父類都沒有虛函數,按照繼承順序先放置各個父類部分,再放置子類部分;
例如對於類B,在32位程序中占據24個字節:
class A1 { private: int nA1; char chA1; }; class A2 { private: int nA2; char chA2; }; class B : public A1, public A2 { private: int nB; char chB; };
在內存中布局如下:
<2>:子類有虛函數,父類沒有虛函數,則先放置子類的虛函數表指針,再依次放置父類部分,最後放置子類部分;
例如對於類B,在32位程序中占據20個字節:
class A { private: int nA; char chA; }; class B : public A { public: virtual void f() { cout << "B::f()\n"; } virtual void fB() { cout << "B::fB()\n"; } private: int nB; char chB; };
在內存中布局如下:
<3>:子類和父類都有虛函數,則先把父類列表中帶有虛函數的父類放到前面,再依次放置沒有虛函數的父類,最後放置子類部分(沒有虛函數指針),同時修改各個虛函數表以及指針,使得其滿足如下條件:第一個虛函數表指針所指向的虛函數表中先存放繼承自本父類的虛函數地址,包括原樣繼承下來的以及重寫的,再放置子類獨有的虛函數地址,其余的虛函數表指針所指向的虛函數表只包含繼承自對應父類的虛函數地址,包括原樣繼承下來的以及重寫的。
例如對於類B,在32位程序中占據40個字節:
class A1 { private: int nA1; char chA1; }; class A2 { public: virtual void f() { cout << "A2::f()\n"; } virtual void fA2() { cout << "A2::fA2()\n"; } private: int nA2; char chA2; }; class A3 { public: virtual void f() { cout << "A3::f()\n"; } virtual void fA3() { cout << "A3::fA3()\n"; } private: int nA3; char chA3; }; class B : public A1, public A2, public A3 { public: virtual void f() { cout << "B::f()\n"; } virtual void fA3() { cout << "B::fA3()\n"; } virtual void fB() { cout << "B::fB()\n"; } private: int nB; char chB; };
在內存中布局如下:
4、(這一點還不太確定)如果對象有虛基類,無論是自己虛擬繼承而來的還是父類虛擬繼承而來的,則先按照以上規則將非虛基類部分處理完畢之後,再插入一個指針,再放置該類剩余的成員變量(該指針指向一個表格,表格中的每一項均是一個32位帶符號整數——無論32位程序還是64位程序,其中第一項的內容是該指針到本類首部的偏移量,之後依次是該指針到本類虛基類的起始位置的偏移量),當這些全部放置完畢之後,然後再依次放置虛基類。這裡有一個問題就是有的時候會在虛基類前面放置一個全零的指針,然而有的時候卻又沒有,按照我目前測試的結果來看,當子類有構造函數並且只要重寫了虛基類的一個函數該虛基類前面就會有這個全零的指針。
例如對於下列程序,在32位程序中占用空間情況分別為:
class A { public: virtual void f() { cout << "A::f()\n"; } virtual void fA() { cout << "A::fA()\n"; } private: int nA; char chA; }; class B : public virtual A { public: virtual void f() { cout << "B::f()\n"; } virtual void fB() { cout << "B::fB()\n"; } private: int nB; char chB; }; class C { public: virtual void f() { cout << "C::f()\n"; } virtual void fC() { cout << "C::fC()\n"; } private: int nC; char chC; }; class D { public: virtual void f() { cout << "D::f()\n"; } virtual void fD() { cout << "D::fD()\n"; } private: int nD; char chD; }; class E { public: virtual void f() { cout << "E::f()\n"; } virtual void fE() { cout << "E::fE()\n"; } private: int nE; char chE; }; class F : public virtual B, public virtual C, public D, public virtual E { public: F() {} virtual void f() { cout << "F::f()\n"; } virtual void fB() { cout << "F::fB()\n"; } virtual void fC() { cout << "F::fC()\n"; } virtual void fE() { cout << "F::fE()\n"; } virtual void fF() { cout << "F::fF()\n"; } private: int nF; char chF; };
A、C、D、E均占12個字節,以A為例,內存布局如下:
B占28個字節,在內存中布局如下,值得注意的一點是,此時B因為已經重寫了虛基類A的一個虛函數f(),所以如果再顯示定義一個構造函數的話,在B中的A部分之前就會添加一個全0的指針,但是如果把構造函數或者f()的重寫隨便去掉一個,這個全0指針就不會存在了:
F占92個字節,在內存布局中如下,其中虛基類的排列順序是按照列表裡的順序來的,比如在本例中,F虛擬繼承的有B、C、E三個類,所以在虛基類的排放順序中先放B,又因為B虛擬繼承了A,所以最後的順序是A、B、C、E。還有另外一點就是因為F定義了一個構造函數,所以虛基類前面會有一個全零指針,如果把這個構造函數去掉的話,F的大小就變成了76個字節,正好是92字節減去4個全零指針的大小: