1. 程式人生 > >C++繼承、虛繼承、虛擬函式類的大小問題

C++繼承、虛繼承、虛擬函式類的大小問題

一、真空類

class CNull

{

};

長度:1

記憶體結構:

??

評註:長度其實為0,這個位元組作為內容沒有意義,可能每次都不一樣。

二、空類

class CNull2

{

public:

    CNull2(){printf("Construct/n");}

    ~CNull2(){printf("Desctruct/n");}

    void Foo(){printf("Foo/n");}

};

長度:1

記憶體結構:

??

評註:同真空類差不多,內部的成員函式並不會影響類大小。

三、簡單類

class COneMember

{

public:

    COneMember(int iValue = 0){m_iOne = iValue;};

private:

    int m_iOne;

};

長度:4

記憶體結構:

00 00 00 00 //m_iOne

評註:成員資料才影響類大小。

四、簡單繼承

class CTwoMember:public COneMember

{

private:

    int m_iTwo;

};

長度:8

記憶體結構:

00 00 00 00 //m_iOne

CC CC CC CC //m_iTwo

評註:子類成員接在父類成員之後。

五、再繼承

class CThreemember:public CTwoMember

{

public:

    CThreemember(int iValue=10) {m_iThree = iValue;};

private:

    int m_iThree;

};

長度:12

記憶體結構:

00 00 00 00 //m_iOne

CC CC CC CC //m_iTwo

0A 00 00 00 //m_iThree

評註:孫類成員接在子類之後,再再繼承就依此類推了。

六、多重繼承

class ClassA

{

public:

    ClassA(int iValue=1){m_iA = iValue;};

private:

    int m_iA;

};

class ClassB

{

public:

    ClassB(int iValue=2){m_iB = iValue;};

private:

    int m_iB;

};

class ClassC

{

public:

    ClassC(int iValue=3){m_iC = iValue;};

private:

    int m_iC;

};

class CComplex :public ClassA, public ClassB, public ClassC

{

public:

    CComplex(int iValue=4){m_iComplex = iValue;};

private:

    int m_iComplex;

};

長度:16

記憶體結構:

01 00 00 00  //A

02 00 00 00  //B

03 00 00 00  //C

04 00 00 00  //Complex

評註:也是父類成員先出現在前邊,我想這都足夠好理解。

七、複雜一些的繼承

不寫程式碼了,怕讀者看了眼花,改畫圖。

長度:32

記憶體結構:

01 00 00 00 //A

02 00 00 00 //B

03 00 00 00 //C

04 00 00 00 //Complex

00 00 00 00 //OneMember

CC CC CC CC //TwoMember

0A 00 00 00 //ThreeMember

05 00 00 00 //VeryComplex

評註:還是把自己的成員放在最後。

只要沒涉及到“虛”(Virtual),我想沒什麼難點,不巧的是“虛”正是我們要研究的內容。

八、趁熱打鐵,看“虛繼承”

class CTwoMember:virtual public COneMember

{

private:

    int m_iTwo;

};

長度:12

記憶體結構:

E8 2F 42 00 //指標,指向一個關於偏移量的陣列,且稱之虛基類偏移量表指標

CC CC CC CC // m_iTwo

00 00 00 00 // m_iOne(虛基類資料成員)

評註:virtual讓長度增加了4,其實是多了一個指標,關於這個指標,確實有些複雜,別的文章有具體分析,這裡就不岔開具體講了,可認為它指向一個關於虛基類偏移量的陣列,偏移量是關於虛基類資料成員的偏移量。

九、“閉合”虛繼承,看看效果

長度:24

記憶體結構:

14 30 42 00 //ClassB的虛基類偏移量表指標

02 00 00 00 //m_iB

C4 2F 42 00 //ClassC的虛基類偏移量表指標

03 00 00 00 //m_iC

04 00 00 00 //m_iComplex

01 00 00 00 //m_iA

評註:和預料中的一樣,虛基類的成員m_iA只出現了一次,而且是在最後邊。當然了,更復雜的情況要比這個難分析得多,但虛繼承不是我們研究的重點,我們只需要知道:虛繼承利用一個“虛基類偏移量表指標”來使得虛基類即使被重複繼承也只會出現一次。

十、看一下關於static成員

class CStaticNull

{

public:

    CStaticNull(){printf("Construct/n");}

    ~CStaticNull(){printf("Desctruct/n");}

    static void Foo(){printf("Foo/n");}

    static int m_iValue;

};

長度:1

記憶體結構:(同CNull2)

評註:可見static成員不會佔用類的大小,static成員的存在區域為靜態區,可認為它們是“全域性”的,只是不提供全域性的訪問而已,這跟C的static其實沒什麼區別。

十一、帶一個虛擬函式的空類

class CVirtualNull

{

public:

    CVirtualNull(){printf("Construct/n");}

    ~CVirtualNull(){printf("Desctruct/n");}

