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