C++語言通過引入虛函數表的形式來支持多態特性,並且為了解決多重繼承中的冗余和二義性問題又引入了虛繼承,這使得C++類的內存模型呈現出一定的復雜性。
C++要求所有實例化的對象都要有相應的內存地址,因此對一個不包含任何成員變量、成員函數的空類的實例會占用一個字節的內存空間。而非空類則按照以下規則安排其成員在內存中的排列順序:
成員函數不占用內存空間
同一個類(不包括父類)中的成員變量在內存中按照在類中的聲明次序依次排列,排列順序與訪問權限、變量名沒有關系
繼承自父類的成員變量排在當前類所有成員變量之前
繼承自多個父類的成員變量按照類繼承列表中的聲明順序依次排列每個父類中的成員變量
根據這些規則,定義下面這些類並限制他們間的繼承關系,我們可以很容易的得出KTestClass類任一實例的內存模型:
class KTopClass { public: KTopClass() : m_iTopVar(0) { cout << "KTopClass constructed." << endl; } private: int m_iTopVar; }; class KLBaseClass : public KTopClass { public: KLBaseClass() : m_iLVar(0) { cout << "KLBaseClass constructed." << endl; } private: int m_iLVar; }; class KRBaseClass { public: KRBaseClass() : m_iRVar(0) { cout << "KRBaseClass constructed." << endl; } private: int m_iRVar; }; class KTestClass : public KLBaseClass, public KRBaseClass { private: int m_iVar1; public: KTestClass() : m_iVar1(0), m_iVar2(0) { cout << "KTestClass constructed." << endl; } int m_iVar2; };
為了支持多態特性,C++引用虛函數表,父類中的所有虛函數均列在虛函數表中(按照聲明順序,純虛函數也不例外),子類首先繼承父類的虛函數表(非虛繼承),如果重寫了某個虛函數,那麼就用自己所重寫的函數地址去覆蓋繼承下來的虛函數表中的對應虛函數的舊地址,這個舊地址可能是父類新定義的虛函數的地址,也可能是父類覆蓋了父類的父類中的虛函數後的“新地址”,之後再將當前子類所新定義的虛函數依次加在父類虛函數表的後面。如果子類沒有重寫父類的虛函數,那麼直接繼承父類的虛函數表,並將當前子類所新定義的虛函數依次加在父類虛函數表的後面。處理完虛函數表後,然後再依次排列父類的成員變量。重復這個過程直到所有父類按照繼承順序依次處理完畢,然後再排列子類的成員變量,成員變量的排列規則同上一節所述。需要注意的是如果是多繼承,那麼子類的虛函數是接在第一個被繼承的有虛函數的父類虛函數表後面的。
舉個較為復雜的例子來理解:
class KTopClass { public: KTopClass() : m_iTopVar(0) { cout << "KTopClass constructed." << endl; } virtual void virtual_top_test(){ cout << "KTopClass::virtual_top_test." << endl; } virtual void pure_virtual_top_test() = 0; private: int m_iTopVar; }; class KLBaseClass : public KTopClass { public: KLBaseClass() : m_iLVar(0) { cout << "KLBaseClass constructed." << endl; } virtual void virtual_lbase_test(){ cout << "KLBaseClass::virtual_lbase_test." << endl; } void pure_virtual_top_test() { cout << "KLBaseClass::pure_virtual_top_test." << endl; } private: int m_iLVar; }; class KRBaseClass { public: KRBaseClass() : m_iRVar(0) { cout << "KRBaseClass constructed." << endl; } virtual void virtual_rbase_test(){ cout << "KRBaseClass::virtual_rbase_test." << endl; } private: int m_iRVar; }; class KTestClass : public KLBaseClass, public KRBaseClass { private: int m_iVar1; public: KTestClass() : m_iVar1(0), m_iVar2(0) { cout << "KTestClass constructed." << endl; } void virtual_lbase_test(){ cout << "KTestClass::virtual_lbase_test." << endl; } int m_iVar2; };
內存模型如下:
這個例子中一共有兩張虛表,注意這裡所說的虛表實際上是指向一個數組的指針,因此每一張虛表占用四個字節。
存在虛繼承的情況下,類實例的內存模型與沒有虛繼承的情況基本是相同的,但是要注意一個原則,即被虛繼承的父類在子類中是共享的,而非虛繼承的父類在每個子類中都有一份。如果要用一個例子來說明,我們可以先將上一節中的KRBaseClass類的聲明修改一下,使其也繼承自KTopClass類,其余部分保持不變,KRBaseClass 部分修改如下:
class KRBaseClass : public KTopClass { public: KRBaseClass() : m_iRVar(0) { cout << "KRBaseClass constructed." << endl; } virtual void virtual_rbase_test(){ cout << "KRBaseClass::virtual_rbase_test." << endl; } void pure_virtual_top_test() { cout << "KRBaseClass::pure_virtual_top_test." << endl; } private: int m_iRVar; };
這時的內存模型就變為:
可見在KTestClass的實例中有兩份KTopClass,這就是所謂的非虛繼承的父類在每個子類中都有一份,這造成了一定的冗余和調用時的二義性,為了消除這種二義性,C++引入了虛繼承,被虛繼承的父類在子類中是共享的。可以設計如下的繼承結構來說明這個問題:
class B { /* ... */ }; class X : virtual public B { /* ... */ }; class Y : virtual public B { /* ... */ }; class Z : public B { /* ... */ }; class AA : public X, public Y, public Z { /* ... */ };
在實際的AA的實例中是有兩份B的子對象的,其中一份由X、Y子對象共享,因為它們是虛繼承自B類的,而另一份是來自於Z子對象的。
我們還是回到最開始的例子中,我們將KLBaseClass類和KRBaseClass均改為虛繼承自KTopClass類:
class KTopClass { public: KTopClass() : m_iTopVar(0) { cout << "KTopClass constructed." << endl; } virtual void virtual_top_test(){ cout << "KTopClass::virtual_top_test." << endl; } virtual void pure_virtual_top_test() = 0; private: int m_iTopVar; }; class KLBaseClass : virtual public KTopClass { public: KLBaseClass() : m_iLVar(0) { cout << "KLBaseClass constructed." << endl; } virtual void virtual_lbase_test(){ cout << "KLBaseClass::virtual_lbase_test." << endl; } void virtual_top_test(){ cout << "KLBaseClass::virtual_top_test." << endl; } private: int m_iLVar; }; class KRBaseClass : virtual public KTopClass { public: KRBaseClass() : m_iRVar(0) { cout << "KRBaseClass constructed." << endl; } virtual void virtual_rbase_test(){ cout << "KRBaseClass::virtual_rbase_test." << endl; } private: int m_iRVar; }; class KTestClass : public KLBaseClass, public KRBaseClass { private: int m_iVar1; public: KTestClass() : m_iVar1(0), m_iVar2(0) { cout << "KTestClass constructed." << endl; } void virtual_lbase_test(){ cout << "KTestClass::virtual_lbase_test." << endl; } void pure_virtual_top_test() { cout << "KTestClass::pure_virtual_top_test." << endl; } int m_iVar2; };
此時KTestClass實例的內存模型變成了如下形式,即所有被虛繼承的父類對應的子對象被統一放在了最後,實際上,不同編譯器對放置位置的處理稍有不同,有的是將虛繼承的子對象放在了子對象所有成員變量的最後面,有的則是放在了所有父類子對象之後,子類子對象之前。
到這裡,很容易就會產生一個疑問,如果KLBaseClass和KRBaseClass中只有KRBaseClass是虛繼承自KTopClass呢,實際上這種情況下,KRBaseClass所對應的那一份KTopClass子對象被放在最後,而KLBaseClass那一份仍按原來的規則存放。
本文分別分析了有無虛函數以及虛繼承情況下類對象的內存模型,從底層對C++的多態特性的實現進行了解釋,但需要注意的是,實際中不同編譯器下對內存模型的實現細節是稍有不同的。