天天看點

ATL炒冷飯學習之二:繞不開的虛函數ATL炒冷飯學習之二:繞不開的虛函數 

ATL炒冷飯學習之二:繞不開的虛函數

一、前言

      對于C++的程式員,多态機制是再熟悉不過的了;多态就是用父類型别的指針指向其子類的執行個體,然後通過父類的指針調用實際子類的成員函數。對于多态機制是如何實作的原理,想必對于一個c++程式員來說是小菜一碟。對于即将學習COM技術的人,将會明白com接口有将這一機制運用到了極緻,是以,多态技術也是學習COM技術的前提和基礎,不知道多态機制的人,是永運無法學習明白COM的。是以,在開始學習COM時,是非常有必要專門複習一下C++的多态機制是如何實作的。

二、多态

從概念上說,多态就是用父類型别的指針指向其子類的執行個體,然後通過父類的指針調用實際子類的成員函數。通過下面的代碼先體會一下多态:

#include <iostream>

using namespace std;

class AClass

{

public:

    void PrintMessage()

    {

        cout<<"this i s AClass"<<endl;

    }

};

class BClass : public AClass

{

public:

    void PrintMessage()

    {

        cout<<"this i s BClass"<<endl;

    }

};

int main()

{

    AClass *pAClassObj = new BClass();

   pAClassObj ->PrintMessage();

}

有興趣的讀者可以編譯運作上面的代碼,運作結果是:this i s AClass!,明顯這不是多态的行為。

ATL炒冷飯學習之二:繞不開的虛函數ATL炒冷飯學習之二:繞不開的虛函數 

假如我們對AClass類的PrintMessage函數前面加入關鍵字virtual,具體代碼如下:

#include <iostream>

using namespace std;

class AClass

{

public:

    virtual void PrintMessage()

    {

        cout<<"this i s AClass"<<endl;

    }

};

class BClass : public AClass

{

public:

    void PrintMessage()

    {

        cout<<"this i s BClass"<<endl;

    }

};

int main()

{

    AClass *pAClassObj = new BClass();

    pAClassObj ->PrintMessage();

}

再次編譯執行此代碼。此時,代碼的運作結果為:this i s BClass!這個時候就表現出來了多态行為。通過這個簡單的例子,你應該體會到多态的概念了。

ATL炒冷飯學習之二:繞不開的虛函數ATL炒冷飯學習之二:繞不開的虛函數 

三、虛函數表

       多态機制的關鍵就是在于虛函數表,也就是vtbl。當定義一個類并且類中包含虛函數時,其實也就定義了一張虛函數表,沒有虛函數的類是不包含虛函數表的,隻有該類被執行個體化時,才會将這個表配置設定到這個執行個體的記憶體中;在這張虛函數表中,存放了每個虛函數的位址;它就像一個地圖一樣,指明了實際所應該調用的函數。如下面定義一個類:

class CVirtualBase

{

public:

     CVirtualBase(){}

    CVirtualBase(int i, int f) : m_intVar(i), m_floatVar(f){}

     virtual void virtualBaseMethod1() { cout<<"this is virtualBaseMethod1!"<<endl; }

     virtual void virtualBaseMethod2() { cout<<"this is virtualBaseMethod2!"<<endl; }

     virtual void virtualBaseMethod3() { cout<<"this is virtualBaseMethod3!"<<endl; }

     void baseMethod4() { cout<<"this is baseMethod4!"<<endl; }

private:

     int m_intVar;

     float m_floatVar;

};

這樣的一個類,當你去定義這個類的執行個體時,編譯器會給這個類配置設定一個成員變量,該變量指向這個虛函數表,這個虛函數表中的每一項都會記錄對應的虛函數的位址;如下圖:

這個類的變量還沒有被初始化時,就像上圖那樣,變量的值都是随機值,而指向虛拟函數表的指針__vfptr中對應的虛函數位址也是錯誤的位址;隻有等我們真正的完成了這個變量的聲明和初始化時,這些值才能被正确的初始化,如下圖:

ATL炒冷飯學習之二:繞不開的虛函數ATL炒冷飯學習之二:繞不開的虛函數 

       從上圖中就可以看到,初始化完成以後,指向虛函數表的__vfptr指針中的元素都被賦予了正确的虛函數值,分别指向了在類中定義的三個虛函數。也看到了,__vfptr指針定義的位置也比m_intVar和m_floatVar變量的位置靠前;在C++編譯器中,它保證虛函數表的指針存在于對象執行個體中最前面的位置,這主要是為了在多層繼承或是多重繼承的情況下,能以高性能取到這張虛函數表,然後進行周遊,查找對應的虛函數指針,進行對應的調用。

      基類定義了虛函數,子類可以重寫該函數,當子類重新定義了父類的虛函數後,父類指針根據賦給它的不同的子類指針,動态地調用屬于子類的該函數,且這樣的函數調用是無法在編譯器期間确認的,而是在運作期确認,也叫做遲綁定。

下面先看看虛函數實作的模型:

ATL炒冷飯學習之二:繞不開的虛函數ATL炒冷飯學習之二:繞不開的虛函數 

    結合上面中模型的特點,并對記憶體存取和空間進行了優化。在此模型中,non static 資料成員被放置到對象内部,static資料成員, static and nonstatic 函數成員均被放到對象之外。對于虛函數的支援則分兩步完成:

1.每一個class産生一堆指向虛函數的指針,放在表格之中。這個表格稱之為虛函數表(virtual table,vtbl)。

2.每一個對象被添加了一個指針,指向相關的虛函數表vtbl。通常這個指針被稱為vptr。vptr的設定(setting)和重置(resetting)都由每一個class的構造函數,析構函數和拷貝指派運算符自動完成。

       另外,虛函數表位址的前面設定了一個指向type_info的指針,RTTI(Run Time Type Identification)運作時類型識别是有編譯器在編譯器生成的特殊類型資訊,包括對象繼承關系,對象本身的描述,RTTI是為多态而生成的資訊,是以隻有具有虛函數的對象在會生成。

     編譯器處理虛函數的方法是:為每個類對象添加一個隐藏成員,隐藏成員中儲存了一個指向函數位址數組的指針,稱為虛表指針(vptr),這種數組成為虛函數表(virtual function table, vtbl),即,每個類使用一個虛函數表,每個類對象用一個虛表指針。

        舉個例子:基類對象包含一個虛表指針,指向基類中所有虛函數的位址表。派生類對象也将包含一個虛表指針,指向派生類虛函數表。看下面兩種情況:

  •         如果派生類重寫了基類的虛方法,該派生類虛函數表将儲存重寫的虛函數的位址,而不是基類的虛函數位址。
  •         如果基類中的虛方法沒有在派生類中重寫,那麼派生類将繼承基類中的虛方法,而且派生類中虛函數表将儲存基類中未被重寫的虛函數的位址。注意,如果派生類中定義了新的虛方法,則該虛函數的位址也将被添加到派生類虛函數表中。

下面的圖檔展現了上述的底層實作機制:

ATL炒冷飯學習之二:繞不開的虛函數ATL炒冷飯學習之二:繞不開的虛函數 

在C++primer第六版第十三章的虛函數的工作原理編譯器處理虛函數的方法是:

      給每個對象添加一個指針,存放了指向虛函數表的位址,虛函數表存儲了為類對象進行聲明的虛函數位址。比如基類對象包含一個指針,該指針指向基類所有虛函數的位址表,派生類對象将包含一個指向獨立位址表的指針,如果派生類提供了虛函數的新定義,該虛函數表将儲存新函數的位址,如果派生類沒有重新定義虛函數,該虛函數表将儲存函數原始版本的位址。如果派生類定義了新的虛函數,則該函數的位址将被添加到虛函數表中,注意虛函數無論多少個都隻需要在對象中添加一個虛函數表的位址。

ATL炒冷飯學習之二:繞不開的虛函數ATL炒冷飯學習之二:繞不開的虛函數 

調用虛函數時,程式将檢視存儲在對象中的虛函數表位址,轉向相應的虛函數表,使用類聲明中定義的第幾個虛函數,程式就使用數組的第幾個函數位址,并執行該函數。

使用虛函數後的變化:

(1) 對象将增加一個存儲位址的空間(32位系統為4位元組,64位為8位元組)。

(2) 每個類編譯器都建立一個虛函數位址表

(3) 對每個函數調用都需要增加在表中查找位址的操作。

虛函數的注意事項

總結前面的内容

(1) 基類方法中聲明了方法為虛後,該方法在基類派生類中是虛的。

(2) 若使用指向對象的引用或指針調用虛方法,程式将根據對象類型來調用方法,而不是指針的類型。

(3)如果定義的類被用作基類,則應将那些要在派生類中重新定義的類方法聲明為虛。

構造函數不能為虛函數。

基類的析構函數應該為虛函數。

友元函數不能為虛,因為友元函數不是類成員,隻有類成員才能是虛函數。

如果派生類沒有重定義函數,則會使用基類版本。

重新定義繼承的方法若和基類的方法不同(協變除外),會将基類方法隐藏;如果基類聲明方法被重載,則派生類也需要對重載的方法重新定義,否則調用的還是基類的方法。

總結

      總結了這麼多關于虛函數表的内容,看似和COM的和口沒有多大的關系;但是,毋庸置疑的是這一切都是COM的基礎,COM實質就是接口,而接口的背後C++的虛函數,隻有很好的了解了虛函數,才可以順利的了解後面COM中的技術術語和深層次的概念。

繼續閱讀