C++內存中的封裝、繼承、多態(下)


上篇講述了內存中的封裝模型,下篇我們講述一下繼承和多態。

 

二、繼承與多態情況下的內存布局

由於繼承下的內存布局以及構造過程很多書籍都講得比較詳細,所以這里不細講。重點講多態。

 

繼承有以下這幾種情況:

1.單一繼承

2.多重繼承

3.重復繼承

4.虛擬繼承

 

1.單一繼承的場合

假設有以下繼承關系,那么大致的內存布局如下

 

代碼

class Parent
{
public:
    
    int p;
};

class Child:public Parent
{
public:

    int c;

};

class GrandChild:public Child
{
public:
    
    int gc;
};

對象布局:

 

成員變量的布局很好理解,那么在有虛函數的場合,虛函數表到底又是怎么樣的呢?

為了解決這個問題我完善上面的代碼。

class Parent
{
public:
    Parent():p(1){}

    virtual void fun1(){ cout<<"Parent::fun1"<<endl; }
    virtual void fun2(){ cout<<"Parent::fun2"<<endl; }
    virtual void fun3(){ cout<<"Parent::fun3"<<endl; }
    
    int p;
};

class Child:public Parent
{
public:
    Child():c(10){}

    virtual void fun1()  { cout<<"Child::fun1"<<endl; }
    virtual void c_fun2(){ cout<<"Child::c_fun2"<<endl; }
    virtual void c_fun3(){ cout<<"Child::c_fun3"<<endl; }

    int c;
};

class GrandChild:public Child
{
public:
    GrandChild():gc(100){}

    virtual void fun1()  { cout<<"GrandChild::fun1"<<endl; }
    virtual void c_fun2(){ cout<<"GrandChild::c_fun2"<<endl; }
    virtual void gc_fun3(){ cout<<"GrandChild::gc_fun3"<<endl; }

    int gc;
};


int main()
{
    GrandChild grandc;
    Child       child;
    Parent     parent;

    return 0;
}

 

我們先使用調試窗口查看一下虛函數表

可以看到三張表是不同的,可以看到fun1函數被改寫了兩次。

比較蛋疼的是grandc只能看到三個函數,君不見c_fun2和gc_fun3,還得自己動手來。

繼上篇的內容,我們使用pf這個函數指針:

typedef void (*PF)();
PF pf = NULL;

在主函數里我們寫下代碼:

    int* vtab = (int*)*(int*)&grandc;

    for (; *vtab != NULL; vtab++)
    {
        pf = (PF)*vtab;
        pf();
    }

    int* member = (int*)&grandc;
    cout<<*++member<<endl;
    cout<<*++member<<endl;
    cout<<*++member<<endl;

 

 

成員變量輸出結果與我們上篇的結論一致,咱們主要來看一下虛函數部分。

並且前三個函數同調試窗口的顯示結果。

 

我們依據以上結果可以得到這么幾個結論:

1.單一繼承時,不同的類維護不同的虛函數表(only one),並且虛函數表初始情況是父類的樣子。

2.當發生overwrite時,例如fun1和c_fun2都會沖刷掉父類的虛函數,代替之。

3.沒有發生overwrite時,直接添加到虛函數表中。

 

圖示:

截止到這里,結合上篇的內容,就能很容易理解為什么使用父類指針能產生多態的效果了。

 

2.多重繼承的場合

假設有以下繼承關系,那么大致的內存布局如下

由於是多繼承,根據1的觀點,單一繼承時一個類維護一個虛函數表。多繼承時怎么辦呢?

那只能是繼承幾個類,就有幾張虛函數表了。

 

實例代碼如下:

class Base1
{
public:
    Base1():b1(1){}

    virtual void fun1(){ cout<<"Base1::fun1"<<endl; }
    virtual void fun2(){ cout<<"Base1::fun2"<<endl; }
    virtual void fun3(){ cout<<"Base1::fun3"<<endl; }
    
    int b1;
};

class Base2
{
public:
    Base2():b2(2){}
    virtual void fun1(){ cout<<"Base2::fun1"<<endl; }
    virtual void fun2(){ cout<<"Base2::fun2"<<endl; }
    virtual void fun3(){ cout<<"Base2::fun3"<<endl; }    

    int b2;
};

class Base3
{
public:
    Base3():b3(3){}
    virtual void fun1(){ cout<<"Base3::fun1"<<endl; }
    virtual void fun2(){ cout<<"Base3::fun2"<<endl; }
    virtual void fun3(){ cout<<"Base3::fun3"<<endl; }
    
