[轉(zhuǎn)載]虛函數(shù)與構(gòu)造函數(shù)、析構(gòu)函數(shù)

原文地址:新浪博客 | zjdtc | 虛函數(shù)與構(gòu)造函數(shù)、析構(gòu)函數(shù) | 2011-06-22
本文在原文之上,增加了些個(gè)人的問題及理解。


構(gòu)造函數(shù)不能是虛函數(shù)

  1. 從存儲(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ù);
  2. 從使用角度
    虛函數(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ù);
  3. 從作用
    虛函數(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ù);
  4. 總結(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ù)表剖析”

參考文章

1. CSDN | haozlee | Leo的博客 | C++虛函數(shù)表剖析

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • C++虛函數(shù) C++虛函數(shù)是多態(tài)性實(shí)現(xiàn)的重要方式,當(dāng)某個(gè)虛函數(shù)通過指針或者引用調(diào)用時(shí),編譯器產(chǎn)生的代碼直到運(yùn)行時(shí)才...
    小白將閱讀 1,756評(píng)論 4 19
  • 轉(zhuǎn):C++繼承中構(gòu)造函數(shù)、析構(gòu)函數(shù)調(diào)用順序及虛析構(gòu)函數(shù) 1.構(gòu)造函數(shù) 大家都知道構(gòu)造函數(shù)里就可以調(diào)用成員變量,而繼...
    資深小夏閱讀 738評(píng)論 0 0
  • 1.面向?qū)ο蟮某绦蛟O(shè)計(jì)思想是什么? 答:把數(shù)據(jù)結(jié)構(gòu)和對(duì)數(shù)據(jù)結(jié)構(gòu)進(jìn)行操作的方法封裝形成一個(gè)個(gè)的對(duì)象。 2.什么是類?...
    少帥yangjie閱讀 5,051評(píng)論 0 14
  • 多態(tài)(1)靜態(tài)多態(tài)與動(dòng)態(tài)多態(tài) 什么是多態(tài) 從字面上理解就是多種形態(tài)的意思。而多態(tài)一詞最初源自希臘語,其含義便是“多...
    kingZXY2009閱讀 2,349評(píng)論 0 2
  • 這一日,行至安平鎮(zhèn)地方。眼見得時(shí)已近午,白福便說:“爺,您看是不是先找個(gè)地方打尖?”白玉堂微微點(diǎn)頭。但見路西有一座...
    6d9d92d8926f閱讀 455評(píng)論 0 7