1. 程式人生 > 實用技巧 >C++ 虛擬函式簡介

C++ 虛擬函式簡介

C++ 虛擬函式簡介

c++virtual functionvftable

緣起

上一篇文章中,測試程式碼2 中的 pBaseA->AA(); 輸出的內容很“奇怪”。其實,完全在情理之中。本文將簡單探究一下 c++ 中的虛擬函式實現機制。本文主要基於 vs2013 生成的 32 位程式碼進行研究,相信其它編譯器(比如,gcc)的實現大同小異。

先從物件大小開始

假設我們有如下程式碼,假設 int4 位元組,指標佔 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; }

輸出結果如下圖:


sizeof-ctest

有沒有覺得意外?從類定義可知,data4 位元組,那另外的 4 位元組是哪裡來的呢?data 的偏移值不應該是 0 嗎?為什麼是 4 呢?

記憶體佈局

如果一個類有虛擬函式,編譯器會自動為這個型別的物件在頭部增加一個虛表指標(vftable),指向虛擬函式表。虛擬函式表中存放著一個個的虛擬函式。

CBaseCDerived 類物件的記憶體佈局如下:


CBase-CDerived-memory-layout

注意:虛擬函式表中索引為 -1 的地方指向了跟動態型別轉換相關的資訊。

虛表指標的初始化

vftable 是在類的建構函式中初始化的。可以在 IDA 中分別檢視 CBase 類 和 CDerived 類的建構函式的反彙編程式碼。

CBase 建構函式的反彙編程式碼如下(關鍵部分已註釋):


view-CBase-constructor-in-ida

由反彙編程式碼可知, CBase 的建構函式會把 CBase 物件開始的位置(存放虛表指標)設定為 CBase::vftable

CDerived 建構函式的反彙編程式碼如下(關鍵部分已註釋):


view-CDerived-constructor-in-ida

由反彙編程式碼可知, CDerived 的建構函式會先呼叫 CBase 的建構函式進行基類部分的初始化,在 CBase 建構函式的內部把 CDerived 物件開始的位置設定為 CBase::vftable,然後呼叫自身的初始化部分,會把 CDerived::vftable 的地址放到物件開始的位置,從而替換掉了 CBase 類的虛表指標。

虛擬函式表的內容

瞭解完了虛表指標的初始化過程,再來看看 vftable 裡面都有哪些內容。

可以雙擊 ??_7CBase@@6B@ (或者直接按回車)跳轉到虛表所在的地方。如下圖:


view-vftable-content-in-ida

說明:上側是 CBase 類的虛表內容,下側是 CDerived 類的虛表內容。

請注意圖片上側黃色高亮部分,也就是 vftable[-1] 的地方,是跟動態型別轉換相關的資訊,後面有機會介紹。

虛擬函式呼叫

理解了類物件的記憶體佈局及虛擬函式表之後,再理解虛擬函式的呼叫過程就比較簡單了。

有些 C++ 基礎的小夥伴兒都知道本例中的輸出結果應該如下圖所示:


virtual-function-output

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


virutal-function-call

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


CDerived-memory-layout

經過以上的分析,輸出結果合情合理。

說明

本文只是拿了一個最最簡單的例子做演示。像多重繼承,虛繼承等比較複雜的情況,感興趣的小夥伴可以自行研究。

雖然這個例子很簡單,但是背後的機理值得了解清楚,非常有用。比如,當庫中的介面與庫標頭檔案不匹配的時候,很可能莫名其妙的就崩潰了。這時可以通過檢視指標對應的虛表的內容來檢視庫中的虛擬函式都有哪些,跟標頭檔案對比後就可以比較準確的判斷是否是庫不匹配的問題。還可以根據虛表的內容,猜測出基類指標指向的具體的子類物件的型別。

可以在 windbg 中使用 dps 命令快速列印,如下圖:


windbg-dps-view-vtable

總結

  • 虛表指標是在類的建構函式中初始化的,相應的程式碼由編譯器自動生成。

  • 在生成呼叫虛擬函式的程式碼的時候,並沒有直接把虛擬函式地址寫死,而是通過虛表進行呼叫,多了一層間接層。

  • Any problem in computer science can be solved by anther layer of indirection. (電腦科學領域的任何問題都可以通過增加一個間接的中間層來解決)

注意:如果通過物件呼叫虛擬函式,會是另外一種情況,因為不存在多型,直接使用函式低階進行呼叫就可以了。感興趣的小夥伴兒可以自行實驗。

參考資料

《深度探索 c++ 物件模型》

https://en.wikipedia.org/wiki/Fundamental_theorem_of_software_engineering