    virtual void Foo(){printf("Foo/n");}

};

長度:4

記憶體結構:

00 31 42 00 //指向虛擬函式表的指標(虛擬函式表後面簡稱“虛表”)

00423100:(虛表)

41 10 40 00 //指向虛擬函式Foo的指標

00401041:

E9 78 02 00 00 E9 C3 03 … //函式Foo的內容(看不懂)

評註:帶虛擬函式的類長度就增加了4,這個4其實就是個指標,指向虛擬函式表的指標,上面這個例子中虛表只有一個函式指標,值就是“0x00401041”,指向的這個地址就是函式的入口了。

十二、繼承帶虛擬函式的類

class CVirtualDerived : public CVirtualNull

{

public:

    CVirtualDerived(){m_iVD=0xFF;};

    ~CVirtualDerived(){};

private:

    int m_iVD;

};

長度:8

記憶體結構:

3C 50 42 00 //虛表指標

FF 00 00 00 //m_iVD

0042503C:(虛表)

23 10 40 00 //指向虛擬函式Foo的指標,如果這時候建立一個CVirtualNull物件,會發現它的虛表的內容跟這個一樣

評註:由於父類帶了虛擬函式,子類就算沒有顯式宣告虛擬函式,虛表還是存在的,虛表存放的位置跟父類不同,但內容是同的,也就是對父類虛表的複製。

十三、子類有新的虛擬函式

class CVirtualDerived: public CVirtualNull

{

public:

    CVirtualDerived(){m_iVD=0xFF;};

    ~CVirtualDerived(){};

    virtual void Foo2(){printf("Foo2/n");};

private:

    int m_iVD;

};

長度:8

記憶體結構:

24 61 42 00 //虛表指標

FF 00 00 00 //m_iVD

00426124:(虛表)

23 10 40 00

50 10 40 00

評註:虛表還是隻有一張,不會因為增加了新的虛擬函式而多出另一張來,新的虛擬函式的指標將新增在複製了的虛表的後面。

十四、當純虛擬函式(pure function)出現時

class CPureVirtual

{

    virtual void Foo() = 0;

};

class CDerivePV : public CPureVirtual

{

    void Foo(){printf("vd: Foo/n");};

};

長度:4(CPureVirtual),4(CDerivePV)

記憶體結構:

CPureVirtual:

(不可例項化)

CDerivePV:

28 50 42 00 //虛表指標

00425028:(虛表)

5A 10 40 00 //指向Foo的函式指標

評註:帶純虛擬函式的類不可例項化,因此列不出其“記憶體結構”,由其派生類實現純虛擬函式。我們可以看到CDerivePV雖然沒有virtual宣告,但由於其父類帶virtual,所以還是繼承了虛表,如果CDerivePV有子類,還是這個道理。

十五、虛擬函式類的多重繼承

前面提到:(子類的虛表)不會因為增加了新的虛擬函式而多出另一張來,但如果有多重繼承的話情況就不是這樣了。下例中你將看到兩張虛表。

大小:24

記憶體結構

F8 50 42 00 //虛表指標

01 00 00 00 //m_iA

02 00 00 00 //m_iB

E8 50 42 00 //虛表指標

03 00 00 00 //m_iC

04 00 00 00 //m_iComplex

004250F8:(虛表)

5A 10 40 00 //FooA

55 10 40 00 //FooB

64 10 40 00 //FooComplex

004250E8:(虛表)

5F 10 40 00 //FooC

評註:子類的虛擬函式接在第一個基類的虛擬函式表的後面,所以B接在A後面,Complex接在B後面。基類依次出現,子類成員接在最後面,所以m_iComplex位於最後面。

十六、包含虛擬函式類的虛繼承

class VirtualInheritance
{
	char k[3];
public:
	virtual void aa(){};
};
class sonClass1: public virtual VirtualInheritance
{
	char j[3];
public:
	virtual void bb(){};
};
class sonClass2: public virtual sonClass1
{
	char f[3];
public:
	virtual void cc(){};
};

int main()
{
	cout << "sizeof(VirtualInheritance):" << sizeof(VirtualInheritance) << endl;
	cout << "sizeof(sonClass1):" << sizeof(sonClass1) << endl;
	cout << "sizeof(sonClass2):" << sizeof(sonClass2) << endl;

	return 0;
}
輸出的結果:

visio studio: 8,20,32

gcc: 8,16,24

評註:

對於VirtualInheritance類,大小為8, 沒有異議,他有個虛表指標vtp_VirtualInheritanc;

對於sonClass1類:

在visio studio 編譯器下,大小為20。由於是虛擬繼承,又有自己的虛擬函式,所以先擁有一個自己的虛擬函式指標vpt_sonClass1,大小為4,指向自己的虛表;還要有一個char[3],大小為4;為了實現虛擬繼承,首先sonClass1加入了一個指向其父類的虛類指標,記作vtp_sonClass1_VirtualInheritanc,大小為4;然後在加上父類的所有大小8,所以總共是20位元組。

