原文鏈接: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 的指針了,怎么調用的呢?
如果你想知道原因,請關注這兩個帖子:
四、多重繼承下的虛函數表
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 的提醒,文章中以上兩點有所提示,但是不是很明顯,確實應該很明確的說清楚這個問題。
