C++ 中類的內存布局


在許多筆試面試中都會涉及到sizeof 運算符的求值問題。

這類問題主要分四類:

  1. 基本數據類型,如int,bool,fload,long,long,int * 等,這一類比較簡單,但要注意x86和x64情況下的指針大小
  2. 枚舉 enum。這個類型網絡上有說是1-4個byte,根據最大值決定的;也有說是sizeof(int)。我這邊個人使用 visual studio 2015 獲得的結果是4個byte
  3. struct 和 union 組合類型。union 是取其中一個最大成員的size作為其size;struct 則要考慮對齊填充因素
  4. class 類型,class 就稍微復雜點,不僅僅要考慮對齊填充因素,還要考慮繼承,虛繼承,虛函數等因素。

下文主要講述class 的內存布局,稍帶介紹一下struct 的size。

 

struct 的內存布局:

struct 的內存對齊和填充概念學過C 的都應該知道一點。其實只要記住一個概念和三個原則就可以了:

一個概念:

  自然對齊:如果一個變量的內存地址正好位於它長度的整數倍,就被稱做自然對齊。

  如果不自然對齊,會帶來CPU存取數據時的性能損失。(PS:具體應該與CPU通過總線讀寫內存數據的細節相關,具體沒有細究)

三個原則:

  1. struct 的起始地址需要能夠被其成員中最寬的基本數據類型整除;
  2. struct 的size 也必須能夠被其成員中最寬的基本數據類型整除;
  3. 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
	+---

  有了上文的基礎,這個派生類的機構就不難理解了。

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM