【整理】C++虛函數及其繼承、虛繼承類大小


參考文章:

http://blog.chinaunix.net/uid-25132162-id-1564955.html

http://blog.csdn.net/haoel/article/details/1948051/

 

一、虛函數與繼承

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的虛表,因為是虛擬繼承,所以子類中有一個指向父類的虛基類指針,防止菱形繼承中數據重復,這樣在菱形繼承中,不會出現祖先數據重復,而只指向祖先數據的指針。


免責聲明!

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



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