图解C++虚函数 虚函数表
圖解C++虛函數(shù)
2016年07月02日 17:47:17?海楓?閱讀數(shù):5181?標簽:?虛函數(shù)c++g++對象模型C++虛函數(shù)更多
個人分類:?C/C++/linux
版權(quán)聲明:本文為博主原創(chuàng)文章,承蒙轉(zhuǎn)載請注明作者和出處 https://blog.csdn.net/linyt/article/details/51811314
介紹
早在5年前寫過《從匯編層面深度剖析C++虛函數(shù)》一文,介紹C++的虛函數(shù)表和調(diào)用過程。最近在看OSv操作系統(tǒng)代碼,迫不得已看了C++11中的新語法,最后還是跳不出虛函數(shù)的五指山。本文盡量使用圖來解釋虛函數(shù)在類,繼承,多繼承各種場下的對象模型結(jié)構(gòu),以及虛函數(shù)實現(xiàn)多態(tài)綁定。
值得注意的是,不同編譯器生成的對象結(jié)構(gòu)和虛函數(shù)表稍為有一些不同,本文均采用gcc 5.3.0版本下的g++編譯器作為研究對象。
普通類
object類的定義
class object {int a;int b;public:object(): a(0), b(1) {}virtual void f() {} };- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
上述代碼中定義object類,定義兩個int成員,分別是a和b,然后定義了虛函數(shù)f。
object對象內(nèi)存結(jié)構(gòu)
下圖是定義兩個object對象o1和o2的內(nèi)存結(jié)構(gòu)。
所有帶虛函數(shù)類對象的首4字節(jié)為虛函數(shù)表(vtable)指針,object也是如此。object對象的第一個4字節(jié)為vtable指針,它指向object全局的虛函數(shù)表,每個類只需一個vtable表即可。o1和o2共享一個虛函數(shù)表。?
虛函數(shù)表的內(nèi)容依次是:object::f(),object::g()。
接下來8個字節(jié)是意想不到的東西,那就是object類的type_info對象,這由編譯器生成的對象結(jié)構(gòu),它與C++庫中的type_info內(nèi)存部局完全相同。?
g++編譯器將類的type_info對象信息放到了vtable表的尾部。
調(diào)用虛函數(shù)過程
下面圖片描述了object指針調(diào)用虛函數(shù)的過程。?
具體過程可解釋如下:
獲取type_info對象
C++的RTTI機制本該屬于別一個話題,不適合在虛函數(shù)中談?wù)?。但在具體實現(xiàn)過程中,編譯器將它和vtable合并到一起,所以還在有必要簡單討論RTTI機制。
由于type_info信息也是放到vtable里面,那可以認為typeid操作符是虛函數(shù)一部分,它在vtable也有一個offset.
下面是object對象獲取它的type_info引用的過程。
與其它虛函數(shù)調(diào)用類似,typeid返回的type_info對象就是vtable尾部的type_info對象。?
每個類只有一個type_inof對象,不能被修改,所以typeid操作符只能是返回const引用。
可以想象一下typeid(o).name()就是返回type_info對象是name成員指向的字符器串”6object”。
繼承類
父類和子類定義
下面代碼定義父類base和子類derive.
class base {int b; public:virtual void f() {}virtual void g() {} };class derive: public base {int d; public:virtual void g() {} };- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
base對象和derive對象內(nèi)存結(jié)構(gòu)
derive子類重寫了g()函數(shù),所以它的vtable中的第二項為derive::g(),而f()函數(shù)沒有重寫,所以第一項仍然是base::f()函數(shù)。
多態(tài)的實現(xiàn)
我們經(jīng)??吹竭@樣的代碼:
base *b = new derive(); b->g();- 1
- 2
在b->g()調(diào)用過程中,調(diào)用的是derive::g()函數(shù),而不是base::g(),是如何實現(xiàn)的呢?這其中的奧秘就是虛函數(shù)表中。詳見下圖。
b對象盡管是base*類型的,但它的地址跟new出來derive對象地址是同一個(后面多重繼承例子中就不是這樣子的了),所以在調(diào)用b->g()時,從vtable指向的虛函數(shù)中找第二項,它值為derive::g()函數(shù)的地址,所以最終調(diào)用的是derive::g()函數(shù)。
多重繼承
多重繼承是更復(fù)雜的一個場景,在多重繼承的情況下,子類指針向基類指針轉(zhuǎn)換時,它的地址是不一樣的,所以編譯必須生成一些額外代碼來做地址轉(zhuǎn)換。
多重繼承類定義
class base1 {int b1; public:virtual void f1() {}virtual void g1() {} };class base2 {int b2; public:virtual void f2() {}virtual void g2() {} };class base3 {int b3; public:virtual void f3() {}virtual void g3() {} };class derive: public base1, public base2, pbulic base3 {int d; public:virtual void f1() {}virtual void f2() {}virtual void f3() {} };- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
基類的對象內(nèi)存結(jié)構(gòu)
下圖分別定義base1, base2, base3基類對象,不需過多解釋。
派生類的對象內(nèi)存結(jié)構(gòu)
從這個圖開始,我們開始要燒腦了。下圖是derive對象d的內(nèi)存結(jié)構(gòu):
derive對象內(nèi)存結(jié)構(gòu)有以下幾個特點:
3.derive對象有前8字節(jié),也是base1基類所在坑的位置;它的vtable指針指向的虛函數(shù)表,供derive類型使用,也供base1類型使用。對于base1類型,只使用前兩項。而derive類型,則使用更多項
派生類向基類轉(zhuǎn)換的秘密
也許你知道,派生類對象轉(zhuǎn)基類對象轉(zhuǎn)換之后,這兩者的地址都是一樣的,而在多重繼承里面,這個結(jié)論就不對了。
從上面看到,派生類是將基類依次排列而成。所以派生類對象指針向第一個基類指針轉(zhuǎn)換時,兩者地址是一樣的;而第二個和第三個基類對象指針轉(zhuǎn)換時,它的地址就不一樣的。請看下圖:
派生類調(diào)用非重寫函數(shù)
以這兩行代碼為例?
derive *d = new derive();?
d->g2();
顯然,derive沒有重寫g2()函數(shù),所以它調(diào)用的是base2類的虛函數(shù)。?
其實,不管derive是否有重寫g2函數(shù),都是通過base2的虛函數(shù)表找出來的。具體過程如下圖所示:
由于g2函數(shù)是最早是由base2類定義的,所以d->g2()調(diào)用時,先從d對象中的base2虛函數(shù)表,查找g2偏移量(值為4)的表項,再調(diào)用。
但這里有個細節(jié)一定要注意的是,base2::g2函數(shù)的this指針是base2 *類型的,而這里的d是derive*類型的,需要先將derive?*指針轉(zhuǎn)換成base2*指針。這個轉(zhuǎn)換完成之后,指針值就增加8字節(jié)了。
多重繼承下的多態(tài)實現(xiàn)
這里詳細分析
base2 *b2 = new derive(); b2->f2();- 1
- 2
是如何實現(xiàn)從基類到派生類f2()函數(shù)的調(diào)用。
b2指針已指向了derive對象的base2部分,然后b2->f2()從base2-vtable對應(yīng)的虛函數(shù)表的第一項,找到了non-virtual thunk to derive::f2(),然后調(diào)用。
咦,這里不應(yīng)該是derive::f2()嗎,那個non-virtual thunk to derive::f2()是什么鬼?
答案是和this指針強相關(guān)。
derive::f2()函數(shù)的this指針肯定是derive*類型的,而這里的b2是base2*類型,不能直接調(diào)用。
non-virtual thunk to derive::f2()代碼其實是兩行匯編,它完成出b2指針從base*類型轉(zhuǎn)換成derive*類型的功能,也即地址減去8。
小結(jié)
其實我想只用圖表將C++虛函數(shù)全部表達出來,但當我畫出來之后,發(fā)現(xiàn)很多細節(jié)不用文字稍作說明,不是很難明白。
其實這里說的C++虛函數(shù)原理跟你之前了解的應(yīng)該是一致的,只是很難技術(shù)細節(jié)你沒有想過而已,但不管理怎么樣,我們一起學(xué)習(xí)吧。
后繼再跟大家分析,菱形繼承和虛繼承場景下,虛函數(shù)的技術(shù)細節(jié)。
-
膽識與智慧:?博主,請問我可以轉(zhuǎn)載你的這篇文章嗎?(1個月前#2樓)查看回復(fù)(1)
-
Q943381546:?看了你的文檔收獲很多,個人認為有點小缺陷?!耙赃@兩行代碼為例 derive *d = new derive(); d->g2(); 其實,不管derive是否有重寫g2函數(shù),都是通過base2的虛函數(shù)表找出來的?!比绻侵匦潞瘮?shù),根據(jù)c++函數(shù)查找優(yōu)先級,找到的是d的g2函數(shù),應(yīng)該是用d指針直接調(diào)用g2,不會退化到父類指針。(10個月前#1樓)查看回復(fù)(1)
總結(jié)
以上是生活随笔為你收集整理的图解C++虚函数 虚函数表的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: C/C++杂记:虚函数的实现的基本原理
- 下一篇: c++面试题【转】 面经