在本文中,我們將繼續(xù)深入研究C ++運(yùn)行時(shí)動(dòng)態(tài)調(diào)度的相關(guān)話(huà)題功偿。 到目前為止恨旱,我們已經(jīng)驗(yàn)證gdb不會(huì)Trivial類(lèi)型的類(lèi)和默認(rèn)構(gòu)造函數(shù)創(chuàng)建虛擬表,我們?cè)诒酒獙㈥U述非虛擬派生類(lèi)它們的構(gòu)造和內(nèi)存布局若专,這是正確區(qū)分虛擬類(lèi)和非虛擬類(lèi)在內(nèi)存分配方面的差異的前提许蓖。
示例導(dǎo)入
讓我引入兩個(gè)新的類(lèi)。一個(gè)基類(lèi)Person和一個(gè)從Person繼承的派生類(lèi)Student调衰。請(qǐng)注意膊爪,兩者都使用其方法who()的相同簽名。
- 有了動(dòng)態(tài)調(diào)度后嚎莉,將查詢(xún)vtable并調(diào)用適當(dāng)?shù)姆椒ā?/li>
- 如果沒(méi)有動(dòng)態(tài)分配米酬,則將調(diào)用與對(duì)象的指針類(lèi)型匹配的方法。 Student *m將調(diào)用Student:: who()趋箩,而Person *p將調(diào)用Person::who()赃额。讓我們用匯編代碼驗(yàn)證一下。
#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)在編譯時(shí)已經(jīng)植入Student類(lèi)的上下文叫确,從callq 0400658 <_ZN7Student3whoEv>,這條指令表明運(yùn)行時(shí)決策是不可能的跳芳。 指針的類(lèi)型在編譯時(shí)是已知的,編譯器會(huì)選擇正確的who()方法調(diào)用,同理callq 0x400626 <_ZN6Person3whoEv>,也是一條編譯時(shí)的靜態(tài)指令竹勉。下圖說(shuō)明類(lèi)一切:
但是我們也可以通過(guò)定義基類(lèi)的名稱(chēng)空間來(lái)調(diào)用基類(lèi)的方法飞盆,如下所示
m->Person::who()
內(nèi)存布局
為了降低問(wèn)題的復(fù)雜性,我將使用此代碼的一些細(xì)微變化次乓,去掉一些who方法:
class Person {
public:
int age=6;
Person() {}
};
class Student: public Person{
public:
int idNo=1000;
Student() {}
};
為了了解虛擬表的隱藏位置吓歇,讓我們首先檢查一下簡(jiǎn)單繼承層次結(jié)構(gòu)的內(nèi)存布局。 讓我們向Student和Person類(lèi)添加一些整數(shù)變量票腰。 使用此特定編譯器的特定計(jì)算機(jī)上的sizeof(int)為4個(gè)字節(jié)城看。 但總是要記住,這個(gè)數(shù)字在不同硬件上尺寸可能不一樣杏慰。我們?cè)趃db中使用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ù)字開(kāi)頭的內(nèi)存位置(在本例中為0x602010)是在堆上分配給Student對(duì)象的炼鞠。由main棧幀上的指針變量m(地址0x7fffffffe2e8)指向它。如此類(lèi)推Person對(duì)象也在堆上的區(qū)域是0x602030的位置鹃愤。 眾所周知簇搅,堆向上增長(zhǎng),而棧向下增長(zhǎng)软吐。 對(duì)象在堆和棧幀的的組織方式在很大程度上取決于所使用的編譯器和操作系統(tǒng)的內(nèi)存管理方式瘩将。
某些值可以完全優(yōu)化并不需要入棧,直接并用寄存器代替。但本示例中沒(méi)有使用任何編譯的優(yōu)化選項(xiàng),因此仍然使用x86的約定組織程序棧凹耙。
從上面的內(nèi)存布局中姿现,我們可以得出幾點(diǎn)啟示:
- 基類(lèi)指針p和派生指針都分別類(lèi)的第一個(gè)字節(jié)⌒けВ基類(lèi)之后是派生類(lèi)备典,位于更高的地址處。這個(gè)簡(jiǎn)單示例有幫于我們找到虛擬指針意述。
- 派生類(lèi)的內(nèi)存分配一般來(lái)說(shuō)會(huì)比父類(lèi)的內(nèi)存分配要大,因?yàn)?strong>派生類(lèi)會(huì)從基類(lèi)中繼承(拷貝)了public和protected修飾的數(shù)據(jù)成員的副本提佣。在本例中Student對(duì)象得到了Person對(duì)象的age副本,當(dāng)你嘗試使用sizeof(*m)得到的結(jié)果是8,而Person的內(nèi)存分配尺寸則是4.
繼承鏈中的構(gòu)造順序
這其實(shí)是前面文章談?wù)摰蕉嗬^承的RAII約定荤崇,我們從反匯編的角度,加深對(duì)此過(guò)程的了解
派生類(lèi)的初始化過(guò)程
- 在當(dāng)前派生類(lèi)構(gòu)造函數(shù)的上下文的按照繼承列表中的順序執(zhí)行依次初始化繼承鏈中各個(gè)父類(lèi)的構(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類(lèi)指令集所在的代碼段地址初始化,在基類(lèi)構(gòu)造返回之前,匯編指令movl $0x6,(%rax),這個(gè)干了這些事:基類(lèi)將數(shù)據(jù)成員-變量age=6作為返回值保存到rax寄存器中緩存的內(nèi)存地址指向的位置(該位置在前一步的Student::Student構(gòu)造函數(shù)的棧內(nèi)存分配了,即-0x8(%rbp)的位置),以便派生類(lèi)Student對(duì)象的構(gòu)造函數(shù)讀取作為它的數(shù)據(jù)成員术荤。
- 返回派生類(lèi)本身的構(gòu)造函數(shù)執(zhí)行剩余的指令集倚喂。
繼承的初始化過(guò)程
垃圾回收的過(guò)程 ,和繼承列表中定義的父類(lèi)順序相反。
- 首先,調(diào)用函數(shù)在結(jié)束之時(shí)隱式執(zhí)行子類(lèi)的解構(gòu)函數(shù)瓣戚。
- 然后,依次逆序執(zhí)行子類(lèi)繼承列表中父類(lèi)的解構(gòu)函數(shù)端圈。
從匯編代碼可知,在每個(gè)構(gòu)造函數(shù)的的匯編上下文,在執(zhí)行retq指令返回之前,當(dāng)前的構(gòu)造函數(shù)已經(jīng)將初始化的一些局部變量緩存到可用的寄存器中緩存的內(nèi)存地址所指向的位置了,當(dāng)然通常是rax寄存器子库。
小結(jié)
不對(duì)派生類(lèi)的成員進(jìn)行任何更改而優(yōu)先初始化基類(lèi)的構(gòu)造函數(shù)舱权。 這對(duì)引入虛擬表時(shí)是一個(gè)非常重要的概念,因?yàn)榇隧樞蚨x了什么函數(shù)在什么階段可見(jiàn)仑嗅。