C++多態(tài)實(shí)現(xiàn)和virtual原理

本文轉(zhuǎn)載自:(做了一些改動(dòng))
http://www.cnblogs.com/feixiang927/p/5048564.html

  1. 基本概念
    多態(tài)性是一個(gè)接口多種實(shí)現(xiàn),分為類的多態(tài)性函數(shù)多態(tài)性
    函數(shù)的多態(tài)性(重載)是指一個(gè)函數(shù)被定義成多個(gè)不同參數(shù)的函數(shù),它們一般被存在頭文件中,當(dāng)你調(diào)用這個(gè)函數(shù),針對(duì)不同的參數(shù),就會(huì)調(diào)用不同的同名函數(shù)。
    類的多態(tài)性用一句話概括就是:在基類的函數(shù)前加上virtual關(guān)鍵字,在派生類中重寫該函數(shù),運(yùn)行時(shí)將會(huì)根據(jù)對(duì)象的實(shí)際類型來(lái)調(diào)用相應(yīng)的函數(shù)。如果對(duì)象類型是派生類,就調(diào)用派生類的函數(shù);如果對(duì)象類型是基類,就調(diào)用基類的函數(shù)。

  2. 多態(tài)實(shí)現(xiàn)機(jī)制
    舉個(gè)例子

#include <iostream.h>
class animal
{
    public:
    void sleep(){cout<<"animal sleep"<<endl;}
    void breathe(){cout<<"animal breathe"<<endl;}
};
class fish:public animal
{
    public:
    void breathe(){cout<<"fish bubble"<<endl;}
};
void main()
{
    fish fh;
    animal *pAn=&fh;
    pAn->breathe();
}

答案是輸出:animal breathe
結(jié)果分析:

  • 從編譯的角度
    C++編譯器在編譯的時(shí)候,要確定每個(gè)對(duì)象調(diào)用的函數(shù)的地址,這稱為早期綁定(early binding),當(dāng)我們將fish類的對(duì)象fh的地址賦給pAn時(shí),C++編譯器進(jìn)行了類型轉(zhuǎn)換,此時(shí)C++編譯器認(rèn)為變量pAn保存的就是animal對(duì)象的地址。當(dāng)在main()函數(shù)中執(zhí)行pAn->breathe()時(shí),調(diào)用的當(dāng)然就是animal對(duì)象的breathe函數(shù)。
  • 從內(nèi)存模型的角度
    我們構(gòu)造fish類的對(duì)象時(shí),首先要調(diào)用animal類的構(gòu)造函數(shù)去構(gòu)造animal類的對(duì)象,然后才調(diào)用fish類的構(gòu)造函數(shù)完成自身部分的構(gòu)造,從而拼接出一個(gè)完整的fish對(duì)象。當(dāng)我們將fish類的對(duì)象轉(zhuǎn)換為animal類型時(shí),該對(duì)象就被認(rèn)為是原對(duì)象整個(gè)內(nèi)存模型的上半部分,也就是圖1-1中的“animal的對(duì)象所占內(nèi)存”。那么當(dāng)我們利用類型轉(zhuǎn)換后的對(duì)象指針去調(diào)用它的方法時(shí),當(dāng)然也就是調(diào)用它所在的內(nèi)存中的方法。因此,輸出animal breathe,也就順理成章了。


    1.png

要想得到我們想要的結(jié)果就要使用虛函數(shù)
前面輸出的結(jié)果是因?yàn)榫幾g器在編譯的時(shí)候,就已經(jīng)確定了對(duì)象調(diào)用的函數(shù)的地址,要解決這個(gè)問題就要使用遲綁定(late binding)技術(shù)。當(dāng)編譯器使用遲綁定時(shí),就會(huì)在運(yùn)行時(shí)再去確定對(duì)象的類型以及正確的調(diào)用函數(shù)。而要讓編譯器采用遲綁定,就要在基類中聲明函數(shù)時(shí)使用virtual關(guān)鍵字,這樣的函數(shù)我們稱為虛函數(shù)。一旦某個(gè)函數(shù)在基類中聲明為virtual,那么在所有的派生類中該函數(shù)都是virtual,而不需要再顯式地聲明為virtual。

下面我們將上面一段代碼進(jìn)行部分修改

virtual void breathe()
{
cout<<"animal breathe"<<endl;
}

運(yùn)行結(jié)果:fish bubble
結(jié)果分析
編譯器為每個(gè)類的對(duì)象提供一個(gè)虛表指針,這個(gè)指針指向?qū)ο笏鶎兕惖奶摫怼T诔绦蜻\(yùn)行時(shí),根據(jù)對(duì)象的類型去初始化vptr,從而讓vptr正確的指向所屬類的虛表,從而在調(diào)用虛函數(shù)時(shí),就能夠找到正確的函數(shù)。

由于pAn實(shí)際指向的對(duì)象類型是fish,因此vptr指向的fish類的vtable,當(dāng)調(diào)用pAn->breathe()時(shí),根據(jù)虛表中的函數(shù)地址找到的就是fish類的breathe()函數(shù)。

正是由于每個(gè)對(duì)象調(diào)用的虛函數(shù)都是通過(guò)虛表指針來(lái)索引的,也就決定了虛表指針的正確初始化是非常重要的。換句話說(shuō),在虛表指針沒有正確初始化之前,我們不能夠去調(diào)用虛函數(shù)。

那么虛表指針在什么時(shí)候,或者說(shuō)在什么地方初始化呢?
答案是在構(gòu)造函數(shù)中進(jìn)行虛表的創(chuàng)建和虛表指針的初始化。還記得構(gòu)造函數(shù)的調(diào)用順序嗎,在構(gòu)造子類對(duì)象時(shí),要先調(diào)用父類的構(gòu)造函數(shù),此時(shí)編譯器只“看到了”父類,并不知道后面是否后還有繼承者,它初始化父類對(duì)象的虛表指針,該虛表指針指向父類的虛表。當(dāng)執(zhí)行子類的構(gòu)造函數(shù)時(shí),子類對(duì)象的虛表指針被初始化,指向自身的虛表。

