虛函數繼承與虛繼承

一、虛函數繼承(原文轉自http://blog.chinaunix.net/uid-25132162-id-1564955.html)

1、空類,空類單繼承,空類多繼承的sizeof

#include <iostream>
using namespace std;

class Base1
{

};

class Base2
{

};

class Derived1:public Base1
{

};

class Derived2:public Base1, public Base2
{

};

int main() 
{ 
    Base1 b1;
    Base2 b2;
    Derived1 d1;
    Derived2 d2;
    cout<<"sizeof(Base1) = "<<sizeof(Base1)<<" sizeof(b1) = "<<sizeof(b1)<<endl;
     cout<<"sizeof(Base2) = "<<sizeof(Base2)<<" sizeof(b2) = "<<sizeof(b2)<<endl;
    cout<<"sizeof(Derived1) = "<<sizeof(Derived1)<<" sizeof(d1) = "<<sizeof(d1)<<endl;
    cout<<"sizeof(Derived2) = "<<sizeof(Derived2)<<" sizeof(d1) = "<<sizeof(d1)<<endl;
  
    return 0; 
}
結果爲:
sizeof(Base1) = 1 sizeof(b1) = 1
sizeof(Base2) = 1 sizeof(b2) = 1
sizeof(Derived1) = 1 sizeof(d1) = 1
sizeof(Derived2) = 1 sizeof(d1) = 1
可以看出所有的結果都是1。

2、含有虛函數的類以及虛繼承類的sizeof
虛函數(Virtual Function)是通過一張虛函數表(Virtual Table)來實現的。編譯器必需要保證虛函數表的指針存在於對象實例中最前面的位置(這是爲了保證正確取到虛函數的偏移量)。
假設我們有這樣的一個類:
class Base {
public:
virtual void f() { cout << "Base::f" << endl; }
virtual void g() { cout << "Base::g" << endl; }
virtual void h() { cout << "Base::h" << endl; }
};
當我們定義一個這個類的實例,Base b時,其b中成員的存放如下:

指向虛函數表的指針在對象b的最前面。虛函數表的最後多加了一個結點,這是虛函數表的結束結點,就像字符串的結束符“\0”一樣,其標誌了虛函數表的結束。這個結束標誌的值在不同的編譯器下是不同的。在WinXP+VS2003下,這個值是NULL。而在Ubuntu 7.10 + Linux 2.6.22 + GCC 4.1.3下,這個值是如果1,表示還有下一個虛函數表,如果值是0,表示是最後一個虛函數表。
因爲對象b中多了一個指向虛函數表的指針,而指針的sizeof是4,因此含有虛函數的類或實例最後的sizeof是實際的數據成員的sizeof加4。
下面將討論針對基類含有虛函數的繼承討論
(1)在派生類中不對基類的虛函數進行覆蓋,同時派生類中還擁有自己的虛函數,比如有如下的派生類:

class Derived: public Base
 {
public:
virtual void f1() { cout << "Derived::f1" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
基類和派生類的關係如下:


當定義一個Derived的對象d後,其成員的存放如下:


可以發現:
1)虛函數按照其聲明順序放於表中。
2)父類的虛函數在子類的虛函數前面。
此時基類和派生類的sizeof都是數據成員的sizeof加4。
(2)在派生類中對基類的虛函數進行覆蓋,假設有如下的派生類:

class Derived: public Base
 {
public:
virtual void f() { cout << "Derived::f" << endl; }
virtual void g1() { cout << "Derived::g1" << endl; }
virtual void h1() { cout << "Derived::h1" << endl; }
};
基類和派生類之間的關係:其中基類的虛函數f在派生類中被覆蓋了

當我們定義一個派生類對象d後,其d的成員存放爲:

可以發現:
1)覆蓋的f()函數被放到了虛表中原來父類虛函數的位置。
2)沒有被覆蓋的函數依舊。
這樣,我們就可以看到對於下面這樣的程序,
Base *b = new Derive();
b->f();
由b所指的內存中的虛函數表的f()的位置已經被Derive::f()函數地址所取代,於是在實際調用發生時,是Derive::f()被調用了。這就實現了多態。
(3)多繼承:無虛函數覆蓋
假設基類和派生類之間有如下關係:

對於子類實例中的虛函數表,是下面這個樣子:

我們可以看到:
1) 每個父類都有自己的虛表。
2) 子類的成員函數被放到了第一個父類的表中。(所謂的第一個父類是按照聲明順序來判斷的)
由於每個基類都需要一個指針來指向其虛函數表,因此d的sizeof等於d的數據成員加3*4=12。
(4)多重繼承,含虛函數覆蓋
假設,基類和派生類又如下關係:派生類中覆蓋了基類的虛函數f

下面是對於子類實例中的虛函數表的圖:

我們可以看見,三個父類虛函數表中的f()的位置被替換成了子類的函數指針。這樣,我們就可以任一靜態類型的父類來指向子類,並調用子類的f()了。如:

