C++ 中的虛函數表及虛函數執行原理詳解

為瞭實現虛函數,C++ 使用瞭虛函數表來達到延遲綁定的目的。虛函數表在動態/延遲綁定行為中用於查詢調用的函數。

盡管要描述清楚虛函數表的機制會多費點口舌,但其實其本身還是比較簡單的。

首先,每個包含虛函數的類(或者繼承自的類包含瞭虛函數)都有一個自己的虛函數表。這個表是一個在編譯時確定的靜態數組。虛函數表包含瞭指向每個虛函數的函數指針以供類對象調用。

其次,編譯器還在基類中定義瞭一個隱藏指針,我們稱為 *__vptr,*__vptr 是在類實例創建時自動設置的,以指向類的虛函數表。*__vptr 是一個真正的指針,這和 *this 指針不同,*this 指針實際是一個函數參數,使編譯器來達到自引用的目的。

結果就是,每個類對象都會多分配一個指針的大小,並且 *__vptr 是被派生類繼承的。

如果你不清楚這些組件是怎麼配合運作的,看下面的例子:

class Base
{
public:
  virtual void function1() {};
  virtual void function2() {};
};
 
class D1: public Base
{
public:
  virtual void function1() {};
};
 
class D2: public Base
{
public:
  virtual void function2() {};
};

因為這裡有 3 個類,編譯器會創建 3 個虛函數表。

然後編譯器會在使用瞭虛函數的最上層基類中定義一個隱藏指針。盡管這個過程編譯器會自動處理,但我們還是通過下面的例子來說明指針添加的位置:

class Base
{
public:
  FunctionPointer *__vptr;
  virtual void function1() {};
  virtual void function2() {};
};
 
class D1: public Base
{
public:
  virtual void function1() {};
};
 
class D2: public Base
{
public:
  virtual void function2() {};
};

*__vptr 在類對象創建的時候會設置成指向類的虛函數表。例如,類型 Base 被實例化的時候,*__vptr 就指向 Base 的虛函數表。類型 D1 或者 D2 被實例化的時候,*__vptr 就指向 D1 或者 D2 的虛函數表。

現在我們來看下虛函數表是怎麼創建的。因為示例中每個類僅有 2 個虛函數,所以每個虛函數表會存放兩個函數指針(分別指向 function1() 和 function2())。

Base 對象的虛函數表最簡單。Base 對象隻能訪問 Base 類型的成員,不能訪問 D1 或者 D2 的函數。所以 Base 的虛函數表中的兩個指針分別指向 Base::function1() 和 Base::function2()。

D1 的虛函數表稍復雜點,D1 對象能夠訪問 D1 以及 Base 的成員。D1 重寫瞭 function1(),但沒有重寫 function2(),所以 D1 的虛函數表中的兩個指針分別指向 D1::function1() 和 Base::function2()。

D2 的虛函數表同理 D1,包含瞭分別指向 Base::function1() 和 D2::function2() 的指針。

考慮如果創建 D1 對象時會發生什麼:

int main()
{
  D1 d1;
}

因為 d1 是 D1 類型對象,d1 有它自己的 *__vptr 指向 D1 類型的虛函數表。

現在創建一個 Base 類型指針 *dPtr 指向 d1:

int main()
{
  D1 d1;
  Base *dPtr = &d1;
 
  return 0;
}

重點:

因為 dPtr 是 Base 類型指針,它隻指向 d1 對象的 Base 類型部分(即,指向 d1 對象中的 Base 子對象),而 *__vptr 也在 Base 類型部分。所以 dPtr 可以訪問 Base 類型部分中的 *__vptr。同時,這裡註意,dPtr->__vptr 指向的是 D1 的虛擬函數表,這是在 d1 初始化時就確定的。所以結果,盡管 dPtr 是 Base 類型指針,但它能夠訪問 D1 的虛函數表。

因此,當有調用 dPtr->function1() 時,發生瞭什麼?

int main()
{
  D1 d1;
  Base *dPtr = &d1;
  dPtr->function1();
 
  return 0;
}

首先,程序識別到 function1() 是一個虛函數。

其次,程序使用 dPtr->__vptr 獲取到瞭 D1 的虛函數表。

然後,它在 D1 的虛函數表中尋找可以調用的 function1() 版本,這裡是 D1::function1()。

因此,dPtr->function1() 實際調用瞭 D1::function1()。

通過虛函數表,編譯器和程序能夠確定調用什麼版本的虛函數,盡管使用的是指向/引用基類的指針或者引用。

調用虛函數會比調用非虛函數更慢,有以下幾個原因:

  • 必須使用 *__vptr 獲取正確的虛函數。
  • 必須建立虛函數表的索引來獲取想要調用的函數。
  • 調用找到的函數。

結果就是必須進行三次操作才能完成對函數的調用。但是對於現代計算機系統,這些額外操作增加的時間幾乎可以忽略不計。

另外,每個使用虛函數表的類都有 *__vptr 指針,從而每個類對象都會多一個指針的空間。虛函數很強大,但是它確實產生瞭性能開銷。

到此這篇關於C++ 中的虛函數表及虛函數執行原理詳解的文章就介紹到這瞭,更多相關C++ 虛函數表內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: