虛函數 多重繼承 動態綁定

class A {
public:
    virtual void vfunc1();
    virtual void vfunc2();
    void func1();
    void func2();
private:
    int m_data1, m_data2;
};

class B : public A {
public:
    virtual void vfunc1();
    void func1();
private:
    int m_data3;
};

class C: public B {
public:
    virtual void vfunc2();
    void func2();
private:
    int m_data1, m_data4;
};

假設我們定義一個類B的對象。由於bObject是類B的一個對象,故bObject包含一個虛表指針,指向類B的虛表。

int main() 
{
    B bObject;
}
  • 現在,我們聲明一個類A的指針p來指向對象bObject。雖然p是基類的指針只能指向基類的部分,但是虛表指針亦屬於基類部分,所以p可以訪問到對象bObject的虛表指針。bObject的虛表指針指向類B的虛表,所以p可以訪問到B vtbl。如圖3所示。
int main() 
{
    B bObject;
    A *p = & bObject;
}
  • 當我們使用p來調用vfunc1()函數時,會發生什麼現象?
int main() 
{
    B bObject;
    A *p = & bObject;
    p->vfunc1();
}
  •  

程序在執行p->vfunc1()時,會發現p是個指針,且調用的函數是虛函數,接下來便會進行以下的步驟。 
首先,根據虛表指針p->__vptr來訪問對象bObject對應的虛表。雖然指針p是基類A*類型,但是*__vptr也是基類的一部分,所以可以通過p->__vptr可以訪問到對象對應的虛表。 
然後,在虛表中查找所調用的函數對應的條目。由於虛表在編譯階段就可以構造出來了,所以可以根據所調用的函數定位到虛表中的對應條目。對於 p->vfunc1()的調用,B vtbl的第一項即是vfunc1對應的條目。 
最後,根據虛表中找到的函數指針,調用函數。從圖3可以看到,B vtbl的第一項指向B::vfunc1(),所以 p->vfunc1()實質會調用B::vfunc1()函數。

如果p指向類A的對象,情況又是怎麼樣?

int main() 
{
    A aObject;
    A *p = &aObject;
    p->vfunc1();
}
  •  

當aObject在創建時,它的虛表指針__vptr已設置爲指向A vtbl,這樣p->__vptr就指向A vtbl。vfunc1在A vtbl對應在條目指向了A::vfunc1()函數,所以 p->vfunc1()實質會調用A::vfunc1()函數。

可以把以上三個調用函數的步驟用以下表達式來表示:

(*(p->__vptr)[n])(p)

可以看到,通過使用這些虛函數表,即使使用的是基類的指針來調用函數,也可以達到正確調用運行中實際對象的虛函數。 
我們把經過虛表調用虛函數的過程稱爲動態綁定,其表現出來的現象稱爲運行時多態。動態綁定區別於傳統的函數調用,傳統的函數調用我們稱之爲靜態綁定,即函數的調用在編譯階段就可以確定下來了。

那麼,什麼時候會執行函數的動態綁定?這需要符合以下三個條件。

  • 通過指針來調用函數
  • 指針upcast向上轉型(繼承類向基類的轉換稱爲upcast,關於什麼是upcast,可以參考本文的參考資料)
  • 調用的是虛函數

如果一個函數調用符合以上三個條件,編譯器就會把該函數調用編譯成動態綁定,其函數的調用過程走的是上述通過虛表的機制。

五、總結

封裝,繼承,多態是面向對象設計的三個特徵,而多態可以說是面向對象設計的關鍵。C++通過虛函數表,實現了虛函數與對象的動態綁定,從而構建了C++面向對象程序設計的基石。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章