在許多筆試面試中都會涉及到sizeof 運算符的求值問題。
這類問題主要分四類:
- 基本數據類型,如int,bool,fload,long,long,int * 等,這一類比較簡單,但要注意x86和x64情況下的指針大小
- 枚舉 enum。這個類型網絡上有說是1-4個byte,根據最大值決定的;也有說是sizeof(int)。我這邊個人使用 visual studio 2015 獲得的結果是4個byte
- struct 和 union 組合類型。union 是取其中一個最大成員的size作為其size;struct 則要考慮對齊填充因素
- class 類型,class 就稍微復雜點,不僅僅要考慮對齊填充因素,還要考慮繼承,虛繼承,虛函數等因素。
下文主要講述class 的內存布局,稍帶介紹一下struct 的size。
struct 的內存布局:
struct 的內存對齊和填充概念學過C 的都應該知道一點。其實只要記住一個概念和三個原則就可以了:
一個概念:
自然對齊:如果一個變量的內存地址正好位於它長度的整數倍,就被稱做自然對齊。
如果不自然對齊,會帶來CPU存取數據時的性能損失。(PS:具體應該與CPU通過總線讀寫內存數據的細節相關,具體沒有細究)
三個原則:
- struct 的起始地址需要能夠被其成員中最寬的基本數據類型整除;
- struct 的size 也必須能夠被其成員中最寬的基本數據類型整除;
- struct 中每個成員地址相對於struct 的起始地址的offset,必須是自然對齊的。
Class 的內存布局:
在學習C++ 的class 的內存布局前,先介紹下文會被用到的Visual studio 中的編譯選項"/d1reportAllClassLayout" 和 "/d1reportSingleClassLayout[ClassName]"。
這兩個編譯選項分別會輸出當前編譯單元中所以class 的內存布局和指定class 的內存布局。對於學習class 的內存布局很方便。
關於一個class 的定義,在定義過程中涉及到的有:
成員數據(靜態,非靜態)和成員函數(靜態,非靜態,virtual)。
所有的成員函數都不會占用對象的存儲空間,無論是靜態,非靜態還是虛函數。
而對於成員數據來說,只有非靜態的數據才會占用對象的存儲空間。
這個很好理解,靜態成員數據和成員函數是屬於class 的,而非屬於具體的對象,所以只要維護一份內存就可以了,無需每個對象都拷貝一份。
但是影響對象的大小的因素並不僅僅與看到的成員變量有關:
非靜態成員變量,虛函數表指針(_vftprt),虛基類表指針(_vbtptr),上文的內存對齊
-
空類
class CEmpty{};
對於空類,許多人想當然的認為大小應該是0。這是錯誤的,如果是正確的話,這個類可以被實例化成一個對象,且這個對象不占任何存儲空間,且可以有很多不占任何空間的對象,而且這個不占空間的對象還可以有指針,這樣就很奇怪了。
所以正常編譯器會給空類分配1個byte 的空間用於標示。
sizeof(CEmpty) = 1
-
普通類
class CBase { public: int m_ia; static int s_ib; private: void f(); void g(); };
其類的布局如下:
class CBase size(4): +--- 0 | m_ia +---
只有m_ia 成員,size 為4個byte。因為靜態數據成員和成員函數不占有對象空間。
-
有虛函數的類
class CBase { public: int m_ia; private: void f(); void g(); virtual void h(); };
其類的布局如下:
class CBase size(8): +--- 0 | {vfptr} 4 | m_ia +--- CBase::$vftable@: | &CBase_meta | 0 0 | &CBase::h
可以看到該類的起始地址是放了一個"vfptr",這個指針用來指向該類的虛函數表。
-
單一繼承的類(無虛函數)
class CBase { public: int m_ia; private: void f(); void g(); }; class CChild :public CBase { public: int m_iChild; };
類的布局如下:
class CChild size(8): +--- | +--- (base class CBase) 0 | | m_ia | +--- 4 | m_iChild +---
即派生類中拷貝了一份基類中的成員數據,所以size 為8個byte。
-
單一繼承的類(含有虛函數)
class CBase { public: int m_ia; public: virtual ~CBase(); virtual void f(); virtual void g(); }; class CChild :public CBase { public: int m_iChild; public: virtual ~CChild(); virtual void g(); };
其類的布局如下:
class CChild size(12): +--- | +--- (base class CBase) 0 | | {vfptr} 4 | | m_ia | +--- 8 | m_iChild +--- CChild::$vftable@: | &CChild_meta | 0 0 | &CChild::{dtor} 1 | &CBase::f 2 | &CChild::g
可以看到派生類中只有一個"vfptr",但是虛函數表中的函數卻不同於基類中的函數,沒有重寫的虛函數沿用基類中的虛函數,而被重寫的虛函數則更新為派生類中的虛函數。
-
多重繼承的類(基類都含有虛函數)
class CBase1 { public: int m_i1; public: virtual ~CBase1(); virtual void f1(); virtual void g1(); }; class CBase2 { public: int m_i2; public: virtual ~CBase2(); virtual void f2(); virtual void g2(); }; class CChild :public CBase1, public CBase2 { public: int m_iChild; public: virtual ~CChild(); virtual void f1(); virtual void g2(); };
其類的布局如下:
class CChild size(20): +--- | +--- (base class CBase1) 0 | | {vfptr} 4 | | m_i1 | +--- | +--- (base class CBase2) 8 | | {vfptr} 12 | | m_i2 | +--- 16 | m_iChild +--- CChild::$vftable@CBase1@: | &CChild_meta | 0 0 | &CChild::{dtor} 1 | &CChild::f1 2 | &CBase1::g1 CChild::$vftable@CBase2@: | -8 0 | &thunk: this-=8; goto CChild::{dtor} 1 | &CBase2::f2 2 | &CChild::g2
CChild 分別從CBase1 和 CBase 中繼承一個vfptr.
-
菱形結構繼承的類(非虛繼承)
class CBase { public: int m_iBase; public: virtual ~CBase(); virtual void f0(); virtual void g0(); virtual void h0(); }; class CChild1:public CBase { public: int m_iChild1; public: virtual ~CChild1(); virtual void f0(); virtual void h1(); }; class CChild2:public CBase { public: int m_iChild2; public: ~CChild2(); void g0(); void h1(); }; class CGrandChild :public CChild1, public CChild2 { public: int m_iGrandChild; public: virtual ~CGrandChild(); virtual void h0(); virtual void h1(); virtual void h2(); virtual void f0(); };
其類的布局如下:
class CGrandChild size(28): +--- | +--- (base class CChild1) | | +--- (base class CBase) 0 | | | {vfptr} 4 | | | m_iBase | | +--- 8 | | m_iChild1 | +--- | +--- (base class CChild2) | | +--- (base class CBase) 12 | | | {vfptr} 16 | | | m_iBase | | +--- 20 | | m_iChild2 | +--- 24 | m_iGrandChild +--- CGrandChild::$vftable@CChild1@: | &CGrandChild_meta | 0 0 | &CGrandChild::{dtor} 1 | &CGrandChild::f0 2 | &CBase::g0 3 | &CGrandChild::h0 4 | &CGrandChild::h1 5 | &CGrandChild::h2 CGrandChild::$vftable@CChild2@: | -12 0 | &thunk: this-=12; goto CGrandChild::{dtor} 1 | &thunk: this-=12; goto CGrandChild::f0 2 | &CChild2::g0 3 | &thunk: this-=12; goto CGrandChild::h0
這種繼承是有風險的,即通過CGrandChild 去訪問m_iBase 時,容易造成二義性,需要使用"pGrandChild->CChild::m_iBase" 這種方法去訪問。
為了避免這種問題,C++ 中有一種機制是虛繼承。
-
單一虛繼承
class CBase { public: int m_iBase; public: virtual ~CBase(); virtual void f0(); virtual void g0(); virtual void h0(); }; class CChild1: virtual public CBase { public: int m_iChild1; public: virtual ~CChild1(); virtual void f0(); virtual void h1(); };
其類的布局如下:
class CChild1 size(24): +--- 0 | {vfptr} 4 | {vbptr} 8 | m_iChild1 +--- 12 | (vtordisp for vbase CBase) +--- (virtual base CBase) 16 | {vfptr} 20 | m_iBase +--- CChild1::$vftable@CChild1@: | &CChild1_meta | 0 0 | &CChild1::h1 CChild1::$vbtable@: 0 | -4 1 | 12 (CChild1d(CChild1+4)CBase) CChild1::$vftable@CBase@: | -16 0 | &(vtordisp) CChild1::{dtor} 1 | &(vtordisp) CChild1::f0 2 | &CBase::g0 3 | &CBase::h0
從布局中看,發現多了一個vbptr 指針,則是一個指向基類的虛基類指針;在派生類和虛基類之間又多了“vtordisp for vbase CBase”,vtordisp 並不是每個虛繼承的派生類都會生成的,關於這部分可以參考MSDN 中 vtordisp;在vtordisp 后面則是虛基類的一個拷貝。
-
多重繼承的類(虛繼承)
class CChild1 { public: int m_iChild1; public: virtual ~CChild1(); virtual void f0(); virtual void h1(); }; class CChild2 { public: int m_iChild2; public: ~CChild2(); void g0(); void h1(); }; class CGrandChild :public CChild1, public CChild2 { public: int m_iGrandChild; public: virtual ~CGrandChild(); virtual void h0(); virtual void h1(); virtual void h2(); virtual void f0(); };
virtual public Child1, public CChild2
其類的布局如下:
class CGrandChild size(28): +--- 0 | {vfptr} | +--- (base class CChild2) 4 | | m_iChild2 | +--- 8 | {vbptr} 12 | m_iGrandChild +--- 16 | (vtordisp for vbase CChild1) +--- (virtual base CChild1) 20 | {vfptr} 24 | m_iChild1 +---
public Child1, virtual public CChild2
其類的布局如下:
class CGrandChild size(20): +--- | +--- (base class CChild1) 0 | | {vfptr} 4 | | m_iChild1 | +--- 8 | {vbptr} 12 | m_iGrandChild +--- +--- (virtual base CChild2) 16 | m_iChild2 +---
virtual public Child1, virtual public CChild2
class CGrandChild size(28): +--- 0 | {vfptr} 4 | {vbptr} 8 | m_iGrandChild +--- 12 | (vtordisp for vbase CChild1) +--- (virtual base CChild1) 16 | {vfptr} 20 | m_iChild1 +--- +--- (virtual base CChild2) 24 | m_iChild2 +---
通過上述虛繼承的情況來看,可以看出有虛繼承的派生類中,派生類和虛基類的數據是完全隔開的,先存放派生類自己的虛函數指針,虛基類指針和數據;然后有vtordisp 作為間隔;在存放虛基類的內容。
-
菱形結構繼承的類(虛繼承)
class CBase { public: int m_iBase; public: virtual ~CBase(); virtual void f0(); virtual void g0(); virtual void h0(); }; class CChild1 : virtual public CBase { public: int m_iChild1; public: virtual ~CChild1(); virtual void f0(); virtual void h1(); }; class CChild2 : virtual public CBase{ public: int m_iChild2; public: virtual ~CChild2(); virtual void g0(); virtual void h1(); }; class CGrandChild : public CChild1, public CChild2 { public: int m_iGrandChild; public: virtual ~CGrandChild(); virtual void h0(); virtual void h1(); virtual void h2(); virtual void f0(); };
其類的布局如下:
class CGrandChild size(40): +--- | +--- (base class CChild1) 0 | | {vfptr} 4 | | {vbptr} 8 | | m_iChild1 | +--- | +--- (base class CChild2) 12 | | {vfptr} 16 | | {vbptr} 20 | | m_iChild2 | +--- 24 | m_iGrandChild +--- 28 | (vtordisp for vbase CBase) +--- (virtual base CBase) 32 | {vfptr} 36 | m_iBase +---
有了上文的基礎,這個派生類的機構就不難理解了。