1. 程式人生 > >C++虛擬函式及其繼承、虛繼承類大小

C++虛擬函式及其繼承、虛繼承類大小

文章轉自: https://www.cnblogs.com/yanqi0124/p/3829964.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()
複製程式碼

3、一個關於含虛擬函式及虛繼承的sizeof計算

複製程式碼
#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。(這個是個人的分析,但原理不知道是不是這樣的)

二、不同編譯器下的虛繼承

1、對虛繼承層次的物件的記憶體佈局,在不同編譯器實現有所區別。

首先,說說GCC的編譯器.

它實現比較簡單,不管是否虛繼承,GCC都是將虛表指標在整個繼承關係中共享的,不共享的是指向虛基類的指標。

複製程式碼
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)=12,sizeof(C)=12,sizeof(D)=16.

  解釋:A中int+虛表指標。B,C中由於是虛繼承因此大小為A+指向虛基類的指標,B,C雖然加入了自己的虛擬函式,但是虛表指標是和基類共享的,因此不會有自己的虛表指標。D由於B,C都是虛繼承,因此D只包含一個A的副本,於是D大小就等於A+B中的指向虛基類的指標+C中的指向虛基類的指標。

如果B,C不是虛繼承,而是普通繼承的話,那麼A,B,C的大小都是8(沒有指向虛基類的指標了),而D由於不是虛繼承,因此包含兩個A副本,大小為16.注意此時雖然D的大小和虛繼承一樣,但是記憶體佈局卻不同。

然後,來看看VC的編譯器

  vc對虛表指標的處理比GCC複雜,它根據是否為虛繼承來判斷是否在繼承關係中共享虛表指標,而對指向虛基類的指標和GCC一樣是不共享,當然也不可能共享。

  程式碼同上。

  執行結果將會是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中的指向虛基類的指標。

  同樣,如果去掉虛繼承,結果將和GCC結果一樣,A,B,C都是8,D為16,原因就是VC的編譯器對於非虛繼承,父類和子類是共享虛表指標的。

  利用visual studio 命令提示(2008),到xx.cpp 檔案目錄下 執行cl /d1 reportSingleClassLayoutB xx.cpp

  第一個vfptr 指向B的虛表,第二個vbptr指向A,第三個指向A的虛表,因為是虛擬繼承,所以子類中有一個指向父類的虛基類指標,防止菱形繼承中資料重複,這樣在菱形繼承中,不會出現祖先資料重複,而只指向祖先資料的指標。