1. 程式人生 > >C++ 虛函數表淺析

C++ 虛函數表淺析

tab -s names 寫到 style quest ios mes 沒有

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

虛函數(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_classs normal_func()
This is  base_classs virtual_fuc()
This is  base_classs normal_func()
This is  drived_class1s virtual_fuc()
This is  base_classs normal_func()
This is  drived_class2s 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_classs v_func1()
This is base_classs v_func2()
This is base_classs 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。

對於指針的轉換,我就解釋這麽多了。下面的文章,我不再做解釋,相信大家可以舉一反三。如果你覺得很費解的話,我不建議繼續去看這篇文章了,建議你去補一補基礎(《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_classs v_func1()
This is base_classs v_func2()
This is base_classs v_func3()
This is dev_classs v_func4()
This is dev_classs 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_classs v_func1()
This is dev_classs v_func2()
This is base_classs v_func3()
This is dev_classs v_func4()
This is dev_classs 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_func1s v_func1()
This is dc_func1s dc_func1()
This is bc2_func1s 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_classs bc1_func1()
This is base_class1s bc1_func2()
This is dev_classs bc2_func1()
This is dev_classs dc_func1()
This is dev_classs bc2_func1()
This is base_class2s bc2_func2()
技術分享圖片

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

技術分享圖片

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

有人覺得多重繼承不好理解。我想如果你明白了它的虛函數表是怎麽樣的,也就沒什麽不好理解了吧。

也許還有人會說,不同的編譯器實現方式是不一樣的,我去研究某一種編譯器的實現有什麽意義呢?我個人理解是這樣的:

  • 實現方式是不一樣的,但是它們的實現結果是一樣的(多態)。
  • 無論你了解虛函數表或者不了解虛函數表,我相信你都很少會用到它。但是當你了解了它的實現機制之後,你再去看多態,再去寫虛函數的時候[作為你一個coder],相信你的感覺是不一樣的。你會感覺很透徹,不會有絲毫的猶豫。
  • 學習編譯器這種處理問題的方式(思想),這才是最重要的。[好像扯遠了,^-^]。

如果你了解了虛函數表之後,可以通過虛函數表直接訪問類的方法,這種訪問是不受成員的訪問權限限制的(private,protected)。這樣做是很危險的,但是確實是可以這樣做的。這也是C++為什麽很危險的語言的一個原因……

寫到這裏,文章也就基本結束了。作為讀者的你,看完之後,你不是產生了許多其他的問題呢?作為筆者的我,有了新幾個問題[我這人問題特別多。^-^]比如:

  • 訪問權限是怎麽實現的?編譯器怎麽知道哪些函數是public,哪些是protected?
  • 虛函數調用是通過虛函數表實現的,那麽非虛成員函數存放在哪裏?是怎麽實現的呢?
  • 類的成員存放在什麽位置?怎麽繼承的呢?[這是對象布局問題,=.=]

你知道的越多,你感覺你知道的越少。推薦大家一本書吧,《深度探索C++對象模型》(英文名字是《Inside to C++ Object Model》),看完你會明白很多。


感謝閱讀,下面列出參考資料[順便給大家推薦一下陳皓的博客吧:http://coolshell.cn/,經常去逛逛,會學到很多,至少我是這樣覺得的。^-^]:

  • http://blog.csdn.net/haoel/article/details/1948051/
  • http://baike.baidu.com/view/3750123.htm
  • http://www.cnblogs.com/wirelesser/archive/2008/03/09/1097463.html

C++ 虛函數表淺析