天天看點

c++多态性之虛函數與虛表指針

1. 用virtual關鍵字申明的函數叫做虛函數,虛函數肯定是類的成員函數。

2. 存在虛函數的類都有一個一維的虛函數表叫做虛表。類的對象有一個指向虛表開始的虛指針。虛表是和類對應的,虛表指針是和對象對應的。

3. 多态性是一個接口多種實作,是面向對象的核心。分為類的多态性和函數的多态性。

4. 多态用虛函數來實作,結合動态綁定。

5. 純虛函數是虛函數再加上= 0。

6. 抽象類是指包括至少一個純虛函數的類。

純虛函數:virtual void breathe()=0;即抽象類!必須在子類實作這個函數!即先有名稱,沒内容,在派生類實作内容!

我們先看一個例子:

[cpp]

view plain copy print ?

  1. #include <iostream.h>  
  2. class animal  
  3. {  
  4. public:  
  5.        void sleep()  
  6.        {  
  7.               cout<<”animal sleep”<<endl;  
  8.        }  
  9.        void breathe()  
  10.        {  
  11.               cout<<”animal breathe”<<endl;  
  12.        }  
  13. };  
  14. class fish:public animal  
  15. {  
  16. public:  
  17.        void breathe()  
  18.        {  
  19.               cout<<”fish bubble”<<endl;  
  20.        }  
  21. };  
  22. void main()  
  23. {  
  24.        fish fh;  
  25.        animal *pAn=&fh; // 隐式類型轉換  
  26.        pAn->breathe();  
  27. }  
c++多态性之虛函數與虛表指針
#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();
}
           

注意,在例1-1的程式中沒有定義虛函數。考慮一下例1-1的程式執行的結果是什麼?

答案是輸出:animal breathe

       我們在main()函數中首先定義了一個fish類的對象fh,接着定義了一個指向animal類的指針變量pAn,将fh的位址賦給了指針變量pAn,然後利用該變量調用pAn->breathe()。許多學員往往将這種情況和C++的多态性搞混淆,認為fh實際上是fish類的對象,應該是調用fish類的breathe(),輸出“fish bubble”,然後結果卻不是這樣。下面我們從兩個方面來講述原因。

1、 編譯的角度

C++編譯器在編譯的時候,要确定每個對象調用的函數(要求此函數是非虛函數)的位址,這稱為早期綁定(early binding),當我們将fish類的對象fh的位址賦給pAn時,C++編譯器進行了類型轉換,此時C++編譯器認為變量pAn儲存的就是animal對象的位址。當在main()函數中執行pAn->breathe()時,調用的當然就是animal對象的breathe函數。

2、 記憶體模型的角度

我們給出了fish對象記憶體模型,如下圖所示:

c++多态性之虛函數與虛表指針

        我們構造fish類的對象時,首先要調用animal類的構造函數去構造animal類的對象,然後才調用fish類的構造函數完成自身部分的構造,進而拼接出一個完整的fish對象。當我們将fish類的對象轉換為animal類型時,該對象就被認為是原對象整個記憶體模型的上半部分,也就是圖1-1中的“animal的對象所占記憶體”。那麼當我們利用類型轉換後的對象指針去調用它的方法時,當然也就是調用它所在的記憶體中的方法。是以,輸出animal breathe,也就順理成章了。

正如很多學員所想,在例1-1的程式中,我們知道pAn實際指向的是fish類的對象,我們希望輸出的結果是魚的呼吸方法,即調用fish類的breathe方法。這個時候,就該輪到虛函數登場了。

        前面輸出的結果是因為編譯器在編譯的時候,就已經确定了對象調用的函數的位址,要解決這個問題就要使用遲綁定(late binding)技術。當編譯器使用遲綁定時,就會在運作時再去确定對象的類型以及正确的調用函數。而要讓編譯器采用遲綁定,就要在基類中聲明函數時使用virtual關鍵字(注意,這是必須的,很多學員就是因為沒有使用虛函數而寫出很多錯誤的例子),這樣的函數我們稱為虛函數。一旦某個函數在基類中聲明為virtual,那麼在所有的派生類中該函數都是virtual,而不需要再顯式地聲明為virtual。

下面修改例1-1的代碼,将animal類中的breathe()函數聲明為virtual,如下:

[cpp]

view plain copy print ?

  1. #include <iostream.h>  
  2. class animal  
  3. {  
  4. public:  
  5.     void sleep()  
  6.     {  
  7.         cout<<”animal sleep”<<endl;  
  8.     }  
  9.     virtual void breathe()  
  10.     {  
  11.         cout<<”animal breathe”<<endl;  
  12.     }  
  13. };  
  14. class fish:public animal  
  15. {  
  16. public:  
  17.     void breathe()  
  18.     {  
  19.         cout<<”fish bubble”<<endl;  
  20.     }  
  21. };  
  22. void main()  
  23. {  
  24.     fish fh;  
  25.     animal *pAn=&fh; // 隐式類型轉換  
  26.     pAn->breathe();  
  27. }  
c++多态性之虛函數與虛表指針
#include <iostream.h>
class animal
{
public:
    void sleep()
    {
        cout<<"animal sleep"<<endl;
    }
    virtual 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();
}
           

        大家可以再次運作這個程式,你會發現結果是“fish bubble”,也就是根據對象的類型調用了正确的函數。

那麼當我們将breathe()聲明為virtual時,在背後發生了什麼呢?

       編譯器在編譯的時候,發現animal類中有虛函數,此時編譯器會為每個包含虛函數的類建立一個虛表(即vtable),該表是一個一維數組,在這個數組中存放每個虛函數的位址。對于例1-2的程式,animal和fish類都包含了一個虛函數breathe(),是以編譯器會為這兩個類都建立一個虛表,(即使子類裡面沒有virtual函數,但是其父類裡面有,是以子類中也有了)如下圖所示:

c++多态性之虛函數與虛表指針

        那麼如何定位虛表呢?編譯器另外還為每個類的對象提供了一個虛表指針(即vptr),這個指針指向了對象所屬類的虛表。在程式運作時,根據對象的類型去初始化vptr,進而讓vptr正确的指向所屬類的虛表,進而在調用虛函數時,就能夠找到正确的函數。對于例1-2的程式,由于pAn實際指向的對象類型是fish,是以vptr指向的fish類的vtable,當調用pAn->breathe()時,根據虛表中的函數位址找到的就是fish類的breathe()函數。

       正是由于每個對象調用的虛函數都是通過虛表指針來索引的,也就決定了虛表指針的正确初始化是非常重要的。換句話說,在虛表指針沒有正确初始化之前,我們不能夠去調用虛函數。那麼虛表指針在什麼時候,或者說在什麼地方初始化呢?

        答案是在構造函數中進行虛表的建立和虛表指針的初始化。還記得構造函數的調用順序嗎,在構造子類對象時,要先調用父類的構造函數,此時編譯器隻“看到了”父類,并不知道後面是否後還有繼承者,它初始化父類對象的虛表指針,該虛表指針指向父類的虛表。當執行子類的構造函數時,子類對象的虛表指針被初始化,指向自身的虛表。對于例2-2的程式來說,當fish類的fh對象構造完畢後,其内部的虛表指針也就被初始化為指向fish類的虛表。在類型轉換後,調用pAn->breathe(),由于pAn實際指向的是fish類的對象,該對象内部的虛表指針指向的是fish類的虛表,是以最終調用的是fish類的breathe()函數。

要注意:對于虛函數調用來說,每一個對象内部都有一個虛表指針,該虛表指針被初始化為本類的虛表。是以在程式中,不管你的對象類型如何轉換,但該對象内部的虛表指針是固定的,是以呢,才能實作動态的對象函數調用,這就是C++多态性實作的原理。

總結(基類有虛函數):

1. 每一個類都有虛表。

2. 虛表可以繼承,如果子類沒有重寫虛函數,那麼子類虛表中仍然會有該函數的位址,隻不過這個位址指向的是基類的虛函數實作。如果基類有3個虛函數,那麼基類的虛表中就有三項(虛函數位址),派生類也會有虛表,至少有三項,如果重寫了相應的虛函數,那麼虛表中的位址就會改變,指向自身的虛函數實作。如果派生類有自己的虛函數,那麼虛表中就會添加該項。

3. 派生類的虛表中虛函數位址的排列順序和基類的虛表中虛函數位址排列順序相同。

        這就是C++中的多态性。當C++編譯器在編譯的時候,發現animal類的breathe()函數是虛函數,這個時候C++就會采用遲綁定(late binding)技術。也就是編譯時并不确定具體調用的函數,而是在運作時,依據對象的類型(在程式中,我們傳遞的fish類對象的位址)來确認調用的是哪一個函數,這種能力就叫做C++的多态性。我們沒有在breathe()函數前加virtual關鍵字時,C++編譯器在編譯時就确定了哪個函數被調用,這叫做早期綁定(early binding)。

C++的多态性是通過遲綁定技術來實作的。

C++的多态性用一句話概括就是:在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運作時将會根據對象的實際類型來調用相應的函數。如果對象類型是派生類,就調用派生類的函數;如果對象類型是基類,就調用基類的函數。

虛函數是在基類中定義的,目的是不确定它的派生類的具體行為。例:

定義一個基類:class Animal//動物。它的函數為breathe()//呼吸。

再定義一個類class Fish//魚 。它的函數也為breathe()

再定義一個類class Sheep //羊。它的函數也為breathe()

為了簡化代碼,将Fish,Sheep定義成基類Animal的派生類。

然而Fish與Sheep的breathe不一樣,一個是在水中通過水來呼吸,一個是直接呼吸空氣。是以基類不能确定該如何定義breathe,是以在基類中隻定義了一個virtual breathe,它是一個空的虛函數。具本的函數在子類中分别定義。程式一般運作時,找到類,如果它有基類,再找它的基類,最後運作的是基類中的函數,這時,它在基類中找到的是virtual辨別的函數,它就會再回到子類中找同名函數。派生類也叫子類。基類也叫父類。這就是虛函數的産生,和類的多态性(breathe)的展現。

這裡的多态性是指類的多态性。

函數的多态性是指一個函數被定義成多個不同參數的函數,它們一般被存在頭檔案中,當你調用這個函數,針對不同的參數,就會調用不同的同名函數。例:Rect()//矩形。它的參數可以是兩個坐标點(point,point)也可能是四個坐标(x1,y1,x2,y2)這叫函數的多态性與函數的重載。

類的多态性,是指用虛函數和延遲綁定來實作的。函數的多态性是函數的重載。

        一般情況下(沒有涉及virtual函數),當我們用一個指針/引用調用一個函數的時候,被調用的函數是取決于這個指針/引用的類型。即如果這個指針/引用是基類對象的指針/引用就調用基類的方法;如果指針/引用是派生類對象的指針/引用就調用派生類的方法,當然如果派生類中沒有此方法,就會向上到基類裡面去尋找相應的方法。這些調用在編譯階段就确定了。

        當設計到多态性的時候,采用了虛函數和動态綁定,此時的調用就不會在編譯時候确定而是在運作時确定。不在單獨考慮指針/引用的類型而是看指針/引用的對象的類型來判斷函數的調用,根據對象中虛指針指向的虛表中的函數的位址來确定調用哪個函數。

from: http://hi.baidu.com/1021161795/blog/item/0ea7ea2ce518af414fc226ce.html

繼續閱讀