    int b3;
};

class Derived:public Base1, public Base2, public Base3
{
public:
    Derived():d(100){}
    virtual void fun1(){ cout<<"Derived::fun1"<<endl; }
    virtual void d_fun(){ cout<<"Derived::d_fun"<<endl; }

    int d;
};

 

通過調試窗口查看一下虛函數表:

可以明確的看到標注了for base,源自哪個基類的虛函數表。

並且可以看到fun1在三個表中全部被重寫了,那么我們關心的d_fun到底會放在哪個表呢?

我們使用相同的辦法:

typedef void (*PF)();
PF pf = NULL;

    Derived dd;
/////////////Base1///////////
    int* vtab1 = (int*)*(int*)&dd;
    for (; *vtab1 != NULL; vtab1++)
    {
        pf = (PF)*vtab1;
        pf();
    }
    int* member1 = (int*)&dd;
    cout<<*++member1<<endl;

/////////////Base2///////////
    int* vtab2 = (int*)*((int*)&dd + sizeof(Base1)/4);
    for (; *vtab2 != NULL; vtab2++)
    {
        pf = (PF)*vtab2;
        pf();
    }
    int* member2 = (int*)((int*)&dd + sizeof(Base1)/4);
    cout<<*++member2<<endl;

/////////////Base3//////////////
    int* vtab3 = (int*)*((int*)&dd + (sizeof(Base1)+sizeof(Base2))/4);
    for (; *vtab3 != NULL; vtab3++)
    {
        pf = (PF)*vtab3;
        pf();
    }
    int* member3 = (int*)((int*)&dd + (sizeof(Base1)+sizeof(Base2))/4);
    cout<<*++member3<<endl;

 

偷了點懶,因為使用的是int型,所以沒有存在字節對齊的情況,直接使用的sizeof/4,使用這種偏移量來訪問不同的base區域。

以下是輸出結果:

我們可以看到d_fun被放到了第一個函數表中去了(聲明的次序的第一個,實例代碼是base1的部分)。

結論:

1.多重繼承的場合,overwirte時,父類的函數在三個表中會全部被重寫。

2.子類新添加的虛函數被放到第一個虛函數表中。

圖示:

 

3.重復繼承的場合

其實重復繼承只是多重繼承的特例,一切的規則依然按照多重繼承的規則實行。只是特殊在祖父類生成了兩個拷貝鏡像,形成數據重復,並且造成二義性。

無論從設計的的角度還是維護的角度,這都是一個失敗的選擇。

所以我們不重點討論,直接跳到虛擬繼承。

 

4.虛擬繼承的場合

關於虛擬繼承的對象模型,其實有多種方法,本文使用的的環境是vs2008,屬於微軟想的招兒。《深入C++對象模型》一書中明確指出了

虛擬繼承的場合,對象模型的構建方式沒有固定的標准,主要的思路是拆分成不變局部和共享局部。當然只有更好的方法,也都是為了達到更高的存取效率。

所以本文描述的內存布局或許只在微軟編譯器的場合成立,正因為如此,我們把重點放在虛擬繼承的要達到的效果上。

假設有以下繼承關系:

實例代碼:

class Base
{
public:
    Base():b(1){}

    virtual void fun(){   cout<<"Base::fun"<<endl; }
    virtual void B_fun(){ cout<<"Base::B_fun"<<endl; }

    int b;
};

class Base1:virtual public Base
{
public:
    Base1():b1(11){}

    virtual void fun(){    cout<<"Base1::fun"<<endl; }
    virtual void fun1(){   cout<<"Base1::fun1"<<endl; }
    virtual void B_fun1(){ cout<<"Base1::B_fun1"<<endl; }
    
    int b1;
};

class Base2:virtual public Base
{
public:
    Base2():b2(12){}
    virtual void fun(){    cout<<"Base2::fun"<<endl; }
    virtual void fun2(){   cout<<"Base2::fun2"<<endl; }
    virtual void B_fun2(){ cout<<"Base2::B_fun2"<<endl; }    

    int b2;
};



class Derived:public Base1, public Base2
{
public:
    Derived():d(111){}
    
    virtual void fun(){   cout<<"Derived::fun"<<endl; }
    virtual void fun1(){  cout<<"Derived::fun1"<<endl; }
    virtual void fun2(){  cout<<"Derived::fun2"<<endl; }
    virtual void D_fun(){ cout<<"Derived::D_fun"<<endl; }    

