在本文中,我們將繼續(xù)深入研究C ++運(yùn)行時動態(tài)調(diào)度的相關(guān)話題。 到目前為止,我們已經(jīng)驗證gdb不會Trivial類型的類和默認(rèn)構(gòu)造函數(shù)創(chuàng)建虛擬表,我們在本篇將闡述非虛擬派生類它們的構(gòu)造和內(nèi)存布局,這是正確區(qū)分虛擬類和非虛擬類在內(nèi)存分配方面的差異的前提。
示例導(dǎo)入
讓我引入兩個新的類。一個基類Person和一個從Person繼承的派生類Student。請注意,兩者都使用其方法who()的相同簽名。
- 有了動態(tài)調(diào)度后,將查詢vtable并調(diào)用適當(dāng)?shù)姆椒ā?/li>
- 如果沒有動態(tài)分配,則將調(diào)用與對象的指針類型匹配的方法。 Student *m將調(diào)用Student:: who(),而Person *p將調(diào)用Person::who()。讓我們用匯編代碼驗證一下。
#include <stdio.h>
class Person {
public:
Person() {}
void who() {
printf("I am a human!!\n");
}
};
class Student: public Person{
public:
Student() {}
void career() {
printf("I am a student!!\n");
}
};
int main() {
Student *m = new Student();
m->who();
Person *p = m;
p->who();
}
我們看到who()方法已經(jīng)在編譯時已經(jīng)植入Student類的上下文,從callq 0400658 <_ZN7Student3whoEv>,這條指令表明運(yùn)行時決策是不可能的。 指針的類型在編譯時是已知的,編譯器會選擇正確的who()方法調(diào)用,同理callq 0x400626 <_ZN6Person3whoEv>,也是一條編譯時的靜態(tài)指令。下圖說明類一切:

但是我們也可以通過定義基類的名稱空間來調(diào)用基類的方法,如下所示
m->Person::who()
內(nèi)存布局
為了降低問題的復(fù)雜性,我將使用此代碼的一些細(xì)微變化,去掉一些who方法:
class Person {
public:
int age=6;
Person() {}
};
class Student: public Person{
public:
int idNo=1000;
Student() {}
};
為了了解虛擬表的隱藏位置,讓我們首先檢查一下簡單繼承層次結(jié)構(gòu)的內(nèi)存布局。 讓我們向Student和Person類添加一些整數(shù)變量。 使用此特定編譯器的特定計算機(jī)上的sizeof(int)為4個字節(jié)。 但總是要記住,這個數(shù)字在不同硬件上尺寸可能不一樣。我們在gdb中使用print命令輸出代碼中相關(guān)變量的地址,如下所示。
(gdb) p &m
$2 = (Student **) 0x7fffffffe2e8
(gdb) p &p
$3 = (Person **) 0x7fffffffe2e0
(gdb) p *m
$4 = {<Person> = {age = 6}, idNo = 1000}
(gdb) p *p
$5 = {age = 6}
(gdb) p m
$6 = (Student *) 0x602010
(gdb) p p
$7 = (Person *) 0x602030
(gdb)
我們用繪制成如下圖

以低數(shù)字開頭的內(nèi)存位置(在本例中為0x602010)是在堆上分配給Student對象的。由main棧幀上的指針變量m(地址0x7fffffffe2e8)指向它。如此類推Person對象也在堆上的區(qū)域是0x602030的位置。 眾所周知,堆向上增長,而棧向下增長。 對象在堆和棧幀的的組織方式在很大程度上取決于所使用的編譯器和操作系統(tǒng)的內(nèi)存管理方式。
某些值可以完全優(yōu)化并不需要入棧,直接并用寄存器代替。但本示例中沒有使用任何編譯的優(yōu)化選項,因此仍然使用x86的約定組織程序棧。
從上面的內(nèi)存布局中,我們可以得出幾點(diǎn)啟示:
- 基類指針p和派生指針都分別類的第一個字節(jié)。基類之后是派生類,位于更高的地址處。這個簡單示例有幫于我們找到虛擬指針。
- 派生類的內(nèi)存分配一般來說會比父類的內(nèi)存分配要大,因為派生類會從基類中繼承(拷貝)了public和protected修飾的數(shù)據(jù)成員的副本。在本例中Student對象得到了Person對象的age副本,當(dāng)你嘗試使用sizeof(*m)得到的結(jié)果是8,而Person的內(nèi)存分配尺寸則是4.
繼承鏈中的構(gòu)造順序
這其實是前面文章談?wù)摰蕉嗬^承的RAII約定,我們從反匯編的角度,加深對此過程的了解

派生類的初始化過程
- 在當(dāng)前派生類構(gòu)造函數(shù)的上下文的按照繼承列表中的順序執(zhí)行依次初始化繼承鏈中各個父類的構(gòu)造函數(shù),本示例如下步驟。
- (1) 從main函數(shù)執(zhí)行call 0x400640指令后,進(jìn)入Student::Student()所在代碼段中的上下文執(zhí)行的一些入棧的操作(構(gòu)造函數(shù)的棧內(nèi)存分配以及狀態(tài)保存)。
- (2) 執(zhí)行call 0x44062c 即在Person類指令集所在的代碼段地址初始化,在基類構(gòu)造返回之前,匯編指令movl $0x6,(%rax),這個干了這些事:基類將數(shù)據(jù)成員-變量age=6作為返回值保存到rax寄存器中緩存的內(nèi)存地址指向的位置(該位置在前一步的Student::Student構(gòu)造函數(shù)的棧內(nèi)存分配了,即-0x8(%rbp)的位置),以便派生類Student對象的構(gòu)造函數(shù)讀取作為它的數(shù)據(jù)成員。
- 返回派生類本身的構(gòu)造函數(shù)執(zhí)行剩余的指令集。
繼承的初始化過程
垃圾回收的過程 ,和繼承列表中定義的父類順序相反。
- 首先,調(diào)用函數(shù)在結(jié)束之時隱式執(zhí)行子類的解構(gòu)函數(shù)。
- 然后,依次逆序執(zhí)行子類繼承列表中父類的解構(gòu)函數(shù)。
從匯編代碼可知,在每個構(gòu)造函數(shù)的的匯編上下文,在執(zhí)行retq指令返回之前,當(dāng)前的構(gòu)造函數(shù)已經(jīng)將初始化的一些局部變量緩存到可用的寄存器中緩存的內(nèi)存地址所指向的位置了,當(dāng)然通常是rax寄存器。
小結(jié)
不對派生類的成員進(jìn)行任何更改而優(yōu)先初始化基類的構(gòu)造函數(shù)。 這對引入虛擬表時是一個非常重要的概念,因為此順序定義了什么函數(shù)在什么階段可見。
