C++虛函數及虛函數表解析


原文鏈接:http://www.keepsimply.org/2012/07/11/cpp-vtable/

作者:獨酌逸醉
時間:2012.07.11

聲明:
  本文內容由自互聯網資源(見參考資料)、個人的一些 C++ 學習感悟、個人實踐整理而成。文章僅以技術學習和交流為目的。如果您發現了文中的錯誤,或者您有的不同的見解,可以給我留言或者給我發郵件,我們共同探 討。如果您覺得我的文章侵犯到您的權益,請聯系我(chinajiezhang@gmail.com),以便我做相應的處理。最后,如需轉載,可不必標明 出處。但一定要全文轉載,保證參考鏈接的完整性,這是對別人寫作的基本尊重。謝謝合作!

寫博緣由:
  1.對C++多態內部機制了解的渴望;
  2.眼過千遍,不如手過一遍;
  3.整理成文,幫助自己記憶;不求幫到他人,只求不會誤導。


一、背景知識(一些基本概念)

虛函數(Virtual Function):在基類中聲明為 virtual 並在一個或多個派生類中被重新定義的成員函數。
純虛函數(Pure Virtual Function):基類中沒有實現體的虛函數稱為純虛函數(有純虛函數的基類稱為虛基類)。
C++  “虛函數”的存在是為了實現面向對象中的“多態”,即父類類別的指針(或者引用)指向其子類的實例,然后通過父類的指針(或者引用)調用實際子類的成員函數。通過動態賦值,實現調用不同的子類的成員函數(動態綁定)。正是因為這種機制,把析構函數聲明為“虛函數”可以防止在內存泄露。
實例:

#include <iostream>
using namespace std;

class base_class
{
public:
    base_class()
    {
    }
    virtual ~base_class()
    {
    }

    int normal_func()
    {
        cout << "This is  base_class's normal_func()" << endl;
        return 0;
    }
    virtual int virtual_fuc()
    {
        cout << "This is  base_class's virtual_fuc()" << endl;
        return 0;
    }

};

class drived_class1 : public base_class
{
public:
    drived_class1()
    {
    }
    virtual ~drived_class1()
    {
    }

    int normal_func()
    {
        cout << "This is  drived_class1's normal_func()" << endl;
        return 0;
    }
    virtual int virtual_fuc()
    {
        cout << "This is  drived_class1's virtual_fuc()" << endl;
        return 0;
    }
};

class drived_class2 : public base_class
{
public:
    drived_class2()
    {
    }
    virtual ~drived_class2()
    {
    }

    int normal_func()
    {
        cout << "This is  drived_class2's normal_func()" << endl;
        return 0;
    }
    virtual int virtual_fuc()
    {
        cout << "This is  drived_class2's virtual_fuc()" << endl;
        return 0;
    }
};

int main()
{
    base_class * pbc = NULL;
    base_class bc;
    drived_class1 dc1;
    drived_class2 dc2;

    pbc = &bc;
    pbc->normal_func();
    pbc->virtual_fuc();

    pbc = &dc1;
    pbc->normal_func();
    pbc->virtual_fuc();

    pbc = &dc2;
    pbc->normal_func();
    pbc->virtual_fuc();
    return 0;
}

輸出結果:

This is  base_class's normal_func()
This is  base_class's virtual_fuc()
This is  base_class's normal_func()
This is  drived_class1's virtual_fuc()
This is  base_class's normal_func()
This is  drived_class2's virtual_fuc()

 

假如將 base_class 類中的 virtual_fuc() 寫成下面這樣(純虛函數,虛基類):

// 無實現體
virtual int virtual_fuc() = 0;

那么 virtual_fuc() 是一個純虛函數,base_class 就是一個虛基類:不能實例化(就是不能用它來定義對象),只能聲明指針或者引用。讀者可以自行測試,這里不再給出實例。


虛函數表(Virtual Table,V-Table):使用 V-Table 實現 C++ 的多態。在這個表中,主要是一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其真實反應實際的函數。這樣,在有虛函數的類的實例中分配了指向這個表的指針的內存,所以,當用父類的指針來操作一個子類的時候,這張虛函數表就顯得尤為重要了,它就像一個地圖一樣,指明了實際所應該調用的函數。
編譯器應該保證虛函數表的指針存在於對象實例中最前面的位置(這是為了保證取到虛函數表的有最高的性能——如果有多層繼承或是多重繼承的情況下)。 這意味着可以通過對象實例的地址得到這張虛函數表,然后就可以遍歷其中函數指針,並調用相應的函數。

二、無繼承時的虛函數表

#include <iostream>
using namespace std;

class base_class
{
public:
    virtual void v_func1()
    {
        cout << "This is base_class's v_func1()" << endl;
    }
    virtual void v_func2()
    {
        cout << "This is base_class's v_func2()" << endl;
    }
    virtual void v_func3()
    {
        cout << "This is base_class's v_func3()" << endl;
    }
};

int main()
{
    // 查看 base_class 的虛函數表
    base_class bc;
    cout << "base_class 的虛函數表首地址為:" << (int*)&bc << endl; // 虛函數表地址存在對象的前四個字節
    cout << "base_class 的 第一個函數首地址:" << (int*)*(int*)&bc+0 << endl; // 指針運算看不懂?沒關系,一會解釋給你聽
    cout << "base_class 的 第二個函數首地址:" << (int*)*(int*)&bc+1 << endl;
    cout << "base_class 的 第三個函數首地址:" << (int*)*(int*)&bc+2 << endl;
    cout << "base_class 的 結束標志: " << *((int*)*(int*)&bc+3) << endl;
    
    // 通過函數指針調用函數,驗證正確性
    typedef void(*func_pointer)(void);
    func_pointer fp = NULL;
    fp = (func_pointer)*((int*)*(int*)&bc+0); // v_func1()
    fp();
    fp = (func_pointer)*((int*)*(int*)&bc+1); // v_func2()
    fp();
    fp = (func_pointer)*((int*)*(int*)&bc+2); // v_func3()
    fp();
    return 0;
}

輸出結果:

base_class 的虛函數表首地址為:0x22ff0c
base_class 的 第一個函數首地址:0x472c98
base_class 的 第二個函數首地址:0x472c9c
base_class 的 第三個函數首地址:0x472ca0
base_class 的虛函數表結束標志: 0
This is base_class's v_func1()
This is base_class's v_func2()
This is base_class's v_func3()


簡單的解釋一下代碼中的指針轉換:
&bc:獲得 bc 對象的地址
(int*)&bc: 類型轉換,獲得虛函數表的首地址。這里使用 int* 的原因是函數指針的大小的 4byte,使用 int* 可以使得他們每次的偏移量保持一致(sizeof(int*) = 4,32-bit機器)。
*(int*)&bc:解指針引用,獲得虛函數表。
(int*)*(int*)&bc+0:和上面相同的類型轉換,獲得虛函數表的第一個虛函數地址。
(int*)*(int*)&bc+1:同上,獲得第二個函數地址。
(int*)*(int*)&bc+2:同上,獲得第三個函數地址。
*((int*)*(int*)&bc+3:獲得虛函數表的結束標志,所以這里我解引用了。和我們使用鏈表的情況是一樣的,虛函數表當然也需要一個結束標志。
typedef void(*func_pointer)(void):定義一個函數指針,參數和返回值都是 void。
*((int*)*(int*)&bc+0):找到第一個函數,注意這里需要解引用。
對於指針的轉換,我就解釋這么多了。下面的文章,我不再做解釋,相信大家可以舉一反三。如果你覺得很費解的話,我不建議繼續去看這篇文章了,建議你去補一補基礎(《C和指針》是一本很好的選擇哦!)。

通過上面的例子的嘗試和輸出結果,我們可以得出下面的布局圖示:

三、單一繼承下的虛函數表
3.1子類沒有父類的虛函數(陳皓文章中用了“覆蓋”一詞,我覺得太合理,但是我又找不到更合理的詞語,所以就用一個句子代替了。^-^)

#include <iostream>
using namespace std;

class base_class
{
public:
    virtual void v_func1()
    {
        cout << "This is base_class's v_func1()" << endl;
    }
    virtual void v_func2()
    {
        cout << "This is base_class's v_func2()" << endl;
    }
    virtual void v_func3()
    {
        cout << "This is base_class's v_func3()" << endl;
    }
};
class dev_class : public base_class
{
public:
    virtual void v_func4()
    {
        cout << "This is dev_class's v_func4()" << endl;
    }
    virtual void v_func5()
    {
        cout << "This is dev_class's v_func5()" << endl;
    }
};

int main()
{
    // 查看 dev_class 的虛函數表
    dev_class dc;
    cout << "dev_class 的虛函數表首地址為:" << (int*)&dc << endl;
    cout << "dev_class 的 第一個函數首地址:" << (int*)*(int*)&dc+0 << endl;
    cout << "dev_class 的 第二個函數首地址:" << (int*)*(int*)&dc+1 << endl;
    cout << "dev_class 的 第三個函數首地址:" << (int*)*(int*)&dc+2 << endl;
    cout << "dev_class 的 第四個函數首地址:" << (int*)*(int*)&dc+3 << endl;
    cout << "dev_class 的 第五個函數首地址:" << (int*)*(int*)&dc+4 << endl;
    cout << "dev_class 的虛函數表結束標志: " << *((int*)*(int*)&dc+5) << endl;
    // 通過函數指針調用函數,驗證正確性
    typedef void(*func_pointer)(void);
    func_pointer fp = NULL;
    for (int i=0; i<5; i++) {
        fp = (func_pointer)*((int*)*(int*)&dc+i);
        fp();
    }
    return 0;
}

 輸出結果:

dev_class 的虛函數表首地址為:0x22ff0c
dev_class 的 第一個函數首地址:0x472d10
dev_class 的 第二個函數首地址:0x472d14
dev_class 的 第三個函數首地址:0x472d18
dev_class 的 第四個函數首地址:0x472d1c
dev_class 的 第五個函數首地址:0x472d20
dev_class 的虛函數表結束標志: 0
This is base_class's v_func1()
This is base_class's v_func2()
This is base_class's v_func3()
This is dev_class's v_func4()
This is dev_class's v_func5()

通過上面的例子的嘗試和輸出結果,我們可以得出下面的布局圖示:


可以看出,v-table中虛函數是順序存放的,先基類后派生類。

3.2子類有重寫父類的虛函數

include <iostream>
using namespace std;

class base_class
{
public:
    virtual void v_func1()
    {
        cout << "This is base_class's v_func1()" << endl;
    }
    virtual void v_func2()
    {
        cout << "This is base_class's v_func2()" << endl;
    }
    virtual void v_func3()
    {
        cout << "This is base_class's v_func3()" << endl;
    }
};
class dev_class : public base_class
{
public:
    virtual void v_func1()
    {
        cout << "This is dev_class's v_func1()" << endl;
    }
    virtual void v_func2()
    {
        cout << "This is dev_class's v_func2()" << endl;
    }
    virtual void v_func4()
    {
        cout << "This is dev_class's v_func4()" << endl;
    }
    virtual void v_func5()
    {
        cout << "This is dev_class's v_func5()" << endl;
    }
};

int main()
{
    // 查看 dev_class 的虛函數表
    dev_class dc;
    cout << "dev_class 的虛函數表首地址為:" << (int*)&dc << endl;
    cout << "dev_class 的 第一個函數首地址:" << (int*)*(int*)&dc+0 << endl;
    cout << "dev_class 的 第二個函數首地址:" << (int*)*(int*)&dc+1 << endl;
    cout << "dev_class 的 第三個函數首地址:" << (int*)*(int*)&dc+2 << endl;
    cout << "dev_class 的 第四個函數首地址:" << (int*)*(int*)&dc+3 << endl;
    cout << "dev_class 的 第五個函數首地址:" << (int*)*(int*)&dc+4 << endl;
    cout << "dev_class 的虛函數表結束標志: " << *((int*)*(int*)&dc+5) << endl;
    // 通過函數指針調用函數,驗證正確性
    typedef void(*func_pointer)(void);
    func_pointer fp = NULL;
    for (int i=0; i<5; i++) {
        fp = (func_pointer)*((int*)*(int*)&dc+i);
        fp();
    }
    return 0;
}

輸出結果:

dev_class 的虛函數表首地址為:0x22ff0c
dev_class 的 第一個函數首地址:0x472d50
dev_class 的 第二個函數首地址:0x472d54
dev_class 的 第三個函數首地址:0x472d58
dev_class 的 第四個函數首地址:0x472d5c
dev_class 的 第五個函數首地址:0x472d60
dev_class 的虛函數表結束標志: 0
This is dev_class's v_func1()
This is dev_class's v_func2()
This is base_class's v_func3()
This is dev_class's v_func4()
This is dev_class's v_func5()

 

通過上面的例子的嘗試和輸出結果,我們可以得出下面的布局圖示:

可以看出當派生類中 dev_class 中重寫了父類 base_class 的前兩個虛函數(v_func1,v_func2)之后,使用派生類的虛函數指針代替了父類的虛函數。未重寫的父類虛函數位置沒有發生變化。

不知道看到這里,你心里有沒有一個小問題?至少我是有的。看下面的代碼:

virtual void v_func1()
{
    base_class::v_func1();
    cout << "This is dev_class's v_func1()" << endl;
}

既然派生類的虛函數表中用 dev_class::v_func1 指針代替了 base_class::v_func1,假如我顯示的調用 base_class::v_func1,會不會有錯呢?答案是沒錯的,可以正確的調用!不是覆蓋了嗎?dev_class 已經不知道 base_class::v_func1 的指針了,怎么調用的呢?
如果你想知道原因,請關注這兩個帖子:

http://stackoverflow.com/questions/11426970/why-can-a-derived-class-virtual-function-call-a-base-class-virtual-fuction-how

http://topic.csdn.net/u/20120711/14/fa9cfba2-8814-4119-8290-99e6af2c21f4.html?seed=742904136&r=79093804#r_79093804

四、多重繼承下的虛函數表


4.1子類沒有重寫父類的虛函數

#include <iostream>
using namespace std;

class base_class1
{
public:
    virtual void bc1_func1()
    {
        cout << "This is bc1_func1's v_func1()" << endl;
    }
};

class base_class2
{
public:
    virtual void bc2_func1()
    {
        cout << "This is bc2_func1's v_func1()" << endl;
    }
};

class dev_class : public base_class1, public base_class2
{
public:
    virtual void dc_func1()
    {
        cout << "This is dc_func1's dc_func1()" << endl;
    }
};

int main()
{
    dev_class dc;
    cout << "dc 的虛函數表 bc1_vt 地址:" << (int*)&dc << endl;
    cout << "dc 的虛函數表 bc1_vt 第一個虛函數地址:" << (int*)*(int*)&dc+0 << endl;
    cout << "dc 的虛函數表 bc1_vt 第二個虛函數地址:" << (int*)*(int*)&dc+1 << endl;
    cout << "dc 的虛函數表 bc1_vt 結束標志:" << *((int*)*(int*)&dc+2) << endl;
    cout << "dc 的虛函數表 bc2_vt 地址:" << (int*)&dc+1 << endl;
    cout << "dc 的虛函數表 bc2_vt 第一個虛函數首地址::" << (int*)*((int*)&dc+1)+0 << endl;
    cout << "dc 的虛函數表 bc2_vt 結束標志:" << *((int*)*((int*)&dc+1)+1) << endl;
    // 通過函數指針調用函數,驗證正確性
    typedef void(*func_pointer)(void);
    func_pointer fp = NULL;
    // bc1_vt
    fp = (func_pointer)*((int*)*(int*)&dc+0);
    fp();
    fp = (func_pointer)*((int*)*(int*)&dc+1);
    fp();
    // bc2_vt
    fp = (func_pointer)*(((int*)*((int*)&dc+1)+0));
    fp();
    return 0;
}

輸出結果:

dc 的虛函數表 bc1_vt 地址:0x22ff08
dc 的虛函數表 bc1_vt 第一個虛函數地址:0x472d38
dc 的虛函數表 bc1_vt 第二個虛函數地址:0x472d3c
dc 的虛函數表 bc1_vt 結束標志:-4
dc 的虛函數表 bc2_vt 地址:0x22ff0c
dc 的虛函數表 bc2_vt 第一個虛函數首地址::0x472d48
dc 的虛函數表 bc2_vt 結束標志:0
This is bc1_func1's v_func1()
This is dc_func1's dc_func1()
This is bc2_func1's v_func1()

通過上面的例子的嘗試和輸出結果,我們可以得出下面的布局圖示:


可以看出:多重繼承的情況,會為每一個基類建一個虛函數表。派生類的虛函數放到第一個虛函數表的后面。

陳皓在他的文章中有這么一句話:“這個結束標志(虛函數表)的值在不同的編譯器下是不同的。在WinXP+VS2003下,這個值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,這個值是如果1,表示還有下一個虛函數表,如果值是0,表示是最后一個虛函數表。”。那么,我在 Windows 7 + Code::blocks 10.05 下嘗試,這個值是如果是 -4,表示還有下一個虛函數表,如果是0,表示是最后一個虛函數表。
我在 Windows 7 + vs2010 下嘗試,兩個值都是 0 。

4.2子類重寫了父類的虛函數

#include <iostream>
using namespace std;

class base_class1
{
public:
    virtual void bc1_func1()
    {
        cout << "This is base_class1's bc1_func1()" << endl;
    }
    virtual void bc1_func2()
    {
        cout << "This is base_class1's bc1_func2()" << endl;
    }
};

class base_class2
{
public:
    virtual void bc2_func1()
    {
        cout << "This is base_class2's bc2_func1()" << endl;
    }
    virtual void bc2_func2()
    {
        cout << "This is base_class2's bc2_func2()" << endl;
    }
};

class dev_class : public base_class1, public base_class2
{
public:
    virtual void bc1_func1()
    {
        cout << "This is dev_class's bc1_func1()" << endl;
    }
    virtual void bc2_func1()
    {
        cout << "This is dev_class's bc2_func1()" << endl;
    }
    virtual void dc_func1()
    {
        cout << "This is dev_class's dc_func1()" << endl;
    }
};

int main()
{
    dev_class dc;
    cout << "dc 的虛函數表 bc1_vt 地址:" << (int*)&dc << endl;
    cout << "dc 的虛函數表 bc1_vt 第一個虛函數地址:" << (int*)*(int*)&dc+0 << endl;
    cout << "dc 的虛函數表 bc1_vt 第二個虛函數地址:" << (int*)*(int*)&dc+1 << endl;
    cout << "dc 的虛函數表 bc1_vt 第三個虛函數地址:" << (int*)*(int*)&dc+2 << endl;
    cout << "dc 的虛函數表 bc1_vt 第四個虛函數地址:" << (int*)*(int*)&dc+3 << endl;
    cout << "dc 的虛函數表 bc1_vt 結束標志:" << *((int*)*(int*)&dc+4) << endl;
    cout << "dc 的虛函數表 bc2_vt 地址:" << (int*)&dc+1 << endl;
    cout << "dc 的虛函數表 bc2_vt 第一個虛函數首地址::" << (int*)*((int*)&dc+1)+0 << endl;
    cout << "dc 的虛函數表 bc2_vt 第二個虛函數首地址::" << (int*)*((int*)&dc+1)+1 << endl;
    cout << "dc 的虛函數表 bc2_vt 結束標志:" << *((int*)*((int*)&dc+1)+2) << endl;
    // 通過函數指針調用函數,驗證正確性
    typedef void(*func_pointer)(void);
    func_pointer fp = NULL;
    // bc1_vt
    fp = (func_pointer)*((int*)*(int*)&dc+0);
    fp();
    fp = (func_pointer)*((int*)*(int*)&dc+1);
    fp();
    fp = (func_pointer)*((int*)*(int*)&dc+2);
    fp();
    fp = (func_pointer)*((int*)*(int*)&dc+3);
    fp();
    // bc2_vt
    fp = (func_pointer)*(((int*)*((int*)&dc+1)+0));
    fp();
    fp = (func_pointer)*(((int*)*((int*)&dc+1)+1));
    fp();
    return 0;
}

 

輸出結果:

dc 的虛函數表 bc1_vt 地址:0x22ff08
dc 的虛函數表 bc1_vt 第一個虛函數地址:0x472e28
dc 的虛函數表 bc1_vt 第二個虛函數地址:0x472e2c
dc 的虛函數表 bc1_vt 第三個虛函數地址:0x472e30
dc 的虛函數表 bc1_vt 第四個虛函數地址:0x472e34
dc 的虛函數表 bc1_vt 結束標志:-4
dc 的虛函數表 bc2_vt 地址:0x22ff0c
dc 的虛函數表 bc2_vt 第一個虛函數首地址::0x472e40
dc 的虛函數表 bc2_vt 第一個虛函數首地址::0x472e44
dc 的虛函數表 bc2_vt 結束標志:0
This is dev_class's bc1_func1()
This is base_class1's bc1_func2()
This is dev_class's bc2_func1()
This is dev_class's dc_func1()
This is dev_class's bc2_func1()
This is base_class2's bc2_func2()

通過上面的例子的嘗試和輸出結果,我們可以得出下面的布局圖示:


是不是感覺很亂?其實一點都不亂!就是兩個單繼承而已。把多余的部分(派生類的虛函數)增加到第一個虛函數表的最后,CB(Code::Blocks)是這樣實現的。我試了一下,vs2010不是這樣實現的,讀者可以自己嘗試一下。本文只針對 CB 來探討。

有人覺得多重繼承不好理解。我想如果你明白了它的虛函數表是怎么樣的,也就沒什么不好理解了吧。
也許還有人會說,不同的編譯器實現方式是不一樣的,我去研究某一種編譯器的實現有什么意義呢?我個人理解是這樣的:1.實現方式是不一樣的,但是它們的實現結果是一樣的(多態)。2.無論你了解虛函數表或者不了解虛函數表,我相信你都很少會用到它。但是當你了解了它的實現機制之后,你再去看多態,再去寫虛函數的時候[作為你一個coder],相信你的感覺是不一樣的。你會感覺很透徹,不會有絲毫的猶豫。3.學習編譯器這種處理問題的方式(思想),這才是最重要的。[好像扯遠了,^-^]。
如果你了解了虛函數表之后,可以通過虛函數表直接訪問類的方法,這種訪問是不受成員的訪問權限限制的(private,protected)。這樣做是很危險的,但是確實是可以這樣做的。這也是C++為什么很危險的語言的一個原因……

看完之后,你不是產生了許多其他的問題呢?至少我有了幾個問題[我這人問題特別多。^-^]比如:
1.訪問權限是怎么實現的?編譯器怎么知道哪些函數是public,哪些是protected?
2.虛函數調用是通過虛函數表實現的,那么非虛成員函數存放在哪里?是怎么實現的呢?
3.類的成員存放在什么位置?怎么繼承的呢?[這是對象布局問題,=.=]
你知道的越多,你感覺你知道的越少。推薦大家一本書吧,《深度探索C++對象模型》(英文名字是《Inside to C++ Object Model》),看完你會明白很多。


 

感謝閱讀,下面列出參考資料[順便給大家推薦一下陳皓的博客吧:http://coolshell.cn/,經常去逛逛,會學到很多,至少我是這樣覺得的。^-^]:
1.http://blog.csdn.net/haoel/article/details/1948051/
2.http://baike.baidu.com/view/3750123.htm
3.http://www.cnblogs.com/wirelesser/archive/2008/03/09/1097463.html

 

2012.07.20 update:
1.本文只針對 Windows 7 Code::blocks 10.05 進行測試和講解;
2.不同的編譯器實現方式可能不同,比如 VS2010 和 CB 10.05 就有些不同,感興趣的朋友可自行測試。
感謝 Adoo 的提醒,文章中以上兩點有所提示,但是不是很明顯,確實應該很明確的說清楚這個問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM