程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> C++入門知識 >> C++之虛函數表,函數表

C++之虛函數表,函數表

編輯:C++入門知識

C++之虛函數表,函數表


本文引自:http://songlee24.github.io/blog/2014/09/02/c-plus-plus-jin-jie-zhi-xu-han-shu-biao/

C++通過繼承(inheritance)和虛函數(virtual function)來實現多態性。所謂多態,簡單地說就是,將基類的指針或引用綁定到子類的實例,然後通過基類的指針或引用調用實際子類的成員函數(虛函數)。本文將介紹單繼承、多重繼承下虛函數的實現機制。

 

一、虛函數表

為了支持虛函數機制,編譯器為每一個擁有虛函數的類的實例創建了一個虛函數表(virtual table),這個表中有許多的槽(slot),每個槽中存放的是虛函數的地址。虛函數表解決了繼承、覆蓋、添加虛函數的問題,保證其真實反應實際的函數。

為了能夠找到 virtual table,編譯器在每個擁有虛函數的類的實例中插入了一個成員指針 vptr,指向虛函數表。下面是一個例子:

123456789101112131415161718192021222324
class Base
{
public:
virtual void x() { cout << "Base::x()" << endl; }
virtual void y() { cout << "Base::y()" << endl; }
virtual void z() { cout << "Base::z()" << endl; }
};
typedef void(*pFun)(void);
int main()
{
Base b;
int* vptr = (int*)&b;                     // 虛函數表地址
pFunfunc1 = (pFun)*((int*)*vptr);        // 第一個函數
pFunfunc2 = (pFun)*((int*)*vptr+1);      // 第二個函數
pFunfunc3 = (pFun)*((int*)*vptr+2);      // 第三個函數
func1();     // 輸出Base::x()
func2();     // 輸出Base::y()
func3();     // 輸出Base::z()
return 0;
}

上面定義了一個Base類,其中有三個虛函數。我們將Base類對象取址 &b 並強制轉換為 int*,取得虛函數表的地址。然後對虛函數表的地址取值 *vptr 並強轉為 int*,即取得第一個虛函數的地址了。將第一個虛函數的地址加1,取得第二個虛函數的地址,再加1即取得第三個虛函數的地址。

注意,之所以可以通過對象實例的地址得到虛函數表,是因為 vptr 指針位於對象實例的最前面(這是由編譯器決定的,主要是為了保證取到虛函數表有最高的性能——如果有多層繼承或是多重繼承的情況下)。如圖所示:

在VS2012中加斷點進行Debug可以查看到虛函數表:

二、單繼承時的虛函數表

1、無虛函數覆蓋

假如現有單繼承關系如下:

123456789101112131415
classBase
{
public:
virtual void x() { cout << "Base::x()" << endl; }
virtual void y() { cout << "Base::y()" << endl; }
virtual void z() { cout << "Base::z()" << endl; }
};
class Derive : public Base
{
public:
virtual void x1() { cout << "Derive::x1()" << endl; }
virtual void y1() { cout << "Derive::y1()" << endl; }
virtual void z1() { cout << "Derive::z1()" << endl; }
};