當(dāng)fish類的fh對(duì)象構(gòu)造完畢后,其內(nèi)部的虛表指針也就被初始化為指向fish類的虛表。在類型轉(zhuǎn)換后,調(diào)用pAn->breathe(),由于pAn實(shí)際指向的是fish類的對(duì)象,該對(duì)象內(nèi)部的虛表指針指向的是fish類的虛表,因此最終調(diào)用的是fish類的breathe()函數(shù)。

在該文章中詳細(xì)講解了繼承中類的內(nèi)存對(duì)象模型:http://www.itdecent.cn/p/31373b52902d

虛函數(shù)與純虛函數(shù)區(qū)別:

  1. 虛函數(shù)和純虛函數(shù)可以定義在同一個(gè)類(class)中,含有純虛函數(shù)的類被稱為抽象類(abstract class),而只含有虛函數(shù)的類(class)不能被稱為抽象類(abstract class)。
  2. 虛函數(shù)可以被直接使用,也可以被子類(sub class)重載以后以多態(tài)的形式調(diào)用,而純虛函數(shù)必須在子類(sub class)中實(shí)現(xiàn)該函數(shù)才可以使用,因?yàn)榧兲摵瘮?shù)在基類(base class)只有聲明而沒有定義。
  3. 虛函數(shù)和純虛函數(shù)都可以在子類(sub class)中被重載,以多態(tài)的形式被調(diào)用。
  4. 虛函數(shù)和純虛函數(shù)通常存在于抽象基類(abstract base class -ABC)之中,被繼承的子類重載,目的是提供一個(gè)統(tǒng)一的接口。
  5. 虛函數(shù)的定義形式:virtual {method body}
    純虛函數(shù)的定義形式:virtual { } = 0;
    在虛函數(shù)和純虛函數(shù)的定義中不能有static標(biāo)識(shí)符,原因很簡(jiǎn)單,被static修飾的函數(shù)在編譯時(shí)候要求前期bind,然而虛函數(shù)卻是動(dòng)態(tài)綁定(run-time bind),而且被兩者修飾的函數(shù)生命周期(life recycle)也不一樣。
  6. 虛函數(shù)必須實(shí)現(xiàn),如果不實(shí)現(xiàn),編譯器將報(bào)錯(cuò),錯(cuò)誤提示為:
    error LNK****: unresolved external symbol "public: virtual void __thiscall
    ClassName::virtualFunctionName(void)"
  7. 對(duì)于虛函數(shù)來(lái)說(shuō),父類和子類都有各自的版本。由多態(tài)方式調(diào)用的時(shí)候動(dòng)態(tài)綁定。
  8. 實(shí)現(xiàn)了純虛函數(shù)的子類,該純虛函數(shù)在子類中就變成了虛函數(shù),子類的子類即孫子類可以覆蓋該虛函數(shù),由多態(tài)方式調(diào)用的時(shí)候動(dòng)態(tài)綁定。
  9. 虛函數(shù)是C++中用于實(shí)現(xiàn)多態(tài)(polymorphism)的機(jī)制。核心理念就是通過(guò)基類訪問派生類定義的函數(shù)。
  10. 多態(tài)性指相同對(duì)象收到不同消息或不同對(duì)象收到相同消息時(shí)產(chǎn)生不同的實(shí)現(xiàn)動(dòng)作。C++支持兩種多態(tài)性:編譯時(shí)多態(tài)性,運(yùn)行時(shí)多態(tài)性。
    a.編譯時(shí)多態(tài)性:通過(guò)重載函數(shù)實(shí)現(xiàn)
    b 運(yùn)行時(shí)多態(tài)性:通過(guò)虛函數(shù)實(shí)現(xiàn)
  11. 如果一個(gè)類中含有純虛函數(shù),那么任何試圖對(duì)該類進(jìn)行實(shí)例化的語(yǔ)句都將導(dǎo)致錯(cuò)誤的產(chǎn)生,因?yàn)槌橄蠡?ABC)是不能被直接調(diào)用的。必須被子類繼承重載以后,根據(jù)要求調(diào)用其子類的方法。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • C++虛函數(shù) C++虛函數(shù)是多態(tài)性實(shí)現(xiàn)的重要方式,當(dāng)某個(gè)虛函數(shù)通過(guò)指針或者引用調(diào)用時(shí),編譯器產(chǎn)生的代碼直到運(yùn)行時(shí)才...
    小白將閱讀 1,803評(píng)論 4 19
  • 繼承和多態(tài) 1. 繼承的優(yōu)缺點(diǎn) 優(yōu)點(diǎn):(1)子類可以靈活地改變父類中的已有方法;(2)能夠最大限度的實(shí)現(xiàn)代碼重用。...
    MinoyJet閱讀 727評(píng)論 0 0
  • 1. 結(jié)構(gòu)體和共同體的區(qū)別。 定義: 結(jié)構(gòu)體struct:把不同類型的數(shù)據(jù)組合成一個(gè)整體,自定義類型。共同體uni...
    breakfy閱讀 2,273評(píng)論 0 22
  • 夜舞東風(fēng)澈云空,新葉重重綴楓黃。 及至三秋寒霜后,滿樹堪比二月紅。 (2017.04.14)
    正誼明道閱讀 553評(píng)論 1 2
  • 人生只有一次。 把握當(dāng)下。
    黑星星i閱讀 370評(píng)論 0 0

友情鏈接更多精彩內(nèi)容