C++小結:多態(1) --實現原理

多      態

     1、動態多態的實現原理

    2、多態的類別及實現方法

    3、動態多態的四種情況

(1)動態多態實現原理(類的多態性、運行時多態)

1.1 多態:一個接口,多種方法

1.2 動態多態主要由繼承和虛函數實現,通過父類的指針或引用,指向子類的對象,在調用函數時可以調用到正確的版本

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

1.4 虛函數被調用的時候,到底調用哪個版本,在編譯的時候無法確定,只有在執行時才能確定,稱爲動態綁定

1.5 純虛函數,沒有函數體,不需要實現,在子類中實現純虛函數的具體功能(虛函數 = 0)

1.6 擁有純虛函數的類,稱爲抽象類,抽象類提供了不同種類對象的一個通用接口。

1.7 不能創建抽象類的對象,因爲抽象類裏面的純虛函數沒有實現。

舉例(無虛函數)

#include <iostream>
using namespace std;  
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 f;  
       Animal *a=&f; // 隱式類型轉換  
       a -> breathe();  
}  
輸出:Animal breathe

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

a是Animal的指針,指向子類Fish的對象f,使用a調用breathe,則調用的是父類版本,原因如下:

1、編譯角度

C++編譯器在編譯的時候,要確定每個對象調用的函數(此時要求此函數是非虛函數)的地址,這稱爲早期綁定(early binding),當我們將Fish類的對象f的地址賦給a時,C++編譯器進行了類型轉換,此時C++編譯器認爲指針變量a保存的就是Animal對象的地址。當在main()函數中執行a->breathe()時,調用的當然就是Animal的breathe函數。

2、內存角度

Fish對象內存模型,如下圖所示:


       構造Fish類的對象時,首先要調用Animal類的構造函數去構造繼承自Animal類的對象,然後才調用Fish類的構造函數完成自身擴展部分的構造,從而構造出一個完整的Fish對象。當將Fish類的對象轉換爲Animal類型時,該對象就被認爲是原對象整個內存模型的上半部分,也就是圖中的“animal的對象所佔內存”。那麼當利用類型轉換後的對象指針去調用它的方法時,當然也就是調用它所在的內存中的方法。因此,輸出Animal breathe。
++++++++++++++++++++-------------------------------+++++++++++++++++++++++
        前面輸出的結果是因爲編譯器在編譯的時候,就已經確定了對象調用的函數的地址,要解決這個問題就要使用遲綁定(late binding)技術。當編譯器使用遲綁定時,就會在運行時再去確定對象的類型以及正確的調用函數。而要讓編譯器採用遲綁定,就要在基類中聲明函數時使用virtual關鍵字,這樣的函數稱爲虛函數。一旦某個函數在基類中聲明爲virtual,那麼在所有的派生類中該函數都是virtual,而不需要再顯式地聲明爲virtual。

舉例(有虛函數)

#include <iostream>
using namespace std;  
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 f;  
       Animal *a=&f; // 隱式類型轉換  
       a -> breathe();  
}  
編譯器在編譯的時候,發現Animal類中有虛函數,此時編譯器會爲每個包含虛函數的類創建一個虛表(即vtable),該表是一個一維數組,在這個數組中存放每個虛函數的地址。對於上例程序,Animal和Fish類都包含了一個虛函數breathe(),因此編譯器會爲這兩個類都建立一個虛表,(即使子類裏面沒有virtual函數,但是其父類裏面有,所以子類中也有了)如下圖所示:


        那麼如何定位虛表呢?編譯器另外還爲每個類的對象提供了一個虛表指針(即vptr),這個指針指向了對象所屬類的虛表。在程序運行時,根據對象的類型去初始化vptr,從而讓vptr正確的指向所屬類的虛表,從而在調用虛函數時,就能夠找到正確的函數。由於a實際指向的對象類型是Fish,因此vptr指向Fish類的vtable,當調用a->breathe()時,根據虛表中的函數地址找到的就是Fish類的breathe()函數。
       正是由於每個對象調用的虛函數都是通過虛表指針來索引的,也就決定了虛表指針的正確初始化是非常重要的。換句話說,在虛表指針沒有正確初始化之前,我們不能夠去調用虛函數。那麼虛表指針在什麼時候,或者說在什麼地方初始化呢?
       答案是在構造函數中進行虛表的創建和虛表指針的初始化。在構造子類對象時,要先調用父類的構造函數,此時編譯器只“看到了”父類,並不知道後面是否還有繼承者,它初始化父類對象的虛表指針,該虛表指針指向父類的虛表。當執行子類的構造函數時,子類對象的虛表指針被初始化,指向自身的虛表。當Fish類的f對象構造完畢後,其內部的虛表指針也就被初始化爲指向Fish類的虛表。在類型轉換後,調用a->breathe(),由於a實際指向的是Fish類的對象,該對象內部的虛表指針指向的是Fish類的虛表,因此最終調用的是Fish類的breathe()函數。

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

發佈了31 篇原創文章 · 獲贊 3 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章