C++ 虛擬函式表剖析
一、概述
為了實現C++的多型,C++使用了一種動態繫結的技術。這個技術的核心是虛擬函式表(下文簡稱虛表)。本文介紹虛擬函式表是如何實現動態繫結的。
二、類的虛表
每個包含了虛擬函式的類都包含一個虛表。
我們知道,當一個類(A)繼承另一個類(B)時,類A會繼承類B的函式的呼叫權。所以如果一個基類包含了虛擬函式,那麼其繼承類也可呼叫這些虛擬函式,換句話說,一個類繼承了包含虛擬函式的基類,那麼這個類也擁有自己的虛表。
我們來看以下的程式碼。類A包含虛擬函式vfunc1,vfunc2,由於類A包含虛擬函式,故類A擁有一個虛表。
class A { public: virtual void vfunc1(); virtual void vfunc2(); void func1(); void func2(); private: int m_data1, m_data2; };
類A的虛表如圖1所示。
圖1:類A的虛表示意圖
虛表是一個指標陣列,其元素是虛擬函式的指標,每個元素對應一個虛擬函式的函式指標。需要指出的是,普通的函式即非虛擬函式,其呼叫並不需要經過虛表,所以虛表的元素並不包括普通函式的函式指標。
虛表內的條目,即虛擬函式指標的賦值發生在編譯器的編譯階段,也就是說在程式碼的編譯階段,虛表就可以構造出來了。
三、虛表指標
虛表是屬於類的,而不是屬於某個具體的物件,一個類只需要一個虛表即可。同一個類的所有物件都使用同一個虛表。
為了指定物件的虛表,物件內部包含一個虛表的指標,來指向自己所使用的虛表。為了讓每個包含虛表的類的物件都擁有一個虛表指標,編譯器在類中新增了一個指標,*__vptr
,用來指向虛表。這樣,當類的物件在建立時便擁有了這個指標,且這個指標的值會自動被設定為指向類的虛表。
圖2:物件與它的虛表
上面指出,一個繼承類的基類如果包含虛擬函式,那個這個繼承類也有擁有自己的虛表,故這個繼承類的物件也包含一個虛表指標,用來指向它的虛表。
四、動態繫結
說到這裡,大家一定會好奇C++是如何利用虛表和虛表指標來實現動態繫結的。我們先看下面的程式碼。
class A { public: virtual void vfunc1(); virtual void vfunc2(); void func1(); void func2(); private: int m_data1, m_data2; }; class B : public A { public: virtual void vfunc1(); void func1(); private: int m_data3; }; class C: public B { public: virtual void vfunc2(); void func2(); private: int m_data1, m_data4; };
類A是基類,類B繼承類A,類C又繼承類B。類A,類B,類C,其物件模型如下圖3所示。
圖3:類A,類B,類C的物件模型
由於這三個類都有虛擬函式,故編譯器為每個類都建立了一個虛表,即類A的虛表(A vtbl),類B的虛表(B vtbl),類C的虛表(C vtbl)。類A,類B,類C的物件都擁有一個虛表指標,*__vptr
,用來指向自己所屬類的虛表。
類A包括兩個虛擬函式,故A vtbl包含兩個指標,分別指向A::vfunc1()
和A::vfunc2()
。
類B繼承於類A,故類B可以呼叫類A的函式,但由於類B重寫了B::vfunc1()
函式,故B vtbl的兩個指標分別指向B::vfunc1()
和A::vfunc2()
。
類C繼承於類B,故類C可以呼叫類B的函式,但由於類C重寫了C::vfunc2()
函式,故C vtbl的兩個指標分別指向B::vfunc1()
(指向繼承的最近的一個類的函式)和C::vfunc2()
。
雖然圖3看起來有點複雜,但是隻要抓住“物件的虛表指標用來指向自己所屬類的虛表,虛表中的指標會指向其繼承的最近的一個類的虛擬函式”這個特點,便可以快速將這幾個類的物件模型在自己的腦海中描繪出來。
非虛擬函式的呼叫不用經過虛表,故不需要虛表中的指標指向這些函式。
假設我們定義一個類B的物件bObject
。由於bObject
是類B的一個物件,故bObject
包含一個虛表指標,指向類B的虛表。
int main() { B bObject; }
現在,我們宣告一個類A的指標p來指向物件bObject
。雖然p
是基類的指標只能指向基類的部分,但是虛表指標亦屬於基類部分,所以p
可以訪問到物件bObject
的虛表指標。bObject
的虛表指標指向類B的虛表,所以p
可以訪問到B vtbl。如圖3所示。
int main() { B bObject; A *p = & bObject; }
當我們使用p
來呼叫vfunc1()
函式時,會發生什麼現象?
int main() { B bObject; A *p = & bObject; p->vfunc1(); }
程式在執行p->vfunc1()
時,會發現p
是個指標,且呼叫的函式是虛擬函式,接下來便會進行以下的步驟。
首先,根據虛表指標p->__vptr
來訪問物件bObject
對應的虛表。雖然指標p
是基類A*
型別,但是*__vptr
也是基類的一部分,所以可以通過p->__vptr
可以訪問到物件對應的虛表。
然後,在虛表中查詢所呼叫的函式對應的條目。由於虛表在編譯階段就可以構造出來了,所以可以根據所呼叫的函式定位到虛表中的對應條目。對於p->vfunc1()
的呼叫,B vtbl的第一項即是vfunc1
對應的條目。
最後,根據虛表中找到的函式指標,呼叫函式。從圖3可以看到,B vtbl的第一項指向B::vfunc1()
,所以p->vfunc1()
實質會呼叫B::vfunc1()
函式。
如果p
指向類A的物件,情況又是怎麼樣?
int main() { A aObject; A *p = &aObject; p->vfunc1(); }
當aObject
在建立時,它的虛表指標__vptr
已設定為指向A vtbl,這樣p->__vptr
就指向A vtbl。vfunc1
在A vtbl對應在條目指向了A::vfunc1()
函式,所以p->vfunc1()
實質會呼叫A::vfunc1()
函式。
可以把以上三個呼叫函式的步驟用以下表示式來表示:
(*(p->__vptr)[n])(p)
可以看到,通過使用這些虛擬函式表,即使使用的是基類的指標來呼叫函式,也可以達到正確呼叫執行中實際物件的虛擬函式。
我們把經過虛表呼叫虛擬函式的過程稱為動態繫結,其表現出來的現象稱為執行時多型。動態繫結區別於傳統的函式呼叫,傳統的函式呼叫我們稱之為靜態繫結,即函式的呼叫在編譯階段就可以確定下來了。
那麼,什麼時候會執行函式的動態繫結?這需要符合以下三個條件。
- 通過指標來呼叫函式
- 指標upcast向上轉型(繼承類向基類的轉換稱為upcast,關於什麼是upcast,可以參考本文的參考資料)
- 呼叫的是虛擬函式
如果一個函式呼叫符合以上三個條件,編譯器就會把該函式呼叫編譯成動態繫結,其函式的呼叫過程走的是上述通過虛表的機制。
五、總結
封裝,繼承,多型是物件導向設計的三個特徵,而多型可以說是物件導向設計的關鍵。C++通過虛擬函式表,實現了虛擬函式與物件的動態繫結,從而構建了C++物件導向程式設計的基石。
參考資料
- 《C++ Primer》第三版,中文版,潘愛民等譯
- http://www.learncpp.com/cpp-tutorial/125-the-virtual-table/
- 侯捷《C++最佳程式設計實踐》視訊,極客班,2015
- Upcasting and Downcasting, http://www.bogotobogo.com/cplusplus/upcasting_downcasting.php
附錄
相關文章
- c++虛擬函式表C++函式
- C++中的虛擬函式與虛擬函式表 (轉)C++函式
- 虛擬函式,虛擬函式表函式
- 【C++筆記】虛擬函式(從虛擬函式表來解析)C++筆記函式
- C++多型(上)——虛擬函式、虛表C++多型函式
- 深入C++成員函式及虛擬函式表C++函式
- 從彙編層面深度剖析 C++ 虛擬函式C++函式
- C++ 虛擬函式C++函式
- C++虛擬函式C++函式
- C++中抽象類、虛擬函式和純虛擬函式C++抽象函式
- C++虛擬函式bugC++函式
- 【C++筆記】虛擬函式(從虛擬函式概念來解析)C++筆記函式
- C++ 介面(純虛擬函式)C++函式
- C++ 中的虛擬函式C++函式
- C++ 虛擬函式表解析C++函式
- 虛擬函式表-C++多型的實現原理函式C++多型
- 虛擬函式 純虛擬函式函式
- C++物件導向總結——虛指標與虛擬函式表C++物件指標函式
- C++多型之虛擬函式C++多型函式
- C++中虛擬函式的作用C++函式
- C++虛擬函式解析(轉載)C++函式
- C++箴言:避免解構函式呼叫虛擬函式C++箴言函式
- C++ 虛擬函式和虛繼承淺析C++函式繼承
- C++虛擬函式學習總結C++函式
- C++繼承二之虛擬函式C++繼承函式
- C++ 繼承、多型、虛擬函式C++繼承多型函式
- C++中的虛擬函式(virtual function)C++函式Function
- C++:純虛擬函式與抽象類C++函式抽象
- C++ Daily 《3》----建構函式可否是虛擬函式C++AI函式
- 介面、虛擬函式、純虛擬函式、抽象類函式抽象
- 虛擬函式函式
- C/C++—— C++中建構函式不能是虛擬函式的原因分析C++函式
- C++函式中那些不可以被宣告為虛擬函式的函式C++函式
- C++之類解構函式為什麼是虛擬函式C++函式
- C++單繼承、多繼承情況下的虛擬函式表分析C++繼承函式
- [Lang] 虛擬函式函式
- C++純虛擬函式簡介及區別C++函式
- c++虛擬函式實現計算表示式子C++函式