虛函數表和虛基類表


原文鏈接:https://www.cnblogs.com/fanzhidongyzby/archive/2013/01/14/2859064.html

 

虛函數與虛繼承尋蹤

  封裝、繼承、多態是面向對象語言的三大特性,熟悉C++的人對此應該不會有太多異議。C語言提供的struct,頂多算得上對數據的簡單封裝,而C++的引入把struct“升級”為class,使得面向對象的概念更加強大。繼承機制解決了對象復用的問題,然而多重繼承又會產生成員沖突的問題,虛繼承在我看來更像是一種“不得已”的解決方案。多態讓對象具有了運行時特性,並且它是軟件設計復用的本質,虛函數的出現為多態性質提供了實現手段。

  如果說C語言的struct相當於對數據成員簡單的排列(可能有對齊問題),那么C++的class讓對象的數據的封裝變得更加復雜。所有的這些問題來源於C++的一個關鍵字——virtual!virtual在C++中最大的功能就是聲明虛函數和虛基類,有了這種機制,C++對象的機制究竟發生了怎樣的變化,讓我們一起探尋之。

  為了查看對象的結構模型,我們需要在編譯器配置時做一些初始化。在VS2010中,在項目——屬性——配置屬性——C/C++——命令行——其他選項中添加選項“/d1reportAllClassLayout”。再次編譯時候,編譯器會輸出所有定義類的對象模型。由於輸出的信息過多,我們可以使用“Ctrl+F”查找命令,找到對象模型的輸出。

一、基本對象模型

  首先,我們定義一個簡單的類,它含有一個數據成員和一個虛函數。

 
class MyClass
{
    int var;
public:
    virtual void fun()
    {}
};
 

  編譯輸出的MyClass對象結構如下:

 
1>  class MyClass    size(8):
1>      +---
1>   0    | {vfptr}
1>   4    | var
1>      +---
1>  
1>  MyClass::$vftable@:
1>      | &MyClass_meta
1>      |  0
1>   0    | &MyClass::fun
1>  
1>  MyClass::fun this adjustor: 0
 

  從這段信息中我們看出,MyClass對象大小是8個字節。前四個字節存儲的是虛函數表的指針vfptr,后四個字節存儲對象成員var的值。虛函數表的大小為4字節,就一條函數地址,即虛函數fun的地址,它在虛函數表vftable的偏移是0。因此,MyClass對象模型的結果如圖1所示。

 

圖1 MyClass對象模型

  MyClass的虛函數表雖然只有一條函數記錄,但是它的結尾處是由4字節的0作為結束標記的。

  adjust表示虛函數機制執行時,this指針的調整量,假如fun被多態調用的話,那么它的形式如下:

*(this+0)[0]()

  總結虛函數調用形式,應該是:

  *(this指針+調整量)[虛函數在vftable內的偏移]()

二、單重繼承對象模型

  我們定義一個繼承於MyClass類的子類MyClassA,它重寫了fun函數,並且提供了一個新的虛函數funA。

 
class MyClassA:public MyClass
{
    int varA;
public:
    virtual void fun()
    {}
    virtual void funA()
    {}
};
 

  它的對象模型為:

 
1>  class MyClassA    size(12):
1>      +---
1>      | +--- (base class MyClass)
1>   0    | | {vfptr}
1>   4    | | var
1>      | +---
1>   8    | varA
1>      +---
1>  
1>  MyClassA::$vftable@:
1>      | &MyClassA_meta
1>      |  0
1>   0    | &MyClassA::fun
1>   1    | &MyClassA::funA
1>  
1>  MyClassA::fun this adjustor: 0
1>  MyClassA::funA this adjustor: 0
 

  可以看出,MyClassA將基類MyClass完全包含在自己內部,包括vfptr和var。並且虛函數表內的記錄多了一條——MyClassA自己定義的虛函數funA。它的對象模型如圖2所示。

 

圖2 MyClassA對象模型

  我們可以得出結論:在單繼承形式下,子類的完全獲得父類的虛函數表和數據。子類如果重寫了父類的虛函數(如fun),就會把虛函數表原本fun對應的記錄(內容MyClass::fun)覆蓋為新的函數地址(內容MyClassA::fun),否則繼續保持原本的函數地址記錄。如果子類定義了新的虛函數,虛函數表內會追加一條記錄,記錄該函數的地址(如MyClassA::funA)。

  使用這種方式,就可以實現多態的特性。假設我們使用如下語句:

MyClass*pc=new MyClassA;
pc->fun();

  編譯器在處理第二條語句時,發現這是一個多態的調用,那么就會按照上邊我們對虛函數的多態訪問機制調用函數fun。

*(pc+0)[0]()

  因為虛函數表內的函數地址已經被子類重寫的fun函數地址覆蓋了,因此該處調用的函數正是MyClassA::fun,而不是基類的MyClass::fun。

  如果使用MyClassA對象直接訪問fun,則不會出發多態機制,因為這個函數調用在編譯時期是可以確定的,編譯器只需要直接調用MyClassA::fun即可。

三、多重繼承對象模型

  和前邊MyClassA類似,我們也定義一個類MyClassB。

 
class MyClassB:public MyClass
{
    int varB;
public:
    virtual void fun()
    {}
    virtual void funB()
    {}
};
 

  它的對象模型和MyClassA完全類似,這里就不再贅述了。

  為了實現多重繼承,我們再定義一個類MyClassC。

 
class MyClassC:public MyClassA,public MyClassB
{
    int varC;
public:
    virtual void funB()
    {}
virtual void funC()
    {}
};
 

  為了簡化,我們讓MyClassC只重寫父類MyClassB的虛函數funB,它的對象模型如下:

 
1>  class MyClassC    size(28):
1>      +---
1>      | +--- (base class MyClassA)
1>      | | +--- (base class MyClass)
1>   0    | | | {vfptr}
1>   4    | | | var
1>      | | +---
1>   8    | | varA
1>      | +---
1>      | +--- (base class MyClassB)
1>      | | +--- (base class MyClass)
1>  12    | | | {vfptr}
1>  16    | | | var
1>      | | +---
1>  20    | | varB
1>      | +---
1>  24    | varC
1>      +---
1>  
1>  MyClassC::$vftable@MyClassA@:
1>      | &MyClassC_meta
1>      |  0
1>   0    | &MyClassA::fun
1>   1    | &MyClassA::funA
1>   2    | &MyClassC::funC
1>  
1>  MyClassC::$vftable@MyClassB@:
1>      | -12
1>   0    | &MyClassB::fun
1>   1    | &MyClassC::funB
1>  
1>  MyClassC::funB this adjustor: 12
1>  MyClassC::funC this adjustor: 0
 

  和單重繼承類似,多重繼承時MyClassC會把所有的父類全部按序包含在自身內部。而且每一個父類都對應一個單獨的虛函數表。MyClassC的對象模型如圖3所示。

 

圖3 MyClassC對象模型

  多重繼承下,子類不再具有自身的虛函數表,它的虛函數表與第一個父類的虛函數表合並了。同樣的,如果子類重寫了任意父類的虛函數,都會覆蓋對應的函數地址記錄。如果MyClassC重寫了fun函數(兩個父類都有該函數),那么兩個虛函數表的記錄都需要被覆蓋!在這里我們發現MyClassC::funB的函數對應的adjust值是12,按照我們前邊的規則,可以發現該函數的多態調用形式為:

*(this+12)[1]()

  此處的調整量12正好是MyClassB的vfptr在MyClassC對象內的偏移量。

四、虛擬繼承對象模型

  虛擬繼承是為了解決多重繼承下公共基類的多份拷貝問題。比如上邊的例子中MyClassC的對象內包含MyClassA和MyClassB子對象,但是MyClassA和MyClassB內含有共同的基類MyClass。為了消除MyClass子對象的多份存在,我們需要讓MyClassA和MyClassB都虛擬繼承於MyClass,然后再讓MyClassC多重繼承於這兩個父類。相對於上邊的例子,類內的設計不做任何改動,先修改MyClassA和MyClassB的繼承方式:

class MyClassA:virtual public MyClass
class MyClassB:virtual public MyClass
class MyClassC:public MyClassA,public MyClassB

  由於虛繼承的本身語義,MyClassC內必須重寫fun函數,因此我們需要再重寫fun函數。這種情況下,MyClassC的對象模型如下:

 
1>  class MyClassC    size(36):
1>      +---
1>      | +--- (base class MyClassA)
1>   0    | | {vfptr}
1>   4    | | {vbptr}
1>   8    | | varA
1>      | +---
1>      | +--- (base class MyClassB)
1>  12    | | {vfptr}
1>  16    | | {vbptr}
1>  20    | | varB
1>      | +---
1>  24    | varC
1>      +---
1>      +--- (virtual base MyClass)
1>  28    | {vfptr}
1>  32    | var
1>      +---
1>  
1>  MyClassC::$vftable@MyClassA@:
1>      | &MyClassC_meta
1>      |  0
1>   0    | &MyClassA::funA
1>   1    | &MyClassC::funC
1>  
1>  MyClassC::$vftable@MyClassB@:
1>      | -12
1>   0    | &MyClassC::funB
1>  
1>  MyClassC::$vbtable@MyClassA@:
1>   0    | -4
1>   1    | 24 (MyClassCd(MyClassA+4)MyClass)
1>  
1>  MyClassC::$vbtable@MyClassB@:
1>   0    | -4
1>   1    | 12 (MyClassCd(MyClassB+4)MyClass)
1>  
1>  MyClassC::$vftable@MyClass@:
1>      | -28
1>   0    | &MyClassC::fun
1>  
1>  MyClassC::fun this adjustor: 28
1>  MyClassC::funB this adjustor: 12
1>  MyClassC::funC this adjustor: 0
1>  
1>  vbi:       class  offset o.vbptr  o.vbte fVtorDisp
1>           MyClass      28       4       4 0
 

  虛繼承的引入把對象的模型變得十分復雜,除了每個基類(MyClassA和MyClassB)和公共基類(MyClass)的虛函數表指針需要記錄外,每個虛擬繼承了MyClass的父類還需要記錄一個虛基類表vbtable的指針vbptr。MyClassC的對象模型如圖4所示。

 

圖4 MyClassC對象模型

  虛基類表每項記錄了被繼承的虛基類子對象相對於虛基類表指針的偏移量。比如MyClassA的虛基類表第二項記錄值為24,正是MyClass::vfptr相對於MyClassA::vbptr的偏移量,同理MyClassB的虛基類表第二項記錄值12也正是MyClass::vfptr相對於MyClassA::vbptr的偏移量。

  和虛函數表不同的是,虛基類表的第一項記錄着當前子對象相對與虛基類表指針的偏移。MyClassA和MyClassB子對象內的虛表指針都是存儲在相對於自身的4字節偏移處,因此該值是-4。假定MyClassA和MyClassC或者MyClassB內沒有定義新的虛函數,即不會產生虛函數表,那么虛基類表第一項字段的值應該是0。

  通過以上的對象組織形式,編譯器解決了公共虛基類的多份拷貝的問題。通過每個父類的虛基類表指針,都能找到被公共使用的虛基類的子對象的位置,並依次訪問虛基類子對象的數據。至於虛基類定義的虛函數,它和其他的虛函數的訪問形式相同,本例中,如果使用虛基類指針MyClass*pc訪問MyClassC對象的fun,將會被轉化為如下形式:

*(pc+28)[0]()

  通過以上的描述,我們基本認清了C++的對象模型。尤其是在多重、虛擬繼承下的復雜結構。通過這些真實的例子,使得我們認清C++內class的本質,以此指導我們更好的書寫我們的程序。本文從對象結構的角度結合圖例為大家闡述對象的基本模型,和一般描述C++虛擬機制的文章有所不同。作者只希望借助於圖表能把C++對象以更好理解的形式為大家展現出來,希望本文對你有所幫助。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM