[一起探索C++類內存分布](https://mp.weixin.qq.com/s/NyNHXIrEjG7vcAS8wZxU2Q)
[面試系列之C++的對象布局](https://mp.weixin.qq.com/s/sufz7wxC_rwc1q3FXY-QMQ) [C++ 虛函數表及多態內部原理詳解](https://mp.weixin.qq.com/s/ZvtEL-d0-2a2QXoAGXTCCA)
[C++ 的 6 種內存順序,你都知道嗎?](https://mp.weixin.qq.com/s/EIxTCpCXZ_GRj-tnxzCHVQ)
*****
本文總結了 C++ 繼承中的對象在內存中的布局,對于 C++ 眾多特性中不同對象的布局進行深入地詳細介紹。
## 前言
文章較長,而且內容相對來說比較枯燥,希望對 C++ 對象的內存布局、虛表指針、虛基類指針等有深入了解的朋友可以慢慢看。本文的結論都在 VS2013 上得到驗證。不同的編譯器在內存布局的細節上可能有所不同。文章如果有解釋不清、解釋不通或疏漏的地方,懇請指出。
### 何為 C++ 對象模型?
引用《深度探索 C++ 對象模型》這本書中的話:
> 有兩個概念可以解釋 C++ 對象模型:
>
> * 語言中直接支持面向對象程序設計的部分。
> * 對于各種支持的底層實現機制。
直接支持面向對象程序設計,包括了構造函數、析構函數、多態、虛函數等等,這些內容在很多書籍上都有討論,也是 C++ 最被人熟知的地方(特性)。而對象模型的底層實現機制卻是很少有書籍討論的。對象模型的底層實現機制并未標準化,不同的編譯器有一定的自由來設計對象模型的實現細節。在我看來,對象模型研究的是對象在存儲上的空間與時間上的更優,并對 C++ 面向對象技術加以支持,如以虛指針、虛表機制支持多態特性。
### 內容簡介
這篇文章主要來討論 C++ 對象在內存中的布局,屬于第二個概念的研究范疇,至于C++ 直接支持面向對象程序設計部分則不多講。文章主要內容如下:
* **虛函數表解析:**含有虛函數或其父類含有虛函數的類,編譯器都會為其添加一個虛函數表(vtptr)。先了解虛函數表的構成,有助對 C++ 對象模型的理解。
* **虛基類表解析:**虛繼承產生虛基類表(vbptr),虛基類表的內容與虛函數表完全不同,我們將在講解虛繼承時介紹虛繼承表。
* **C++ 對象模型概述:**介紹簡單對象模型、表格驅動對象模型,以及非繼承情況下的 C++ 對象模型。
* 繼承下的 C++ 對象模型,分析 C++ 類對象在下面情形中的內存布局:
* **單繼承:**子類單一繼承自父類,分析了子類重寫父類虛函數、子類定義了新的虛函數情況下子類對象內存布局
* **多繼承:**子類繼承于多個父類,分析了子類重寫父類虛函數、子類定義了新的虛函數情況下子類對象內存布局,同時分析了非虛繼承下的菱形繼承。
* **虛繼承:**分析了單一繼承下的虛繼承、多重基層下的虛繼承、重復繼承下的虛繼承。
* 理解對象的內存布局之后,我們可以分析一些問題:
* C++ 封裝帶來的布局成本是多大?
* 由空類組成的繼承層次中,每個類對象的大小是多大?
至于其他與內存有關的知識,我假設大家都有一定的了解,如內存對齊,指針操作等。本文初看可能晦澀難懂,要求讀者有一定的 C++ 基礎。
**注:部分程序截圖其中名稱錯誤,以文章內容為準。**
## 虛函數表
### 多態和虛函數表
C++ 中**虛函數的作用主要是為了實現多態**機制。多態,簡單來說,是指在繼承層次中,父類的指針可以具有多種形態——當它指向某個子類對象時,通過它能夠調用到子類的函數,而非父類的函數。
```
class Base {
virtual void print(void);
}
class Drive1 :public Base{
virtual void print(void);
}
class Drive2 :public Base{
virtual void print(void);
}
Base * ptr1 = new Base;
Base * ptr2 = new Drive1;
Base * ptr3 = new Drive2;
ptr1->print(); //調用Base::print()
prt2->print(); //調用Drive1::print()
prt3->print(); //調用Drive2::print()
```
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/virtual_derived.png "多態和虛表")
[多態和虛表](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/virtual_derived.png "多態和虛表")
這是一種運行期多態,即父類指針唯有在程序運行時才能知道所指的真正類型是什么。這種運行期決議,是通過虛函數表來實現的。
### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E8%AE%BF%E9%97%AE%E8%99%9A%E5%87%BD%E6%95%B0%E8%A1%A8 "訪問虛函數表")訪問虛函數表
如果我們豐富我們的 Base 類,使其擁有多個 virtual 函數:
```
class Base
{
public:
Base(int i) :baseI(i){};
virtual void print(void) { cout << "調用了虛函數Base::print()"; }
virtual void setI() { cout << "調用了虛函數Base::setI()"; }
virtual ~Base(){}
private:
int baseI;
};
```
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/base_class.png "使用指針訪問虛表")
[使用指針訪問虛表](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/base_class.png "使用指針訪問虛表")
C++中,當一個類本身定義了虛函數,或其父類有虛函數時,為了支持多態機制,編譯器將為該類添加一個**虛函數表指針(vtptr),虛表指針一般都放在對象內存布局的第一個位置上**,這是為了保證在多層繼承或多重繼承的情況下能以最高效率取到虛函數表。當 vtptr 位于對象內存最前面時,**對象的地址即為虛函數表指針地址**,因此我們可以很容易取得虛函數表指針的地址:
```
Base b(1000);
int *vtptrAdree = (int *)(&b);
cout << "虛函數表指針(vtptr)的地址是:\t" << vtptrAdree << endl;
```
我們運行代碼出結果:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/result1.jpg)
我們強行把類對象的地址轉換為`int*`類型,取得了虛表指針的地址。**虛表指針指向虛函數表,虛函數表中存儲的是一系列虛函數的地址,虛函數地址出現的順序與類中虛函數聲明的順序一致**。對虛函數表指針取內容值,可以得到虛函數表的指針(vfptr,內存中這個地址存放的就是第一個虛函數的地址),也即是虛函數表中第一個虛函數的地址:
```
typedef void(*Fun)(void);
Fun vfunc = (Fun)*((int *)*(int*)(&b));
cout << "第一個虛函數的地址是:" << (int *)*(int*)(&b) << endl;
cout << "通過地址,調用虛函數Base::print():";
vfunc();
```
* 我們把虛表指針的值取出來:`*(int*)(&b)`,它是一個地址,虛函數表的地址
* 把虛函數表的地址強制轉換成int指針:`int* : (int*) *(int*)( &b )`
* 再把它轉化成我們`Fun`函數指針類型 :`(Fun)*(int*)*(int*)(&b)`
注意這里,int 和指針的長度一般都是一樣的,所以將地址指針轉換為 int 指針。這樣,我們就取得了類中的第一個虛函數,我們可以通過函數指針訪問它。運行結果:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/result2.jpg)
同理,第二個虛函數`setI()`的地址為:
```
(int *)(*(int*)(&b)+1)
```
同樣可以通過函數指針訪問它。
目前為止,我們知道了類中虛表指針 vtptr 以及 虛表中虛函數指針 vfptr 的由來,知道了虛函數表中的內容,以及如何通過指針訪問虛函數表。下面的文章中將常使用指針訪問對象內存來驗證我們的 C++ 對象模型,以及討論在各種繼承情況下虛表指針的變化,先把這部分的內容消化完再接著看下面的內容。
## [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#C-%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B "C++ 對象模型")C++ 對象模型
在 C++ 面向對象的類中,有兩種數據成員:static 和 non-static,以及三種類成員函數:static、non-static 和 virtual,如下圖所示。
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/class_members.png)
現在我們有一個類 Base,它包含了上面這 5 種類型的數據或函數:
```
class Base
{
public:
Base(int i) :baseI(i){};
int getI(){ return baseI; }
static void countI(){};
virtual void print(void){ cout << "Base::print()"; }
virtual ~Base(){}
private:
int baseI;
static int baseS;
};
```
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/base_class1.jpg)
那么,這個類在內存中將被如何表示?5 種數據都是連續存放的嗎?如何布局才能支持 C++ 多態? 我們的 C++ 標準與編譯器將如何塑造出各種數據成員與成員函數呢?我們將分章節進行討論。
## [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E9%9D%9E%E7%BB%A7%E6%89%BF%E4%B8%8B%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B "非繼承下的對象模型")非繼承下的對象模型
概述:在此模型下,non-static 數據成員被置于每一個對象中,而 static 數據成員被置于對象之外。static 與 non-static 函數也都放在對象之外,而對于 virtual 函數,則通過虛函數表和虛指針來支持,具體如下:
* 每個**類**生成一個表格,稱為**虛函數表或者虛表**(virtual table,簡稱 vt)。虛表中存放著一堆指針,這些指針指向該類的每一個虛函數。虛表中的函數地址將按**聲明時**的順序排列(**虛析構函數永遠是第一個**),不過當子類有多個重載函數時例外,后面會討論。
* 每個**對象**都擁有一個**虛表指針**(vtptr),由編譯器為其生成。虛表指針的設定與重置皆由類的復制控制(也即是構造函數、析構函數、賦值操作符)來完成。vtptr 的位置為編譯器決定,傳統上它被放在所有顯示聲明的成員之后,不過現在許多**編譯器把 vtptr 放在一個對象的在內存中的最前端**。關于數據成員布局的內容,在后面會詳細分析。
* 另外,虛函數表的前面設置了一個指向`type_info`的指針,用以支持 RTTI(Run Time Type Identification,運行時類型識別)。RTTI 是為多態而生成的信息,包括對象繼承關系,對象本身的描述等,**只有具有虛函數的對象在會生成,因為是放在虛函數表的前面**。
在此模型下,Base 的對象模型如圖:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/object_model1.png)
先在 VS2013 上驗證類對象的布局:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/result3.png)
可見對象 b 含有一個 vtptr,并且只有 non-static 數據成員被放置于對象內。我們展開 vt:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/result4.png)
vt 中有兩個指針類型的數據(地址),第一個指向了 Base 類的析構函數,第二個指向了Base的虛函數`print`,**順序與聲明順序相同**。這與上述的 C++ 對象模型相符合。也可以通過代碼來進行驗證:
```
void testBase( Base&p)
{
cout << "對象的內存起始地址:" << &p << endl;
cout << "type_info信息:" << endl;
RTTICompleteObjectLocator str = *((RTTICompleteObjectLocator*)*((int*)*(int*)(&p) - 1));
string classname(str.pTypeDescriptor->name);
classname = classname.substr(4, classname.find("@@") - 4);
cout << "根據type_info信息輸出類名:"<< classname << endl;
cout << "虛函數表地址:" << (int *)(&p) << endl;
//驗證虛表
cout << "虛函數表第一個函數的地址:" << (int *)*((int*)(&p)) << endl;
cout << "析構函數的地址:" << (int* )*(int *)*((int*)(&p)) << endl;
cout << "虛函數表中,第二個虛函數即print()的地址:" << ((int*)*(int*)(&p) + 1) << endl;
//通過地址調用虛函數print()
typedef void(*Fun)(void);
Fun IsPrint=(Fun)* ((int*)*(int*)(&p) + 1);
cout << endl;
cout<<"調用了虛函數";
IsPrint(); //若地址正確,則調用了Base類的虛函數print()
cout << endl;
//輸入static函數的地址
p.countI();//先調用函數以產生一個實例
cout << "static函數countI()的地址:" << p.countI << endl;
//驗證non-static數據成員
cout << "推測non-static數據成員baseI的地址:" << (int *)(&p) + 1 << endl;
cout << "根據推測出的地址,輸出該地址的值:" << *((int *)(&p) + 1) << endl;
cout << "Base::getI():" << p.getI() << endl;
}
Base b(1000);
testBase(b);
```
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/result5.png)
**結果分析:**
* 通過`(int *)(&p)`取得虛函數表的地址。
* `type_info`信息的確存在于虛表的前一個位置。通過`((int*)*(int*)(&p) - 1)`取得`type_info`信息的指針,并成功獲得類的名稱的 Base。
* 虛函數表的第一個函數是析構函數。
* 虛函數表的第二個函數是虛函數`print`,取得地址后通過地址調用它(而非通過對象),驗證正確。
* 對象內存中虛表指針的下一個位置為 non-static 數據成員`baseI`。
* 可以看到,static 成員函數的地址段位與虛表指針、`baseI`的地址段位不同。
好的,至此我們了解了非繼承下類對象五種數據在內存上的布局,也知道了在每一個虛函數表前都有一個指針指向`type_info`,負責對 RTTI 的支持。而加入繼承后類對象在內存中該如何表示呢?
## [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E7%BB%A7%E6%89%BF%E4%B8%8B%E7%9A%84%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B "繼承下的對象模型")繼承下的對象模型
### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E5%8D%95%E7%BB%A7%E6%89%BF "單繼承")單繼承
在之前的基礎上,如果我們定義了派生類:
```
class Derive : public Base
{
public:
Derive(int d) :Base(1000), DeriveI(d){};
//overwrite父類虛函數
virtual void print(void) { cout << "Drive::Drive_print()" ; }
// Derive聲明的新的虛函數
virtual void Drive_print() { cout << "Drive::Drive_print()" ; }
virtual ~Derive(){}
private:
int DeriveI;
};
```
繼承類圖為:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/base_class2.png)
一個派生類如何在機器層面上塑造其父類的實例呢?在 C++ 對象模型中:
* 對于一般繼承(這個一般是相對于虛繼承而言),若子類重寫(overwrite)了父類的虛函數,則**子類虛函數將覆蓋虛表中對應的父類虛函數**(注意子類與父類擁有各自的一個虛函數表);若子類并沒有 overwrite 父類虛函數,而是聲明了自己新的虛函數,則**該虛函數地址將擴充到虛函數表最后**(在 VS2013 中無法通過監視看到擴充的結果,不過我們通過取地址的方法可以做到,子類新的虛函數確實在虛函數表末端)。
* 對于虛繼承,若子類 overwrite 父類虛函數,同樣地將覆蓋從父類繼承過來的虛函數表中的對應位置,**若子類聲明了自己新的虛函數,則編譯器將為子類增加一個新的虛表指針 vtptr,這與一般繼承不同**。
* 子類虛析構函數會覆蓋掉虛函數表中的分類的虛析構函數。
* 其實這個圖可以表明繼承關系中是如何產生類的對象中,我們知道,**C++ 繼承時,先調用父類構造函數生成一個父類對象,如圖 Base 類實例,然后調用子類的構造函數生成一個子類的對象,其實這個對象在父類的對象上進行擴充**。
* 另一方面,子類繼承了父類全部的成員,包括 private,只是子類沒有訪問權限。所以不是沒有,是不能訪問。
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/object_model2.png)
我們使用代碼來驗證以上模型:
```
typedef void(*Fun)(void);
int main()
{
Derive d(2000);
//[0]
cout << "[0]Base::vtptr";
cout << "\t地址:" << (int *)(&d) << endl;
//vtptr[0]
cout << " [0]";
Fun fun1 = (Fun)*((int *)*((int *)(&d)));
fun1();
cout << "\t地址:\t" << *((int *)*((int *)(&d))) << endl;
//vtptr[1]析構函數無法通過地址調用,故手動輸出
cout << " [1]" << "Derive::~Derive" << endl;
//vtptr[2]
cout << " [2]";
Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2);
fun2();
cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 2) << endl;
//[1]
cout << "[2]Base::baseI=" << *(int*)((int *)(&d) + 1);
cout << "\t地址:" << (int *)(&d) + 1;
cout << endl;
//[2]
cout << "[2]Derive::DeriveI=" << *(int*)((int *)(&d) + 2);
cout << "\t地址:" << (int *)(&d) + 2;
cout << endl;
getchar();
}
```
運行結果:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/result6.png)
這個結果與我們的對象模型符合。
#### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E7%BB%A7%E6%89%BF%E5%AF%BC%E8%87%B4%E9%87%8D%E8%BD%BD%E5%87%BD%E6%95%B0%E7%9A%84%E9%9A%90%E8%97%8F "繼承導致重載函數的隱藏")繼承導致重載函數的隱藏
* 首先要說明的是,**重載只能發生在同一個類中**,子類和父類之間的同名函數(參數列表),無法構成重載,**子類的同名函數(無論參數列表是否相同),會覆蓋所有父類(多繼承情況下)的所有同名函數(包括虛函數)**。
* 因為這種特性,也就導致了子類的同名函數會隱藏父類的重載函數。如果想用父類的重載函數,可以通過`using Base::foo`來聲明繼承所有的重載函數,然后重寫特定參數列表的函數。如果不需要重寫,則也可以通過使用父類作用域來顯式地調用父類的重載函數。(可以參見《effecive C++》)
### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E5%A4%9A%E7%BB%A7%E6%89%BF "多繼承")多繼承
#### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E4%B8%80%E8%88%AC%E7%9A%84%E5%A4%9A%E9%87%8D%E7%BB%A7%E6%89%BF "一般的多重繼承")一般的多重繼承
單繼承中(一般繼承),子類會擴展父類的虛函數表。在多繼承中,子類含有多個父類的子對象,該往哪個父類的虛函數表擴展呢?當子類 overwrite 了父類的函數,需要覆蓋多個父類的虛函數表嗎?
* 子類繼承所有父類的虛函數表,因此子類中擁有多個虛函數表指針和虛函數表。
* 子類的新增的虛函數被放在聲明的第一個基類的虛函數表中。
* overwrite 時,所有基類的同名函數都被子類的同名函數覆蓋。保證了父類指針指向子類對象時,總是能夠調用到真正的函數。
* 內存布局中,父類按照其聲明順序排列。
```
class Base
{
public:
Base(int i) :baseI(i){};
virtual ~Base(){}
int getI(){ return baseI; }
static void countI(){};
virtual void print(void){ cout << "Base::print()"; }
private:
int baseI;
static int baseS;
};
class Base_2
{
public:
Base_2(int i) :base2I(i){};
virtual ~Base_2(){}
int getI(){ return base2I; }
static void countI(){};
virtual void print(void){ cout << "Base_2::print()"; }
private:
int base2I;
static int base2S;
};
class Drive_multyBase :public Base, public Base_2
{
public:
Drive_multyBase(int d) :Base(1000), Base_2(2000) ,Drive_multyBaseI(d){};
virtual void print(void){ cout << "Drive_multyBase::print" ; }
virtual void Drive_print(){ cout << "Drive_multyBase::Drive_print" ; }
private:
int Drive_multyBaseI;
};
```
繼承類圖為:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/class1.png)
此時 Drive\_multyBase 的對象模型是這樣的:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/object_model3.png)
我們使用代碼驗證:
```
typedef void(*Fun)(void);
int main()
{
Drive_multyBase d(3000);
//[0]
cout << "[0]Base::vtptr";
cout << "\t地址:" << (int *)(&d) << endl;
//vtptr[0]析構函數無法通過地址調用,故手動輸出
cout << " [0]" << "Derive::~Derive" << endl;
//vtptr[1]
cout << " [1]";
Fun fun1 = (Fun)*((int *)*((int *)(&d))+1);
fun1();
cout << "\t地址:\t" << *((int *)*((int *)(&d))+1) << endl;
//vtptr[2]
cout << " [2]";
Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2);
fun2();
cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 2) << endl;
//[1]
cout << "[1]Base::baseI=" << *(int*)((int *)(&d) + 1);
cout << "\t地址:" << (int *)(&d) + 1;
cout << endl;
//[2]
cout << "[2]Base_::vtptr";
cout << "\t地址:" << (int *)(&d)+2 << endl;
//vtptr[0]析構函數無法通過地址調用,故手動輸出
cout << " [0]" << "Drive_multyBase::~Derive" << endl;
//vtptr[1]
cout << " [1]";
Fun fun4 = (Fun)*((int *)*((int *)(&d))+1);
fun4();
cout << "\t地址:\t" << *((int *)*((int *)(&d))+1) << endl;
//[3]
cout << "[3]Base_2::base2I=" << *(int*)((int *)(&d) + 3);
cout << "\t地址:" << (int *)(&d) + 3;
cout << endl;
//[4]
cout << "[4]Drive_multyBase::Drive_multyBaseI=" << *(int*)((int *)(&d) + 4);
cout << "\t地址:" << (int *)(&d) + 4;
cout << endl;
getchar();
}
```
運行結果:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/result7.png)
#### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E8%8F%B1%E5%BD%A2%E7%BB%A7%E6%89%BF "菱形繼承")菱形繼承
菱形繼承也稱為鉆石型繼承或重復繼承,它指的是基類被某個派生類簡單重復繼承了多次。這樣,派生類對象中擁有多份基類實例(這會帶來一些問題)。為了方便敘述,我們不使用上面的代碼了,而重新寫一個重復繼承的繼承層次:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/class2.png)
```
class B
{
public:
int ib;
public:
B(int i=1) :ib(i){}
virtual void f() { cout << "B::f()" << endl; }
virtual void Bf() { cout << "B::Bf()" << endl; }
};
class B1 : public B
{
public:
int ib1;
public:
B1(int i = 100 ) :ib1(i) {}
virtual void f() { cout << "B1::f()" << endl; }
virtual void f1() { cout << "B1::f1()" << endl; }
virtual void Bf1() { cout << "B1::Bf1()" << endl; }
};
class B2 : public B
{
public:
int ib2;
public:
B2(int i = 1000) :ib2(i) {}
virtual void f() { cout << "B2::f()" << endl; }
virtual void f2() { cout << "B2::f2()" << endl; }
virtual void Bf2() { cout << "B2::Bf2()" << endl; }
};
class D : public B1, public B2
{
public:
int id;
public:
D(int i= 10000) :id(i){}
virtual void f() { cout << "D::f()" << endl; }
virtual void f1() { cout << "D::f1()" << endl; }
virtual void f2() { cout << "D::f2()" << endl; }
virtual void Df() { cout << "D::Df()" << endl; }
};
```
這時,根據單繼承,我們可以分析出 B1,B2 類繼承于 B 類時的內存布局。又根據一般多繼承,我們可以分析出 D 類的內存布局。我們可以得出 D 類子對象的內存布局如下圖:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/object_model4.png)
D 類對象內存布局中,圖中青色表示 b1 類子對象實例,黃色表示 b2 類子對象實例,灰色表示 D 類子對象實例。從圖中可以看到,由于 D 類間接繼承了 B 類兩次,導致 D 類對象中含有兩個 B 類的數據成員 ib,一個屬于來源 B1 類,一個來源 B2 類。這樣不僅增大了空間,更重要的是引起了程序歧義:
```
D d;
d.ib =1 ; //二義性錯誤,調用的是B1的ib還是B2的ib?
d.B1::ib = 1; //正確
d.B2::ib = 1; //正確
```
盡管我們可以通過明確指明調用路徑以消除二義性,但二義性的潛在性還沒有消除,我們可以通過虛繼承來使 D 類只擁有一個 ib 實體。
### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E8%99%9A%E7%BB%A7%E6%89%BF "虛繼承")虛繼承
虛繼承解決了菱形繼承中最派生類擁有多個間接父類實例的情況。虛繼承的派生類的內存布局與普通繼承很多不同,主要體現在:
* 虛繼承的子類,如果本身定義了新的虛函數,則編譯器為其生成一個虛函數表指針(vtptr)以及一張虛函數表。該 vtptr 位于對象內存最前面(非虛繼承則是直接擴展父類虛函數表)。
* 虛繼承的子類也單獨**保留了父類的 vtptr 與虛函數表**。這部分內容與子類內容以一個四字節的 0 來分界。
* 這兩條規則表面,如果虛繼承的子類定義了新的虛函數,而且父類中已經有了虛函數,則子類對象中**擁有多個虛表指針**。
* 虛繼承的子類對象中,含有 4 個字節的虛基類表指針偏移值。
為了分析最后的菱形繼承,我們還是先從單虛繼承繼承開始。
#### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E8%99%9A%E5%9F%BA%E7%B1%BB%E8%A1%A8 "虛基類表")虛基類表
在 C++ 對象模型中,虛繼承而來的子類會**生成一個隱藏的虛基類指針(vbptr)**,在 Microsoft Visual C++ 中,虛基類表指針總是在虛函數表指針(vtptr)之后,因而,對某個類實例來說,如果它有虛基類指針,那么虛基類指針可能在實例的 0 字節偏移處(該類沒有 vtptr 時,vbptr 就處于類實例內存布局的最前面),也可能在類實例的4字節偏移處(有 vtptr)。
虛基類表的一些特性:
* 一個類的虛基類指針指向虛基類表
* 與虛函數表一樣,虛基類表也由多個條目組成,條目中存放的是偏移值。**第一個條目存放虛基類表指針(vbptr)所在地址到該類內存首地址的偏移值**,由第一段的分析我們知道,這個偏移值為 0(類沒有 vtptr)或者 -4(類有虛函數,此時有 vtptr)。
* 虛基類表的第二、第三…個條目依次為**該類的最左虛繼承父類、次左虛繼承父類…的內存地址相對于虛基類表指針的偏移值。**
我們通過一張圖來更好地理解。
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/vtptr_in_class.png)
#### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E7%AE%80%E5%8D%95%E8%99%9A%E7%BB%A7%E6%89%BF "簡單虛繼承")簡單虛繼承
如果我們的 B1 類虛繼承于 B 類:
```
//類的內容與前面相同
class B{...}
class B1 : virtual public B
```
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/class3.png)
根據我們前面對虛繼承的派生類的內存布局的分析,B1 類的對象模型應該是這樣的:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/object_model5.png)
**注意上圖,子類對象中有兩個虛表指針,分別是子類的虛表指針和父類的虛表指針。如果子類重寫父類的虛函數,則將父類的虛表中的虛函數地址替換為子類的虛函數地址**
我們通過指針訪問 B1 類對象的內存,以驗證上面的 C++ 對象模型:
```
int main()
{
B1 a;
cout <<"B1對象內存大小為:"<< sizeof(a) << endl;
//取得B1的虛函數表
cout << "[0]B1::vtptr";
cout << "\t地址:" << (int *)(&a)<< endl;
//輸出虛表B1::vtptr中的函數
for (int i = 0; i<2;++ i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun)*((int *)*(int *)(&a) + i);
fun1();
cout << "\t地址:\t" << *((int *)*(int *)(&a) + i) << endl;
}
//[1]
cout << "[1]vbptr " ;
cout<<"\t地址:" << (int *)(&a) + 1<<endl; //虛表指針的地址
//輸出虛基類指針條目所指的內容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int *)((int *)*((int *)(&a) + 1) + i);
cout << endl;
}
//[2]
cout << "[2]B1::ib1=" << *(int*)((int *)(&a) + 2);
cout << "\t地址:" << (int *)(&a) + 2;
cout << endl;
//[3]
cout << "[3]值=" << *(int*)((int *)(&a) + 3);
cout << "\t\t地址:" << (int *)(&a) + 3;
cout << endl;
//[4]
cout << "[4]B::vtptr";
cout << "\t地址:" << (int *)(&a) +3<< endl;
//輸出B::vtptr中的虛函數
for (int i = 0; i<2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun)*((int *)*((int *)(&a) + 4) + i);
fun1();
cout << "\t地址:\t" << *((int *)*((int *)(&a) + 4) + i) << endl;
}
//[5]
cout << "[5]B::ib=" << *(int*)((int *)(&a) + 5);
cout << "\t地址: " << (int *)(&a) + 5;
cout << endl;
}
```
**運行結果**:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/result8.png)
這個結果與我們的 C++ 對象模型圖完全符合。這時我們可以來分析一下虛表指針的第二個條目值 12 的具體來源了,回憶上文講到的:
> 第二、第三…個條目依次為該類的最左虛繼承父類、次左虛繼承父類…的內存地址相對于虛基類表指針的偏移值。
在我們的例子中,也就是 B 類實例內存地址相對于 vbptr 的偏移值,也即是:\[4\]-\[1\]的偏移值,結果即為 12,從地址上也可以計算出來:007CFDFC-007CFDF4 結果的十進制數正是 12。現在,我們對虛基類表的構成應該有了一個更好的理解。
#### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E8%8F%B1%E5%BD%A2%E8%99%9A%E7%BB%A7%E6%89%BF "菱形虛繼承")菱形虛繼承
如果我們有如下繼承層次:
```
class B{...}
class B1: virtual public B{...}
class B2: virtual public B{...}
class D : public B1,public B2{...}
```
類圖如下所示:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/class4.png)
菱形虛擬繼承下,最派生類 D 類的對象模型又有不同的構成了。在D類對象的內存構成上,有以下幾點:
* 在 D 類對象內存中,基類出現的順序是:先是 B1(最左父類),然后是 B2(次左父類),最后是 B(虛祖父類)。
* D 類對象的數據成員 id 放在 B 類前面,兩部分數據依舊以 0 來分隔。
* **編譯器沒有為 D 類生成一個它自己的 vtptr,而是覆蓋并擴展了最左父類的虛基類表,與簡單繼承的對象模型相同**。
* 超類 B 的內容放到了 D 類對象內存布局的最后。
菱形虛擬繼承下的C++對象模型為:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/object_model6.png)
下面使用代碼加以驗證:
```
int main()
{
D d;
cout << "D對象內存大小為:" << sizeof(d) << endl;
//取得B1的虛函數表
cout << "[0]B1::vtptr";
cout << "\t地址:" << (int *)(&d) << endl;
//輸出虛表B1::vtptr中的函數
for (int i = 0; i<3; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun)*((int *)*(int *)(&d) + i);
fun1();
cout << "\t地址:\t" << *((int *)*(int *)(&d) + i) << endl;
}
//[1]
cout << "[1]B1::vbptr ";
cout << "\t地址:" << (int *)(&d) + 1 << endl; //虛表指針的地址
//輸出虛基類指針條目所指的內容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int *)((int *)*((int *)(&d) + 1) + i);
cout << endl;
}
//[2]
cout << "[2]B1::ib1=" << *(int*)((int *)(&d) + 2);
cout << "\t地址:" << (int *)(&d) + 2;
cout << endl;
//[3]
cout << "[3]B2::vtptr";
cout << "\t地址:" << (int *)(&d) + 3 << endl;
//輸出B2::vtptr中的虛函數
for (int i = 0; i<2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun)*((int *)*((int *)(&d) + 3) + i);
fun1();
cout << "\t地址:\t" << *((int *)*((int *)(&d) + 3) + i) << endl;
}
//[4]
cout << "[4]B2::vbptr ";
cout << "\t地址:" << (int *)(&d) + 4 << endl; //虛表指針的地址
//輸出虛基類指針條目所指的內容
for (int i = 0; i < 2; i++)
{
cout << " [" << i << "]";
cout << *(int *)((int *)*((int *)(&d) + 4) + i);
cout << endl;
}
//[5]
cout << "[5]B2::ib2=" << *(int*)((int *)(&d) + 5);
cout << "\t地址: " << (int *)(&d) + 5;
cout << endl;
//[6]
cout << "[6]D::id=" << *(int*)((int *)(&d) + 6);
cout << "\t地址: " << (int *)(&d) + 6;
cout << endl;
//[7]
cout << "[7]值=" << *(int*)((int *)(&d) + 7);
cout << "\t\t地址:" << (int *)(&d) + 7;
cout << endl;
//間接父類
//[8]
cout << "[8]B::vtptr";
cout << "\t地址:" << (int *)(&d) + 8 << endl;
//輸出B::vtptr中的虛函數
for (int i = 0; i<2; ++i)
{
cout << " [" << i << "]";
Fun fun1 = (Fun)*((int *)*((int *)(&d) + 8) + i);
fun1();
cout << "\t地址:\t" << *((int *)*((int *)(&d) + 8) + i) << endl;
}
//[9]
cout << "[9]B::id=" << *(int*)((int *)(&d) + 9);
cout << "\t地址: " << (int *)(&d) +9;
cout << endl;
getchar();
}
```
查看運行的結果:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/result10.png)
## [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E5%86%85%E5%AD%98%E5%AF%B9%E8%B1%A1%E6%A8%A1%E5%9E%8B%E7%9A%84%E6%80%BB%E7%BB%93 "內存對象模型的總結")內存對象模型的總結
### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#C-%E5%B0%81%E8%A3%85%E5%B8%A6%E6%9D%A5%E7%9A%84%E5%B8%83%E5%B1%80%E6%88%90%E6%9C%AC%E6%98%AF%E5%A4%9A%E5%A4%A7%EF%BC%9F "C++ 封裝帶來的布局成本是多大?")C++ 封裝帶來的布局成本是多大?
在 C 語言中,“數據”和“處理數據的操作(函數)”是分開來聲明的,也就是說,語言本身并沒有支持“數據和函數”之間的關聯性。在 C++ 中,我們通過類來將屬性與操作綁定在一起,稱為 ADT,抽象數據結構。C 語言中使用 struct(結構體)來封裝數據,使用函數來處理數據。舉個例子,如果我們定義了一個 struct Point3d 如下:
```
typedef struct Point3d
{
float x;
float y;
float z;
} Point3d;
```
為了打印這個 Point3d,我們可以定義一個函數:
```
void Point3d_print(const Point3d *pd)
{
printf("(%f,%f,%f)",pd->x,pd->y,pd_z);
}
```
而在 C++ 中,我們更傾向于定義一個 Point3d 類,以 ADT 來實現上面的操作:
```
class Point3d
{
public:
point3d (float x = 0.0,float y = 0.0,float z = 0.0)
: _x(x), _y(y), _z(z){}
float x() const {return _x;}
float y() const {return _y;}
float z() const {return _z;}
private:
float _x;
float _y;
float _z;
};
```
```
inline ostream&
operator<<(ostream &os, const Point3d &pt)
{
os<<"("<<pr.x()<<","
<<pt.y()<<","<<pt.z()<<")";
}
```
看到這段代碼,很多人第一個疑問可能是:加上了封裝,布局成本增加了多少?答案是**`class Point3d`并沒有增加成本**。學過了C++對象模型,我們知道,**Point3d 類對象的內存中,只有三個數據成員**。上面的類聲明中,三個數據成員直接內含在每一個 Point3d 對象中,而成員函數雖然在類中聲明,卻不出現在類對象(object)之中,這些函數(non-inline)屬于類而不屬于類對象,只會為類產生唯一的函數實例。所以,Point3d 的封裝并沒有帶來任何空間或執行期的效率影響。而在下面這種情況下,C++ 的封裝額外成本才會顯示出來:
* 虛函數機制(virtual function),用以支持執行期綁定,實現多態。
* 虛基類(virtual base class),虛繼承關系產生虛基類,用于在多重繼承下保證基類在子類中擁有唯一實例。
不僅如此,Point3d 類數據成員的內存布局與 C 語言的結構體 Point3d 成員內存布局是相同的。C++ 中處在同一個訪問標識符(指 public、private、protected)下的聲明的數據成員,在內存中必定保證以其聲明順序出現。而處于不同訪問標識符聲明下的成員則無此規定。對于 Point3d 類來說,它的三個數據成員都處于 private 下,在內存中一起聲明順序出現。我們可以做下實驗:
```
void TestPoint3Member(const Point3d& p)
{
cout << "推測_x的地址是:" << (float *) (&p) << endl;
cout << "推測_y的地址是:" << (float *) (&p) + 1 << endl;
cout << "推測_z的地址是:" << (float *) (&p) + 2 << endl;
cout << "根據推測出的地址輸出_x的值:" << *((float *)(&p)) << endl;
cout << "根據推測出的地址輸出_y的值:" << *((float *)(&p)+1) << endl;
cout << "根據推測出的地址輸出_z的值:" << *((float *)(&p)+2) << endl;
}
```
```
//測試代碼
Point3d a(1,2,3);
TestPoint3Member(a);
```
運行結果:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/result11.png)
從結果可以看到,`_x`,`_y`,`_z`三個數據成員在內存中緊挨著。
總結一下就是:**不考慮虛函數與虛繼承,當數據都在同一個訪問標識符下,C++ 的類與 C 語言的結構體在對象大小和內存布局上是一致的,C++ 的封裝并沒有帶來空間時間上的影響**。
### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E4%B8%8B%E9%9D%A2%E8%BF%99%E4%B8%AA%E7%A9%BA%E7%B1%BB%E6%9E%84%E6%88%90%E7%9A%84%E7%BB%A7%E6%89%BF%E5%B1%82%E6%AC%A1%E4%B8%AD%EF%BC%8C%E6%AF%8F%E4%B8%AA%E7%B1%BB%E7%9A%84%E5%A4%A7%E5%B0%8F%E6%98%AF%E5%A4%9A%E5%B0%91%EF%BC%9F "下面這個空類構成的繼承層次中,每個類的大小是多少?")下面這個空類構成的繼承層次中,每個類的大小是多少?
```
class B{};
class B1 :public virtual B{};
class B2 :public virtual B{};
class D : public B1, public B2{};
int main()
{
B b;
B1 b1;
B2 b2;
D d;
cout << "sizeof(b)=" << sizeof(b)<<endl;
cout << "sizeof(b1)=" << sizeof(b1) << endl;
cout << "sizeof(b2)=" << sizeof(b2) << endl;
cout << "sizeof(d)=" << sizeof(d) << endl;
getchar();
}
```
輸出結果:
[](https://murphypei.github.io/images/posts/cplusplus/object_memory_model/result12.png)
解析:
* 編譯器為空類安插 1 字節的char,以使該類對象在內存得以配置一個地址。
* b1 虛繼承于 b,編譯器為其安插一個 4 字節的**虛基類表指針(32為機器)**,此時 b1 已不為空,編譯器不再為其安插 1 字節的 char(優化)。
* d 含有來自 b1 與 b2 兩個父類的兩個虛基類表指針。大小為 8 字節。
#### [](https://murphypei.github.io/blog/2017/03/cpp-object-memory-model.html#%E5%8F%82%E8%80%83%E8%B5%84%E6%96%99 "參考資料")參考資料
* [http://www.cnblogs.com/QG-whz/p/4909359.html](http://www.cnblogs.com/QG-whz/p/4909359.html)
- C++基礎
- 什么是 POD 數據類型?
- 面向對象三大特性五大原則
- 低耦合高內聚
- C++類型轉換
- c++仿函數
- C++仿函數了解一下?
- C++對象內存模型
- C++11新特性
- 智能指針
- 動手實現C++的智能指針
- C++ 智能指針 shared_ptr 詳解與示例
- 現代 C++:一文讀懂智能指針
- Lamda
- c++11多線程
- std::thread
- std::async
- std::promise
- std::future
- C++11 的內存模型
- 初始化列表
- std::bind
- std::tuple
- auto自動類型推導
- 可變參數模板
- 右值引用與移動語義
- 完美轉發
- 基于范圍的for循環
- C++11之POD類型
- std::enable_if
- C++14/17
- C++20
- 協成
- 模塊
- Ranges
- Boost
- boost::circular_buffer
- 使用Boost.Asio編寫通信程序
- Boost.Asio C++ 網絡編程
- 模板
- 模板特化/偏特化
- C++模板、類模板、函數模板詳解都在這里了
- 泛化之美--C++11可變模版參數的妙用
- 模板元編程
- 這是我見過最好的模板元編程文章!