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 的指針了,怎麼調用的呢?
若是你想知道緣由,請關注這兩個帖子:
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》),看完你會明白不少。