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