本文將簡單探究一下 c++ 中的虛函式實作機制,主要基于 vs2013 生成的 32 位代碼進行研究,相信其它編譯器(比如, gcc )的實作大同小異,
先從物件大小開始
假設我們有如下代碼,假設 int 占 4 位元組,指標占 4 位元組,
#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 位元組,那另外的 4 位元組是哪里來的呢? data的偏移值不應該是 0 嗎?為什么是 4 呢?
記憶體布局
如果一個類有虛函式,編譯器會自動為這個型別的物件在頭部增加一個虛表指標( vftable),指向虛函式表,虛函式表中存放著一個個的虛函式,
CBase 和 CDerived 類物件的記憶體布局如下:

注意:虛函式表中索引為 -1 的地方指向了跟動態型別轉換相關的資訊,
虛表指標的初始化
vftable 是在類的建構式中初始化的,可以在 IDA 中分別查看 CBase 類 和 CDerived 類的建構式的反匯編代碼,
CBase 建構式的反匯編代碼如下(關鍵部分已注釋):

由反匯編代碼可知, CBase 的建構式會把 CBase 物件開始的位置(存放虛表指標)設定為 CBase::vftable ,
CDerived 建構式的反匯編代碼如下(關鍵部分已注釋):

由反匯編代碼可知, CDerived 的建構式會先呼叫 CBase 的建構式進行基類部分的初始化,在 CBase 建構式的內部把 CDerived 物件開始的位置設定為 CBase::vftable ,然后呼叫自身的初始化部分,會把 CDerived::vftable 的地址放到物件開始的位置,從而替換掉了 CBase類的虛表指標,
虛函式表的內容
了解完了虛表指標的初始化程序,再來看看 vftable 里面都有哪些內容,
可以雙擊 ??_7CBase@@6B@ (或者直接按回車)跳轉到虛表所在的地方,如下圖:

說明:上側是 CBase 類的虛表內容,下側是 CDerived 類的虛表內容,
請注意圖片上側黃色高亮部分,也就是 vftable[-1] 的地方,是跟動態型別轉換相關的資訊,后面有機會介紹,
虛函式呼叫
理解了類物件的記憶體布局及虛函式表之后,再理解虛函式的呼叫程序就比較簡單了,
有些 C++ 基礎的小伙伴兒都知道本例中的輸出結果應該如下圖所示:

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

因為 pBase 指向的實際是 CDerived 型別的物件,所以虛表是 CDerived 類的,如下圖所示:

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

總結
虛表指標是在類的建構式中初始化的,相應的代碼由編譯器自動生成,
在生成呼叫虛函式的代碼的時候,并沒有直接把虛函式地址寫死,而是通過虛表進行呼叫,多了一層間接層,
Any problem in computer science can be solved by anther layer of indirection. (計算機科學領域的任何問題都可以通過增加一個間接的中間層來解決)
注意:如果通過物件呼叫虛函式,會是另外一種情況,因為不存在多型,直接使用函式低級進行呼叫就可以了,感興趣的小伙伴兒可以自行實驗,

如果你想快速掌握C/C++編程,小編推薦我的C語言/C++編程學習基地【點擊進入】!
都是學編程小伙伴們,帶你入個門還是簡簡單單啦,一起學習,一起加油~
還有許多學習資料和視頻,相信你會喜歡的!
涉及:編程入門、游戲編程、課程設計、黑客等等......
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/68294.html
標籤:C
上一篇:Qt5通過msvc編譯qtmqtt來添加MQTT模塊
下一篇:C語言01
