C++ | 虛函數(shù)簡介
緣起
在上一篇文章中,測試代碼2 中的 pBaseA->AA(); 輸出的內(nèi)容很“奇怪”。其實,完全在情理之中。本文將簡單探究一下 c++ 中的虛函數(shù)實現(xiàn)機制。本文主要基于 vs2013 生成的 32 位代碼進行研究,相信其它編譯器(比如,gcc)的實現(xiàn)大同小異。
先從對象大小開始
假設我們有如下代碼,假設 int 占 4 字節(jié),指針占 4 字節(jié)。
#include?"stdafx.h"
#include?"stdlib.h"
#include?"stddef.h"
class?CBase
{
public:
????virtual?void?VFun1()?{?printf(__FUNCTION__?"\n");?}
????virtual?void?VFun2()?{?printf(__FUNCTION__?"\n");?}
????virtual?~CBase()?{?printf(__FUNCTION__?"\n");?}
????int?data;
};
class?CDerived?:?public?CBase
{
public:
????virtual?void?VFunNew()?{?printf(__FUNCTION__?"\n");?}
????virtual?void?VFun1()?override?{?printf(__FUNCTION__?"\n");?}
????virtual?~CDerived()?override?{?printf(__FUNCTION__?"\n");?}
};
int?_tmain(int?argc,?_TCHAR*?argv[])
{
????printf("sizeof?CBase?is:?%d,?offset?of?data?is?%d\n",
???????????sizeof(CBase),?offsetof(CBase,?data));
????system("pause");
????CBase*?pBase?=?new?CDerived();
????pBase->VFun1();
????pBase->VFun2();
????system("pause");
????return?0;
}
輸出結果如下圖:

有沒有覺得意外?從類定義可知,data 占 4 字節(jié),那另外的 4 字節(jié)是哪里來的呢?data 的偏移值不應該是 0 嗎?為什么是 4 呢?
內(nèi)存布局
如果一個類有虛函數(shù),編譯器會自動為這個類型的對象在頭部增加一個虛表指針(vftable),指向虛函數(shù)表。虛函數(shù)表中存放著一個個的虛函數(shù)。
CBase 和 CDerived 類對象的內(nèi)存布局如下:

注意:虛函數(shù)表中索引為
-1的地方指向了跟動態(tài)類型轉(zhuǎn)換相關的信息。
虛表指針的初始化
vftable 是在類的構造函數(shù)中初始化的。可以在 IDA 中分別查看 CBase 類 和 CDerived 類的構造函數(shù)的反匯編代碼。
CBase 構造函數(shù)的反匯編代碼如下(關鍵部分已注釋):

由反匯編代碼可知, CBase 的構造函數(shù)會把 CBase 對象開始的位置(存放虛表指針)設置為 CBase::vftable。
CDerived 構造函數(shù)的反匯編代碼如下(關鍵部分已注釋):

由反匯編代碼可知, CDerived 的構造函數(shù)會先調(diào)用 CBase 的構造函數(shù)進行基類部分的初始化,在 CBase 構造函數(shù)的內(nèi)部把 CDerived 對象開始的位置設置為 CBase::vftable,然后調(diào)用自身的初始化部分,會把 CDerived::vftable 的地址放到對象開始的位置,從而替換掉了 CBase 類的虛表指針。
虛函數(shù)表的內(nèi)容
了解完了虛表指針的初始化過程,再來看看 vftable 里面都有哪些內(nèi)容。
可以雙擊 ??_7CBase@@6B@ (或者直接按回車)跳轉(zhuǎn)到虛表所在的地方。如下圖:

說明:上側是 CBase 類的虛表內(nèi)容,下側是 CDerived 類的虛表內(nèi)容。
請注意圖片上側黃色高亮部分,也就是
vftable[-1]的地方,是跟動態(tài)類型轉(zhuǎn)換相關的信息,后面有機會介紹。
虛函數(shù)調(diào)用
理解了類對象的內(nèi)存布局及虛函數(shù)表之后,再理解虛函數(shù)的調(diào)用過程就比較簡單了。
有些 C++ 基礎的小伙伴兒都知道本例中的輸出結果應該如下圖所示:

直接看一下 pBase->VFun1() 和 pBase->VFun2() 對應的反匯編代碼就應該明白一切了。如下圖:

因為 pBase 指向的實際是 CDerived 類型的對象,所以虛表是 CDerived 類的。如下圖所示:

經(jīng)過以上的分析,輸出結果合情合理。
說明
本文只是拿了一個最最簡單的例子做演示。像多重繼承,虛繼承等比較復雜的情況,感興趣的小伙伴可以自行研究。
雖然這個例子很簡單,但是背后的機理值得了解清楚,非常有用。比如,當庫中的接口與庫頭文件不匹配的時候,很可能莫名其妙的就崩潰了。這時可以通過查看指針對應的虛表的內(nèi)容來查看庫中的虛函數(shù)都有哪些,跟頭文件對比后就可以比較準確的判斷是否是庫不匹配的問題。還可以根據(jù)虛表的內(nèi)容,猜測出基類指針指向的具體的子類對象的類型。
可以在 windbg 中使用 dps 命令快速打印,如下圖:

總結
虛表指針是在類的構造函數(shù)中初始化的,相應的代碼由編譯器自動生成。
在生成調(diào)用虛函數(shù)的代碼的時候,并沒有直接把虛函數(shù)地址寫死,而是通過虛表進行調(diào)用,多了一層間接層。
Any problem in computer science can be solved by anther layer of indirection.(計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決)
注意:如果通過對象調(diào)用虛函數(shù),會是另外一種情況,因為不存在多態(tài),直接使用函數(shù)地址進行調(diào)用就可以了。感興趣的小伙伴兒可以自行實驗。
參考資料
《深度探索 c++ 對象模型》
https://en.wikipedia.org/wiki/Fundamental_theorem_of_software_engineering