Derive d;
Base1 *b1 = &d;
Base2 *b2 = &d;
Base3 *b3 = &d;
b1->f(); //Derive::f()
b2->f(); //Derive::f()
b3->f(); //Derive::f()
b1->g(); //Base1::g()
b2->g(); //Base2::g()
b3->g(); //Base3::g()
二、虛繼承(原文轉自http://blog.csdn.net/gxiaob/article/details/10149069)

虛擬繼承是爲了解決多重繼承下公共基類的多份拷貝問題。

class A {
int a;
virtual ~A(){}
};
class B:virtual public A{
virtual void myfunB(){}
};
class C:virtual public A{
virtual void myfunC(){}
};
class D:public B,public C{
virtual void myfunD(){}
};
運行結果將會是sizeof(A)=8,sizeof(B)=16,sizeof(C)=16,sizeof(D)=24.
解釋:A中依然是int+虛表指針。B,C中由於是虛繼承因此虛表指針不共享,由於B,C加入了自己的虛函數,所以B,C分別自己維護一個虛表指針,它指向自己的虛函數。(注意:只有子類有新的虛函數時,編譯器纔會在子類中添加虛表指針)因此B,C大小爲A+自己的虛表指針+指向虛基類的指針。D由於B,C都是虛繼承,因此D只包含一個A的副本,同時D是從B,C普通繼承的,而不是虛繼承的,因此沒有自己的虛表指針。於是D大小就等於A+B的虛表指針+C的虛表指針+B中的指向虛基類的指針+C中的指向虛基類的指針。
如果去掉虛繼承,,A,B,C都是8,D爲16,原因就是VC的編譯器對於非虛繼承,父類和子類是共享虛表指針的。

#include <iostream>
using namespace std;

class Base
{
public:
    virtual void f();
    virtual void g();
    virtual void h();
};

class Derived1: public Base
{
public:
    virtual void f1();
    virtual void g1();
    virtual void h1();
};

class Derived2:public Base
{
public:
    virtual void f();
    virtual void g1();
    virtual void h1();
};

class Derived3:virtual public Base
{
public:
    virtual void f1();
    virtual void g1();
    virtual void h1();
};

class Derived4:virtual public Base
{
public:
    virtual void f();
    virtual void g1();
    virtual void h1();
};

class Derived5:virtual public Base
{
public:
    virtual void f();
    virtual void g();
    virtual void h();
};

class Derived6:virtual public Base
{

};

int main() 
{ 
    cout<<sizeof(Base)<<endl; //4
    cout<<sizeof(Derived1)<<endl; //4
    cout<<sizeof(Derived2)<<endl; //4
    cout<<sizeof(Derived3)<<endl; //12
    cout<<sizeof(Derived4)<<endl; //12
    cout<<sizeof(Derived5)<<endl; //8
    cout<<sizeof(Derived6)<<endl; //8

    return 0; 
}
對於Base, Derived1和Derived2的結果根據前面關於繼承的分析是比較好理解的,不過對於虛繼承的方式則有點不一樣了,根據結果自己得出的一種關於虛繼承的分析,如對Derived3或Derived4定義一個對象d,其裏面會出現三個跟虛函數以及虛繼承的指針,因爲是虛繼承,因此引入一個指針指向虛繼承的基類,第二由於在基類中有虛函數,因此需要指針指向其虛函數表,由於派生類自己本身也有自己的虛函數,因爲採取的是虛繼承,因此它自己的虛函數不會放到基類的虛函數表的後面,而是另外分配一個只存放自己的虛函數的虛函數表,於是又引入一個指針,從例子中看到Derived5和Derived6的結果是8,原因是在派生類要麼沒有自己的虛函數,要麼全部都是對基類虛函數的覆蓋,因此就少了指向其派生類自己的虛函數表的指針,故結果要少4。
三、虛函數與虛繼承尋蹤(原文轉自http://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”查找命令,找到對象模型的輸出。

1、基本對象模型
首先,我們定義一個簡單的類,它含有一個數據成員和一個虛函數。

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對象模型的結果如圖所示。


MyClass的虛函數表雖然只有一條函數記錄,但是它的結尾處是由4字節的0作爲結束標記的。
adjust表示虛函數機制執行時,this指針的調整量,假如fun被多態調用的話,那麼它的形式如下:
*(this+0)[0]()
總結虛函數調用形式,應該是:
*(this指針+調整量)[虛函數在vftable內的偏移]()

2、單重繼承對象模型
我們定義一個繼承於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。它的對象模型如下圖所示。

我們可以得出結論:在單繼承形式下,子類的完全獲得父類的虛函數表和數據。子類如果重寫了父類的虛函數(如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即可。

3、多重繼承對象模型
和前邊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的對象模型如下圖所示。

多重繼承下,子類不再具有自身的虛函數表,它的虛函數表與第一個父類的虛函數表合併了。同樣的,如果子類重寫了任意父類的虛函數,都會覆蓋對應的函數地址記錄。如果MyClassC重寫了fun函數(兩個父類都有該函數),那麼兩個虛函數表的記錄都需要被覆蓋!在這裏我們發現MyClassC::funB的函數對應的adjust值是12,按照我們前邊的規則,可以發現該函數的多態調用形式爲:
*(this+12)[1]()
此處的調整量12正好是MyClassB的vfptr在MyClassC對象內的偏移量。

4、虛擬繼承對象模型
虛擬繼承是爲了解決多重繼承下公共基類的多份拷貝問題。比如上邊的例子中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的對象模型下圖所示。

虛基類表每項記錄了被繼承的虛基類子對象相對於虛基類表指針的偏移量。比如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++虛擬機制的文章有所不同。























發佈了36 篇原創文章 · 獲贊 8 · 訪問量 9萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章