11-c++虛擬函數 虛擬函數 C++虛函數及虛函數表解析

虛擬函數

#include "stdio.h"html

 

class A{ios

  public:ide

    void print()函數

    {佈局

      printf("%s","this is A\n");post

    }性能

};學習

 

class B:public A{測試

  public:this

    void print()

    {

      printf("%s","this is B\n");

    } 

};

 

int main()

{

  int i=1;

  A a;  

  B b;

  a.print();  //輸出this is A

  b.print();  //輸出this is B

  A* point_a=&a;

  point_a->print();

  return 0;

}

上面標成藍色的兩行

若是是:

  A* point_a=&a;

  的時候輸出結果是:

  This is A

若是是:

  A* point_a=&b;

  的時候輸出結果是:

  This is A

若是是:

  B* point_a=&b;

  的時候輸出結果是:

  This is B

若是是:

  B* point_a=&a;  

  這個編譯是會報錯的

 

 

若是都寫成虛擬函數

#include "stdio.h"

 

class A{

  public:

    virtual void print()

    {

      printf("%s","this is A\n");

    }

};

 

class B:public A{

  public:

    virtual void print()

    {

      printf("%s","this is B\n");

    } 

};

 

int main()

{

  int i=1;

  A a;  

  B b;

  a.print();//輸出this is A

  b.print();//輸出this is B

  A* point_a=&a;

  point_a->print();

  return 0;

}

上面標成藍色的兩行

若是是:

  A* point_a=&a;

  的時候輸出結果是:

  This is A

若是是:

  A* point_a=&b;

  的時候輸出結果是:

  This is B

若是是:

  B* point_a=&b;

  的時候輸出結果是:

  This is B

若是是:

  B* point_a=&a;  

  這個編譯是會報錯的

 

 

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

 
 

 


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

虛函數(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++ 的多態。在這個表中,主要是一個類的虛函數的地址表,這張表解決了繼承、覆蓋的問題,保證其真實反應實際的函數。這樣,在有虛函數的類的實例中分配了指向這個表的指針的內存,因此,當用父類的指針來操做一個子類的時候,這張虛函數表就顯得尤其重要了,它就像一個地圖同樣,指明瞭實際所應該調用的函數。
編譯器應該保證虛函數表的指針存在於對象實例中最前面的位置(這是爲了保證取到虛函數表的有最高的性能——若是有多層繼承或是多重繼承的狀況下)。 這意味着能夠經過對象實例的地址獲得這張虛函數表,而後就能夠遍歷其中函數指針,並調用相應的函數。

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;
    }
};

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、單一繼承下的虛函數表
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、多重繼承下的虛函數表


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》),看完你會明白不少。

相關文章
相關標籤/搜索