關(guān)于C++內(nèi)部如何實(shí)現(xiàn)多態(tài),對(duì)程序員來說即使不知道也沒關(guān)系蚪缀,但是如果你想加深對(duì)多態(tài)的理解,寫出優(yōu)秀的代碼恕出,那么這一節(jié)就具有重要的意義询枚。 我們知道,函數(shù)調(diào)用實(shí)際上是執(zhí)行函數(shù)體中的代碼浙巫。函數(shù)體是內(nèi)存中的一個(gè)代碼段金蜀,函數(shù)名就表示該代碼段的首地址刷后,函數(shù)執(zhí)行時(shí)就從這里開始。說得簡(jiǎn)單一點(diǎn)渊抄,就是必須要知道函數(shù)的入口地址尝胆,才能成功調(diào)用函數(shù)。
找到函數(shù)名對(duì)應(yīng)的地址护桦,然后將函數(shù)調(diào)用處用該地址替換含衔,這稱為函數(shù)綁定,或符號(hào)決議二庵。
一般情況下贪染,在編譯期間(包括鏈接期間)就能完成符號(hào)決議,不用等到程序執(zhí)行時(shí)再進(jìn)行額外的操作催享,這稱為靜態(tài)綁定杭隙。如果編譯期間不能完成符號(hào)決議,就必須在程序執(zhí)行期間完成因妙,這稱為動(dòng)態(tài)綁定痰憎。
非虛成員函數(shù)屬于靜態(tài)綁定:編譯器在編譯期間,根據(jù)指針(或?qū)ο螅┑念愋屯瓿闪私壎ā?br>
而對(duì)于虛函數(shù)攀涵,知道指針的類型也無(wú)濟(jì)于事信殊。假設(shè) func() 為虛函數(shù),p 的類型為 A汁果,那么 p->func() 可能調(diào)用 A 類的函數(shù),也可能調(diào)用 B玲躯、C 類的函數(shù)据德,不能根據(jù)指針 p 的類型對(duì)函數(shù)重命名。也就是說跷车,虛函數(shù)在編譯期間無(wú)法綁定棘利。
虛函數(shù)表 vtable
如果一個(gè)類包含了虛函數(shù),那么在創(chuàng)建對(duì)象時(shí)會(huì)額外增加一張表朽缴,表中的每一項(xiàng)都是虛函數(shù)的入口地址善玫。這張表就是虛函數(shù)表,也稱為 vtable密强。 可以認(rèn)為虛函數(shù)表是一個(gè)數(shù)組茅郎。 為了把對(duì)象和虛函數(shù)表關(guān)聯(lián)起來,編譯器會(huì)在對(duì)象中安插一個(gè)指針或渤,指向虛函數(shù)表的起始位置系冗。
例如對(duì)于下面的繼承關(guān)系:
class A{
protected:
int a1;
int a2;
public:
virtual void display(){ cout<<"A::display()"<
virtual void clone(){ cout<<"A::clone()"<
};
class B: public A{
protected:
int b;
public:
virtual void display(){ cout<<"B::display()"<
virtual void init(){ cout<<"B::init()"<
};
class C: public B{
protected:
int c;
public:
virtual void display(){ cout<<"C::display()"<
virtual void execute(){ cout<<"C::execute()"<
};
各個(gè)類的內(nèi)存分布如下所示:
通過上圖可以發(fā)現(xiàn),對(duì)于單繼承薪鹦,不管繼承層次有多深掌敬,只需要增加一個(gè)指針即可惯豆,不會(huì)隨著繼承層次的加深讓對(duì)象背負(fù)越來越多的指針。而且奔害,基類中的虛函數(shù)在 vtable 中的索引是固定的楷兽,不會(huì)隨著繼承層次的增加而改變,例如 display() 的索引值始終是 0华临。當(dāng)調(diào)用虛函數(shù)時(shí)芯杀,借助指針 vfptr 完成一次間接轉(zhuǎn)換,就可以得到虛函數(shù)的入口地址银舱。
對(duì)于虛函數(shù) display()瘪匿,它在 vtable 中的索引為 0,發(fā)生調(diào)用時(shí):
p->display();
編譯器內(nèi)部會(huì)發(fā)生轉(zhuǎn)換寻馏,產(chǎn)生類似下面的代碼:
( *( p->vptr )[0] ) (p);? //*( p->vptr )[0]是函數(shù)入口地址
這條語(yǔ)句沒有用到與指針 p 的類型有關(guān)的信息棋弥,也沒有用到 Name Mangling 算法。程序運(yùn)行后會(huì)執(zhí)行這條語(yǔ)句诚欠,完成函數(shù)的調(diào)用顽染,這就是動(dòng)態(tài)綁定。
編譯器在編譯期間會(huì)備足各種信息轰绵,并完成相應(yīng)的轉(zhuǎn)換粉寞,程序運(yùn)行后只需要執(zhí)行簡(jiǎn)單的代碼就能找到函數(shù)入口地址,進(jìn)而調(diào)用函數(shù)左腔。
init() 函數(shù)在 vtable 中的索引為 2唧垦,發(fā)生調(diào)用時(shí):
p->init();
編譯器內(nèi)部的轉(zhuǎn)換為:
( *( p->vptr )[2] ) (p);
對(duì)于不同的虛函數(shù),僅僅改變索引值即可液样。
當(dāng)派生類有多重繼承時(shí)振亮,虛函數(shù)表的結(jié)構(gòu)會(huì)變得復(fù)雜,尤其是有虛繼承時(shí)鞭莽,還會(huì)增加虛基類表坊秸,更加讓人抓狂,這里我們就不分析了澎怒,有興趣的讀者可以自行研究褒搔。