在許多筆試面試中都會涉及到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
+---
有了上文的基礎,這個派生類的機構就不難理解了。
