原文地址:新浪博客 | zjdtc | 虛函數(shù)與構(gòu)造函數(shù)、析構(gòu)函數(shù) | 2011-06-22
本文在原文之上,增加了些個(gè)人的問題及理解。
構(gòu)造函數(shù)不能是虛函數(shù)
- 從存儲(chǔ)空間角度
虛函數(shù)對(duì)應(yīng)一個(gè)vtable,而這個(gè)vtable是存儲(chǔ)在對(duì)象的內(nèi)存空間的,也就是說,如果構(gòu)造函數(shù)是虛函數(shù),就需要通過vtable來調(diào)用,可對(duì)象就是通過構(gòu)造函數(shù)來實(shí)例化的,實(shí)例化之前尚沒有內(nèi)存空間(衍生出“先有雞還是先有蛋的問題”),所以構(gòu)造函數(shù)不能是虛函數(shù); - 從使用角度
虛函數(shù)主要用于在信息不全的情況下,能使重載的函數(shù)得到對(duì)應(yīng)的調(diào)用,特別允許調(diào)用一個(gè)只知道接口而不知道其準(zhǔn)確對(duì)象類型的函數(shù);而構(gòu)造函數(shù)本身就是要初始化對(duì)象,勢(shì)必要知道對(duì)象的準(zhǔn)確類型,所以構(gòu)造函數(shù)不能是虛函數(shù); - 從作用
虛函數(shù)的作用在于通過基類的指針或引用來調(diào)用它的時(shí)候能夠變成調(diào)用派生類的那個(gè)成員函數(shù),而構(gòu)造函數(shù)是在創(chuàng)建對(duì)象時(shí)自動(dòng)調(diào)用的,不可能通過基類的指針或者引用去調(diào)用,所以構(gòu)造函數(shù)不能是虛函數(shù); - 總結(jié)
vtable在構(gòu)造函數(shù)調(diào)用后才建立,因而構(gòu)造函數(shù)不可能成為虛函數(shù);在調(diào)用構(gòu)造函數(shù)時(shí)還不能確定對(duì)象的真實(shí)類型,因?yàn)榕缮悤?huì)調(diào)用基類的構(gòu)造函數(shù),而且構(gòu)造函數(shù)的作用是提供初始化,在對(duì)象生命期只執(zhí)行一次,不是對(duì)象的動(dòng)態(tài)行為,也沒有必要成為虛函數(shù)。
析構(gòu)函數(shù)可以是虛函數(shù),甚至是純虛函數(shù)
在面向?qū)ο蟮木幊踢^程中,基類的指針或引用通常會(huì)指向基類或派生類對(duì)象,如果基類的析構(gòu)函數(shù)不是虛函數(shù),在通過刪除指針或引用來釋放對(duì)象時(shí),只會(huì)調(diào)用基類的析構(gòu)函數(shù),而不會(huì)調(diào)用派生類的析構(gòu)函數(shù),從而導(dǎo)致內(nèi)存泄漏;反之,如果基類的析構(gòu)函數(shù)是虛函數(shù),就不會(huì)發(fā)生這類問題;因此,當(dāng)一個(gè)類打算被用作其它類的基類時(shí),它的析構(gòu)函數(shù)必須是虛函數(shù)??紤]如下例子:
class A
{
public:
A() { ptra_ = new char[10];}
~A() { delete[] ptra_;} // 非虛析構(gòu)函數(shù)
private:
char * ptra_;
};
class B: public A
{
public:
B() { ptrb_ = new char[20];}
~B() { delete[] ptrb_;}
private:
char * ptrb_;
};
void foo()
{
A * a = new B();
delete a;
}
在這個(gè)例子中,在執(zhí)行delete a的時(shí)候,實(shí)際上只有A::~ A()被調(diào)用了,而B類的析構(gòu)函數(shù)并沒有被調(diào)用。如果將上面A::~A()改為virtual,就可以保證B:: ~B()也在delete a的時(shí)候被調(diào)用了;因此基類的析構(gòu)函數(shù)都必須是virtual的;
但是,一般如果不做基類的類的析構(gòu)函數(shù)一般不聲明為虛函數(shù),因?yàn)樘摵瘮?shù)的實(shí)現(xiàn)要求對(duì)象攜帶額外的信息,添加系統(tǒng)開銷,即需要在對(duì)象的內(nèi)存空間中添加一個(gè)vptr,該指針指向vtable;
析構(gòu)函數(shù)可以是純虛函數(shù),通常只有在將一個(gè)類設(shè)定為抽象類,而這個(gè)類又沒有合適的函數(shù)可以被純虛化的時(shí)候,可以使用純虛的析構(gòu)函數(shù)來達(dá)到目的;但是,純虛的析構(gòu)函數(shù)不同于其它純虛函數(shù)的一點(diǎn)是,純虛的析構(gòu)函數(shù)要提供它的定義,其它的純虛函數(shù)只提供聲明即可,原因是:當(dāng)釋放一個(gè)派生類對(duì)象時(shí),其析構(gòu)函數(shù)調(diào)用最終會(huì)到抽象基類這一層,會(huì)調(diào)用抽象基類的虛構(gòu)函數(shù),如果抽象類的析構(gòu)函數(shù)沒有定義,會(huì)導(dǎo)致編譯時(shí)錯(cuò)誤。
多態(tài)與虛函數(shù)
虛函數(shù)是C++中用于實(shí)現(xiàn)多態(tài)的機(jī)制,核心理念就是通過基類訪問派生類定義的函數(shù)。
多態(tài)的用途
在面向?qū)ο蟮木幊讨?,首先?huì)針對(duì)數(shù)據(jù)進(jìn)行抽象(確定基類)和繼承(確定派生類),構(gòu)成類層次。這個(gè)類層次的使用者在使用它們的時(shí)候,如果仍然在需要基類的時(shí)候?qū)戓槍?duì)基類的代碼,在需要派生類的時(shí)候?qū)戓槍?duì)派生類的代碼,就等于類層次完全暴露在使用者面前。如果這個(gè)類層次有任何的改變(增加了新類),都需要使用者“知道”(針對(duì)新類寫代碼)。這樣就增加了類層次與其使用者之間的耦合,有人把這種情況列為程序中的“bad smell”之一。多態(tài)可以使程序員脫離這種窘境,通過一個(gè)基類指針或引用,調(diào)用一個(gè)虛函數(shù),可以達(dá)到實(shí)際調(diào)用不同派生類的函數(shù)的效果,降低了類層次與使用者之間的耦合。
如何“動(dòng)態(tài)聯(lián)編”
編譯器是如何針對(duì)虛函數(shù)產(chǎn)生可以再運(yùn)行時(shí)刻確定被調(diào)用函數(shù)的代碼呢?也就是說,虛函數(shù)實(shí)際上是如何被編譯器處理的呢?Lippman在深度探索C++對(duì)象模型[1]中的不同章節(jié)講到了幾種方式,這里把“標(biāo)準(zhǔn)的”方式簡(jiǎn)單介紹一下。
所說的“標(biāo)準(zhǔn)”方式,也就是“vtable”機(jī)制。編譯器發(fā)現(xiàn)一個(gè)類中有被聲明為virtual的函數(shù),就會(huì)為其生成一個(gè)虛函數(shù)表,也就是vtable。vtable實(shí)際上是一個(gè)函數(shù)指針的數(shù)組,每個(gè)虛函數(shù)占用一個(gè)slot。一個(gè)類只有一個(gè)vtable,不管它有多少個(gè)對(duì)象。派生類有自己的vtable,但是派生類的vtable與基類的vtable有相同的函數(shù)排列順序,同名的虛函數(shù)被放在兩個(gè)數(shù)組的相同位置上。在創(chuàng)建類對(duì)象的時(shí)候,編譯器還會(huì)在每個(gè)實(shí)例的內(nèi)存布局中增加一個(gè)vptr字段,該字段指向本類的vtable。通過這些手段,編譯器在看到一個(gè)虛函數(shù)調(diào)用的時(shí)候,就會(huì)將這個(gè)調(diào)用改寫:
void bar(A * a){ a->foo(); }
會(huì)被改寫為:
void bar(A * a){ (a->vptr[1])(); }
因?yàn)榕缮惡突惖膄oo()函數(shù)具有相同的vtable索引,而他們的vptr又指向不同的vtable,因此通過這樣的方法可以在運(yùn)行時(shí)刻決定調(diào)用哪個(gè)foo()函數(shù)。雖然實(shí)際情況遠(yuǎn)非這么簡(jiǎn)單,但是基本原理大致如此。
構(gòu)造函數(shù)和析構(gòu)函數(shù)中的虛函數(shù)調(diào)用
一個(gè)類的虛函數(shù)在它自己的構(gòu)造函數(shù)和析構(gòu)函數(shù)中被調(diào)用的時(shí)候,它們就變成普通函數(shù)了,不“虛”了。也就是說不能在構(gòu)造函數(shù)和析構(gòu)函數(shù)中讓自己“多態(tài)”。
當(dāng)構(gòu)造函數(shù)內(nèi)部有虛函數(shù)時(shí),只調(diào)用自己類中的虛函數(shù),原因是調(diào)用時(shí)還沒有派生類版本的信息。
當(dāng)析構(gòu)函數(shù)內(nèi)部有虛函數(shù)時(shí),與構(gòu)造函數(shù)相同,只有“局部”的版本被調(diào)用,原因是因?yàn)榕缮惏姹镜男畔⒁呀?jīng)不可靠了。由于析構(gòu)函數(shù)的調(diào)用順序與構(gòu)造函數(shù)相反,是從派生類的析構(gòu)函數(shù)到基類的析構(gòu)函數(shù)。當(dāng)某個(gè)類的析構(gòu)函數(shù)被調(diào)用時(shí),派生自該類的類的析構(gòu)函數(shù)已經(jīng)被調(diào)用了,相應(yīng)的數(shù)據(jù)也已丟失,如果再調(diào)用虛函數(shù)的最后一級(jí)的版本,就相當(dāng)于對(duì)一些不可靠的數(shù)據(jù)進(jìn)行操作,這是非常危險(xiǎn)的。因此,在析構(gòu)函數(shù)中,虛函數(shù)機(jī)制也是不起作用的。
什么時(shí)候使用虛函數(shù)
在你設(shè)計(jì)一個(gè)基類的時(shí)候,如果發(fā)現(xiàn)一個(gè)函數(shù)需要在派生類里有不同的表現(xiàn),那么它就應(yīng)該是虛的。從設(shè)計(jì)的角度講,出現(xiàn)在基類中的虛函數(shù)是接口,出現(xiàn)在派生類中的虛函數(shù)是接口的具體實(shí)現(xiàn)。通過這樣的方法,就可以將對(duì)象的行為抽象化。
Things to Remember
- 定義一個(gè)函數(shù)為虛函數(shù),不代表函數(shù)為不被實(shí)現(xiàn)的函數(shù);定義它為虛函數(shù)是為了允許用基類的指針來調(diào)用子類的這個(gè)函數(shù);
- 定義一個(gè)函數(shù)為純虛函數(shù),才代表函數(shù)沒有被實(shí)現(xiàn);定義它是為了實(shí)現(xiàn)一個(gè)接口,起到一個(gè)規(guī)范的作用,規(guī)范繼承這個(gè),類的程序員必須實(shí)現(xiàn)這個(gè)函數(shù);
- 有純虛函數(shù)的類是不可能生成類對(duì)象的,如果沒有純虛函數(shù)則可以;
- 多態(tài)一般就是通過指向基類的指針來實(shí)現(xiàn)的。
問題:如果在基類中把某個(gè)函數(shù)聲明為虛函數(shù),在兩個(gè)派生類中,一個(gè)再次定義該函數(shù),另一個(gè)再將該函數(shù)聲明為虛函數(shù),兩個(gè)派生類的虛函數(shù)表有什么不同?(答案見“參考文章”中的“C++虛函數(shù)表剖析”)