    int d;
};

 

先來討論單一虛擬繼承的情況,看一下Base1的布局:

bb是Base的對象,bb1是Base1的對象。

明顯可以看到與普通單一繼承不同,使用了兩個虛函數指針,一個指向了虛基類Base的表,以及自己再生成一個表。

而指向虛基類Base的表的虛函數fun明顯被重寫了。

使用代碼讀取:

int* vtab = (int*)*(int*)&bb1;
    for (; *vtab != NULL; vtab++)
    {
        pf = (PF)*vtab;
        pf();
    }

 

 

這個循環運行會中斷,原因是vtab訪問了一個神奇的數字-4,這個是用來隔開的,不小心訪問了。(陳皓老師的一篇博文《C++對象的內存布局》也遇到了相同的問題,而GCC卻沒有)

足以證明,這里的不變局部是Derived自己后來添加的函數。而共享局部fun跑到虛基類包含的虛函數表上去了。

我們使用二級指針來解決中斷的問題。

Base1 bb1;

    int** pVtab = (int**)&bb1;

    //////Base1//////////
    pf = (PF)pVtab[0][0];
    pf(); //Base1::fun1
    
    pf = (PF)pVtab[0][1];
    pf(); //Base1::B_fun1

    //cout << pVtab[0][2] << endl;//訪問是一個隨機值,證明越界了。
    cout << pVtab[1][0] << endl;//-4

    cout << (int)*((int*)(&bb1)+2) <<endl; //b1

    cout <<"0x"<<(int*)*((int*)(&bb1)+3) <<endl;//NULL 父類子類分隔處

    //////Base//////////
    pf = (PF)pVtab[4][0];
    pf();
    pf = (PF)pVtab[4][1];
    pf();
    cout << pVtab[4][2] << endl;//0x00

    cout << (int)*((int*)(&bb1)+5) <<endl; //b

 

可以看出內存布局:

1.不變布局(子類)放在對象模型的前端,共享布局(虛基類)放在尾端。

2.其中子類部分,虛函數表使用了-4作為分隔結尾。接下來是子類成員變量值

3.虛基類屬於共享局部,是一個正常的虛函數表布局,並且重寫了fun函數。

圖示:

 

這樣是能夠保證共享部分處於虛基類中(包括虛函數表),不變部分處於子類中。

 

接下來看完整的繼承結構,解析Derived的布局。

使用代碼:

Derived dd;

    int** pVtab = (int**)&dd;

    //////Base1//////////
    pf = (PF)pVtab[0][0];
    pf(); 
    
    pf = (PF)pVtab[0][1];
    pf();

    cout << pVtab[1][0] << endl;//-4

    cout << (int)*((int*)(&dd)+2) <<endl; //b1

    //////Base2//////////
    pf = (PF)pVtab[3][0];
    pf();
    pf = (PF)pVtab[3][1];
    pf();

    cout << pVtab[4][0] << endl;//-4
    cout << (int)*((int*)(&dd)+5) <<endl; //b2

    //////Derived 成員//////////
    cout << (int)*((int*)(&dd)+6) <<endl; //d

    //////NULL虛基類分隔//////////
    cout << "0x"<<(int*)*((int*)(&dd)+7) <<endl;

    pf = (PF)pVtab[8][0];
    pf();
    pf = (PF)pVtab[8][1];
    pf();
    cout << (int)*((int*)(&dd)+9) <<endl; //b

 

運行結果:

與單一虛擬繼承類似:

1.按照聲明的次序,不變布局(父類)依次放在對象模型的前端,共享布局(虛基類)放在最尾端。

2.其中不變布局部分,虛函數表使用了-4作為分隔結尾。接下來是子類成員變量值

3.虛基類屬於共享局部,是一個正常的虛函數表布局,並且重寫了fun函數。

 

圖就不畫了,與單一虛擬繼承的情況類似。

引用《深入C++對象模型》一書的描述:

要在編譯器中支持虛擬繼承,困難度頗高。

難度在於,要找到一個足夠有效的辦法,將Base1和Base2各自維護的Base部分,折疊成為一個由Derived單一維護的Base部分,並且還可以保持base class和Derived class的指針之間的多態操作。

這也整是虛擬繼承要達到的效果。

 

至此,全篇差不多講完了。

主要參考書籍《深入C++對象模型》以及上文提到的陳皓老師的博文,內容稍長,難免有紕漏,望大家指正。

 


免責聲明!

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



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