在gcc編譯器下,大小為16,沒有計運算元類中指向父類的虛類指標vtp_sonClass1_VirtualInheritanc的大小。

對於sonClass2:

在visio studio環境下,大小為32。和上面一樣,子類擁有char[3],大小為4位元組,因為是虛繼承,還有自己的虛擬函式,所以擁有自己的一個虛表指標,vtp_sonClass2,大小為4位元組。然後還有一個指向父類的虛類指標vtp_sonClass2_sonClass

1,大小為4。最後加上其父類的總大小20,所以總的大小為4+4+4+20=32;

在gcc環境下,沒有計算虛類指標的大小,即4+4+16=24。

17、包含虛擬函式的多重普通、虛擬混合繼承

class VirtualInheritance
{
	char k[3];
public:
	virtual void aa(){};
};
class sonClass1: public virtual VirtualInheritance
{
	char j[3];
public:
	virtual void bb(){};
};

class VirtualInheritance2
{
    char l[3];
public:
	virtual void dd(){};
};

class sonClass2: public virtual sonClass1,public VirtualInheritance2
{
	char f[3];
public:
   virtual void cc(){};
};

int main()
{
	cout << "sizeof(VirtualInheritance):" << sizeof(VirtualInheritance) << endl;
	cout << "sizeof(sonClass1):" << sizeof(sonClass1) << endl;
	cout << "sizeof(sonClass2):" << sizeof(sonClass2) << endl;

	return 0;
}

評註:此時sonClass2的大小變成36。和16不同的是,此時sonClass2是多重繼承,其中一個是虛繼承,一個普通繼承,他的大小在visio studio中變成36,相比16增加了4,這剛好是char l[3]的大小,因為聳sonClass2已經有了一個虛表,所以在他原有的虛表中多一條記錄即可。

18、包含虛擬函式的多重虛擬繼承

class VirtualInheritance
{
	char k[3];
public:
	virtual void aa(){};
};
class sonClass1: public virtual VirtualInheritance
{
	char j[3];
public:
	virtual void bb(){};
};

class VirtualInheritance2
{
    char l[3];
public:
	virtual void dd(){};
};

class sonClass2: public virtual sonClass1,public virtual VirtualInheritance2
{
	char f[3];
public:
   virtual void cc(){};
};

int main()
{
	cout << "sizeof(VirtualInheritance):" << sizeof(VirtualInheritance) << endl;
	cout << "sizeof(sonClass1):" << sizeof(sonClass1) << endl;
	cout << "sizeof(sonClass2):" << sizeof(sonClass2) << endl;

	return 0;
}

評註:此時sonClass2的大小變成40。與17不同的是,sonClass2的多重繼承都是虛擬繼承。sonClass2的大小由以下幾部分構成:

1.自己本身的大小,char[3] 大小為4,一個虛擬函式,所以有個指向虛表的指標,大小為4,所以自身大小總的為8;

2.虛擬繼承sonClass1,因為虛擬繼承所以有個虛類指標ptr_sonClass2_sonClass1,大小為4,而sonClass1的大小為20,所以虛擬繼承sonClass1的大小為24;

3.虛擬繼承VirtualInheritance2,一個虛類指標ptr_sonClass2_VirtualInheritance2=ptr_sonClass2_sonClass1+偏移量,該指標和ptr_sonClass2_sonClass1公用一個指標,只是偏移量不同,所以大小為0(即使再多繼承幾個virtual class,這個指標的大小隻算 一次),而VirtualInheritance2的大小為8,所以總的大小為8。

所以40=8+24+8

總結:

1,普通單繼承,只需將自身成員變數的大小加上父類大小(父類中 有虛擬函式,子類中不管有沒有)若父類沒有虛擬函式,則子類大小需要加上指向虛表的指標大小。

2,普通多繼承,若幾個父類都有虛表,則子類與第一個父類公用一個虛表指標,其他有幾個有虛擬函式的父類則就有幾個虛表指標。

3,虛擬單繼承,此時若子類有虛擬函式則加上一個自身的虛表指標的大小,(若沒有則不加)再加上自身的成員變數大小,還要加上一個虛類指標ptr_sonclass_fatherclass,最後加上父類的大小。

4,多重虛擬繼承,此時若子類有虛擬函式則加上一個自身的虛表指標的大小,(若沒有則不叫)再加上自身的成員變數大小,還要加上 一個公用的虛類指標(不管有幾個虛擬父類,只加一個),在加上所有父類的大小。

5、普通、虛擬混合多繼承,此時子類的大小為自身大小(若子類或普通父類有虛擬函式,則為成員變數+虛表指標大小;若都沒虛擬函式,則就為成員變數大小),加上一個虛類指標大小,在加上虛擬父類的大小,在加上普通父類的大小(除虛表指標,因為它和子類公用一個虛表指標)。

歡迎補充!