在這個單繼承的關系中,子類沒有重寫父類的任何方法,而是加入了三個新的虛函數。Derive類實例的虛函數表布局如圖示:

  • Derive class 繼承了 Base class 中的三個虛函數,准確的說,是該函數實體的地址被拷貝到 Derive 實例的虛函數表對應的 slot 之中。
  • 新增的 虛函數 置於虛函數表的後面,並按聲明順序存放。
  • 2、有虛函數覆蓋

    如果在繼承關系中,子類重寫了父類的虛函數:

    123456789101112131415
    class Base
    {
    public:
    virtual void x() { cout << "Base::x()" << endl; }
    virtual void y() { cout << "Base::y()" << endl; }
    virtual void z() { cout << "Base::z()" << endl; }
    };
    class Derive : public Base
    {
    public:
    virtual void x() { cout << "Derive::x()" << endl; }  // 重寫
    virtual void y1() { cout << "Derive::y1()" << endl; }
    virtual void z1() { cout << "Derive::z1()" << endl; }
    };
    

    則Derive類實例的虛函數表布局為:

    相比於無覆蓋的情況,只是把 Derive::x() 覆蓋了Base::x(),即第一個槽的函數地址發生了變化,其他的沒有變化。

    這時,如果通過綁定了子類對象的基類指針調用函數 x(),會執行 Derive 版本的 x(),這就是多態。

     

    三、多重繼承時的虛函數表

    1、無虛函數覆蓋

    現有如下的多重繼承關系,子類沒有覆蓋父類的虛函數:

    12345678910111213141516171819202122
    class Base1
    {
    public:
    virtual void x() { cout << "Base1::x()" << endl; }
    virtual void y() { cout << "Base1::y()" << endl; }
    virtual void z() { cout << "Base1::z()" << endl; }
    };
    class Base2
    {
    public:
    virtual void x() { cout << "Base2::x()" << endl; }
    virtual void y() { cout << "Base2::y()" << endl; }
    virtual void z() { cout << "Base2::z()" << endl; }
    };
    class Derive : public Base1, public Base2
    {
    public:
    virtual void x1() { cout << "Derive::x1()" << endl; }
    virtual void y1() { cout << "Derive::y1()" << endl; }
    };
    

    對於 Derive 實例 d 的虛函數表布局,如下圖:

    可以看出:

    • 每個基類子對象對應一個虛函數表。
    • 派生類中新增的虛函數放到第一個虛函數表的後面。

    測試代碼(VS2012):

    12345678910111213141516171819202122232425262728293031
    typedef void(*pFun)(void);
    int main()
    {
    Derive b;
    int** vptr = (int**)&b;                     // 虛函數表地址
    // virtual table 1
    pFuntable1_func1 = (pFun)*((int*)*vptr+0);         // vptr[0][0]
    pFuntable1_func2 = (pFun)*((int*)*vptr+1);         // vptr[0][1]
    pFuntable1_func3 = (pFun)*((int*)*vptr+2);         // vptr[0][2]
    pFuntable1_func4 = (pFun)*((int*)*vptr+3);         // vptr[0][3]
    pFuntable1_func5 = (pFun)*((int*)*vptr+4);         // vptr[0][4]
    // virtual table 2
    pFuntable2_func1 = (pFun)*((int*)*(vptr+1)+0);     // vptr[1][0]
    pFuntable2_func2 = (pFun)*((int*)*(vptr+1)+1);     // vptr[1][1]
    pFuntable2_func3 = (pFun)*((int*)*(vptr+1)+2);     // vptr[1][2]
    // call
    table1_func1();
    table1_func2();
    table1_func3();
    table1_func4();
    table1_func5();
    table2_func1();
    table2_func2();
    table2_func3();
    return0;
    }
    

    不同的編譯器對 virtual table 的實現不同,經測試,在 g++ 中需要這樣:

    1234567891011
    // virtual table 1
    pFuntable1_func1 = (pFun)*((int*)*vptr+0);         // vptr[0][0]
    pFuntable1_func2 = (pFun)*((int*)*vptr+2);         // vptr[0][2]
    pFuntable1_func3 = (pFun)*((int*)*vptr+4);         // vptr[0][4]
    pFuntable1_func4 = (pFun)*((int*)*vptr+6);         // vptr[0][6]
    pFuntable1_func5 = (pFun)*((int*)*vptr+8);         // vptr[0][8]
    // virtual table 2
    pFuntable2_func1 = (pFun)*((int*)*(vptr+1)+0);     // vptr[1][0]
    pFuntable2_func2 = (pFun)*((int*)*(vptr+1)+2);     // vptr[1][2]
    pFuntable2_func3 = (pFun)*((int*)*(vptr+1)+4);     // vptr[1][4]
    

    2、有虛函數覆蓋

    將上面的多重繼承關系稍作修改,讓子類重寫基類的 x() 函數:

    12345678910111213141516171819202122
    class Base1
    {
    public:
    virtual void x() { cout << "Base1::x()" << endl; }
    virtual void y() { cout << "Base1::y()" << endl; }
    virtual void z() { cout << "Base1::z()" << endl; }
    };
    class Base2
    {
    public:
    virtual void x() { cout << "Base2::x()" << endl; }
    virtual void y() { cout << "Base2::y()" << endl; }
    virtual void z() { cout << "Base2::z()" << endl; }
    };
    class Derive : public Base1, public Base2
    {
    public:
    virtual void x() { cout << "Derive::x()" << endl; }     // 重寫
    virtual void y1() { cout << "Derive::y1()" << endl; }
    };
    

    這時 Derive 實例的虛函數表布局會變成下面這個樣子:

    相比於無覆蓋的情況,只是將Derive::x()覆蓋了Base1::x()Base2::x()而已,你可以自己寫測試代碼測試一下,這裡就不再贅述了。

    注:若虛函數是 private 或 protected 的,我們照樣可以通過訪問虛函數表來訪問這些虛函數,即上面的測試代碼一樣能運行。


    附:編譯器對指針的調整

    在多重繼承下,我們可以將子類實例綁定到任一父類的指針(或引用)上。以上述有覆蓋的多重繼承關系為例:

    1
    2
    3
    Deriveb;
    Base1* ptr1 = &b;   // 指向 b 的初始地址
    Base2* ptr2 = &b;   // 指向 b 的第二個子對象
    
    • 因為 Base1 是第一個基類,所以 ptr1 指向的是 Derive 對象的起始地址,不需要調整指針(偏移)。
    • 因為 Base2 是第二個基類,所以必須對指針進行調整,即加上一個 offset,讓 ptr2 指向 Base2 子對象。
    • 當然,上述過程是由編譯器完成的。

    當然,你可以在VS2012裡通過Debug看出 ptr1 和 ptr2 是不同的,我們可以這樣子:

    1234
    Base1* b1 = (Base1*)ptr2;
    b1->y();                   // 輸出 Base2::y()
    Base2* b2 = (Base2*)ptr1;
    b2->y();                   // 輸出 Base1::y()
    

    其實,通過某個類型的指針訪問某個成員時,編譯器只是根據類型的定義查找這個成員所在偏移量,用這個偏移量獲取成員。由於 ptr2 本來就指向 Base2 子對象的起始地址,所以b1->y()調用到的是Base2::y(),而 ptr1 本來就指向 Base1 子對象的起始地址(即 Derive對象的起始地址),所以b2->y()調用到的是Base1::y()

    1. 上一頁:
    2. 下一頁:
    Copyright © 程式師世界 All Rights Reserved