1. 程式人生 > >c++單繼承、多繼承、菱形繼承記憶體佈局(虛擬函式表結構)

c++單繼承、多繼承、菱形繼承記憶體佈局(虛擬函式表結構)

這裡寫圖片描述

單繼承:只有一個基類和一個派生類

class Base
{
public:
    virtual void fun1()
    {
        cout << "Base::func1()" << endl;
    }
    virtual void fun2()
    {
        cout << "Base::func2()" << endl;
    }
private:
    int b;
};
class Derive :public Base
{
public:
    virtual void fun1()           //重寫基類虛擬函式,實現多型
{ cout << "Derive::func1()" << endl; } virtual void fun3() { cout << "Derive::func3()" << endl; } void fun4() { cout << "Derive::func4()" << endl; } private: int d; };
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

1. 虛表就是存放虛擬函式的表。

2. 主函式中分別定義一個基類物件和一個派生類物件,通過除錯視窗可以看到所謂的虛表,如下圖(整型b和d未初始化):

這裡寫圖片描述

>也許你會有疑問:除錯視窗中派生類虛表為什麼看不到Derive中的fun3()函式,這是編譯器的問題,我所用的是vs2013,在除錯的時候確實不見fun3()函式,所以有時編譯器的除錯視窗顯示的也不能完全相信,那有什麼辦法證明fun3()函式也在派生類虛表裡呢?通過列印虛表!

程式碼如下:

typedef void (*FUNC)();        //重定義函式指標,指向函式的指標
void PrintVTable(int* vTable)  //列印虛擬函式表
{
    if (vTable == NULL)
    {
        return;
    }
    cout << "虛擬函式表地址:" << vTable << endl;
    int  i = 0;
    for (; vTable[i] != 0; ++i)
    {
        printf(" 第%d個虛擬函式地址 :0X%x,->", i, vTable[i]);
        FUNC f = (FUNC)vTable[i];
        f();         //訪問虛擬函式
    }
    cout << endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
void Test1()
{
    Base b;
    Derive d;
    int* tmp = (int*)(*(int*)&b);     //取到虛擬函式的地址
    PrintVTable(tmp);
    int* tmp1 = (int*)(*(int*)&d);
    PrintVTable(tmp1);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

解析:int* tmp = (int*)(*(int*)&b);

如下圖:

這裡寫圖片描述

列印虛表:

這裡寫圖片描述

>注意:

不知你是否注意到派生類中還有一個函式:void fun4(); 
虛擬函式是為了實現動態多型,是當程式執行到該函式時才會去虛表裡找這個函式;而函式的過載實現的是靜態多型, 是在程式編譯時就能找到該函式地址,而函式:void fun4();不是虛擬函式,自然不會在虛擬函式表裡。

這裡寫圖片描述

class Base1   //基類
{
public:
    virtual void fun1()
    {
        cout << "Base1::fun1" << endl;
    }
    virtual void fun2()
    {
        cout << "Base1::fun2" << endl;
    }
private:
    int b1;
};
class Base2  //基類
{
public:
    virtual void fun1()
    {
        cout << "Base2::fun1" << endl;
    }
    virtual void fun2()
    {
        cout << "Base2::fun2" << endl;
    }
private:
    int b2;
};
class Derive : public Base1, public Base2  //派生類
{
public:
    virtual void fun1()
    {
        cout << "Derive::fun1" << endl;
    }
    virtual void fun3()
    {
        cout << "Derive::fun3" << endl;
    }
private:
    int d1;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

除錯看結果:

這裡寫圖片描述

同樣以上面單繼承列印虛表函式來列印多繼承虛表:void PrintVTable(int* vTable); //列印虛擬函式表

void Test1()
{
    Derive d1;
    int* VTable = (int*)(*(int*)&d1);
    PrintVTable(VTable);
    VTable = (int*)(*((int*)&d1 + sizeof (Base1)/4));
    PrintVTable(VTable);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

解析:VTable = (int*)(*((int*)&d1 + sizeof (Base1)/4));

這裡寫圖片描述

列印多繼承虛表如下圖:

這裡寫圖片描述

這裡寫圖片描述

先了解什麼是菱形繼承?

這裡寫圖片描述 
這裡寫圖片描述

下列程式碼是菱形繼承體系:

class Base          //Derive的間接基類
{
public:
    virtual void func1()
    {
        cout << "Base::func1()" << endl;
    }
    virtual void func2()
    {
        cout << "Base::func2()" << endl;
    }
private:
    int b;
};
class Base1 :public Base  //Derive的直接基類
{
public:
    virtual void func1()          //重寫Base的func1()
    {
        cout << "Base1::func1()" << endl;
    }
    virtual void func3()
    {
        cout << "Base1::func3()" << endl;
    }
private:
    int b1;
};
class Base2 :public Base    //Derive的直接基類
{
public:
    virtual void func1()       //重寫Base的func1()
    {
        cout << "Base2::func2()" << endl;
    }
    virtual void func4()
    {
        cout << "Base2::func4()" << endl;
    }
private:
    int b2;
};
class Derive :public Base1, public Base2
{
public:
    virtual void func1()          //重寫Base1的func1()
    {
        cout << "Derive::func1()" << endl;
    }
    virtual void func5()
    {
        cout << "Derive::func5()" << endl;
    }
private:
    int d;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

菱形繼承其實是一個單繼承與多繼承的結合。

這裡寫圖片描述

下面跟蹤Derive物件d的記憶體佈局:

這裡寫圖片描述

進一步解析如下圖:

這裡寫圖片描述

同樣以上面單繼承列印虛表函式來列印多繼承虛表:void PrintVTable(int* vTable); //列印虛擬函式表

這裡寫圖片描述

這裡寫圖片描述

這裡寫圖片描述

>1. 先來看下面兩個方案:

這裡寫圖片描述

主要解析第一種方案:

vs2003下虛繼承的VBPTR及VBTBL: 
在類中增加一個指標(VBPTR)指向一個VBTBL,這個VBTBL的第一項記載的是從VBPTR 與本類的偏移地址,如果本類有虛擬函式,那麼第一項是FF FF FF FC(也就是-4),如果沒有則是零,第二項起是VBPTR與本類的虛基類的偏移值。

下面這段程式碼與上面菱形繼承(非虛繼承)類似:

class Base
{
public:
    virtual void fun1()
    {
        cout << "Base::fun1()" << endl;
    }
    virtual void fun2()
    {
        cout << "Base::fun2()" << endl;
    }
private:
    int b;
};
class Base1 :virtual public Base  虛繼承
{
public:
    virtual void fun1()          //重寫Base的func1()
    {
        cout << "Base1::fun1()" << endl;
    }
    virtual void fun3()
    {
        cout << "Base1::fun3()" << endl;
    }
private:
    int b1;
};
class Base2 :virtual public Base  //虛繼承
{
public:
    virtual void fun1()       //重寫Base的func1()
    {
        cout << "Base2::fun1()" << endl;
    }
    virtual void fun4()
    {
        cout << "Base2::fun4()" << endl;
    }
private:
    int b2;
};
class Derive :public Base1, public Base2
{
public:
    virtual void fun1()          //重寫Base1的func1()
    {
        cout << "Derive::fun1()" << endl;
    }
    virtual void fun5()
    {
        cout << "Derive::fun5()" << endl;
    }
private:
    int d;
};
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56

1. 詳細地分析一下vs2003下虛繼承的VBPTR及VBTBL:

以Base1 b1;為例子,詳細分析記憶體佈局如下(Base2和Base1的記憶體佈局相似):

sizeof(Base1) = 20;(下圖中黑色區域中所有變數所佔的大小)

這裡寫圖片描述

當在主函式中定義兩個物件Base1 b1和Base2 b2時,還可通過除錯進一步探索其記憶體佈局如下:

這裡寫圖片描述

最後,我們再來探索一下 Derive d 的記憶體佈局,首先我們先通過除錯視窗來跟蹤如下:

這裡寫圖片描述

通過上面除錯視窗可能沒辦法瞭解全部,那麼請看下圖所示:

sizeof(Derive) = 36;(下圖黑色區域所有變數的大小)

這裡寫圖片描述

我們怎麼驗證菱形繼承(虛繼承)的記憶體佈局就是這樣的呢?我們可以通過列印虛表!!

typedef void(*FUNC)();
void PrintVPTR(int* VPTR)                   //列印虛表(虛擬函式)
{
    cout << "虛擬函式表地址:" << VPTR << endl;
    for (int i = 0; VPTR[i] != 0; ++i)
    {
        printf("第%d個虛擬函式地址:0X%x->", i, VPTR[i]);
        FUNC f = (FUNC)VPTR[i];
        f();
    }
    cout << endl;
}
void PrintVBPTR(int* VBPTR)                  //列印偏移地址與值
{
    cout << "虛擬函式表地址:" << VBPTR << endl;
    int i = 0;
    printf("與本類的偏移地址:0X%x\n", VBPTR[i]);
    for (i = 1; VBPTR[i] != 0; i++)
    {
        cout << VBPTR[i] << " " << endl;
    }
    cout << endl;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

主函式中的呼叫如下:

void Test1()
{
    Base b;
    Base1 b1;
    Base2 b2;
    Derive d;
    cout << "sizeof_Base = " << sizeof(Base) << endl;
    int* BvTable = (int*)(*((int*)&b));
    PrintVPTR(BvTable);
    cout << "-------------------------------" << endl;

    cout << "sizeof_Base1 = " << sizeof(Base1) << endl;
    int* BVPTR1 = (int*)(*((int*)&b1));                             //存放自己的虛擬函式(虛表)
    PrintVPTR(BVPTR1);
    int* VBPTR1 = (int*)(*((int*)&b1 + 1));//訪問偏移地址以及偏移量
    PrintVBPTR(VBPTR1);
    int* VPTR1 = (int*)(*((int*)&b1 + (*(VBPTR1 + 1)) / 4 + 1));    //在Base1中訪問Base虛表
    PrintVPTR(VPTR1);
    cout << "-------------------------------" << endl;

    cout << "sizeof_Base2 = " << sizeof(Base2) << endl;
    int* BVPTR2 = (int*)(*((int*)&b2));                      //存放自己的虛擬函式(虛表)
    PrintVPTR(BVPTR2);
    int* VBPTR2 = (int*)(*((int*)&b2 + 1));//訪問偏移地址以及偏移量
    PrintVBPTR(VBPTR2);
    int* VPTR2 = (int*)(*((int*)&b2 + (*(VBPTR2 + 1)) / 4 + 1));//在Base2中訪問Base虛表
    PrintVPTR(VPTR2);
    cout << "-------------------------------" << endl;

    cout << "sizeof_Derive = " << sizeof(Derive) << endl;
    int* dVPTR1 = (int*)(*((int*)&d));                           //存放自己的虛擬函式(虛表)
    PrintVPTR(dVPTR1);
    int* dVBPTR3 = (int*)(*((int*)&d + 1));//訪問偏移地址以及偏移量
    PrintVBPTR(dVBPTR3);
    int* dVPTR2 = (int*)(*((int*)&d + 3));                       //在Derive中訪問Base2虛表
    PrintVPTR(dVPTR2);
    int* dVBPTR = (int*)(*((int*)&d + 4));//訪問偏移地址以及偏移量
    PrintVBPTR(dVBPTR);
    int* VPTR = (int*)(*((int*)&d + (*(dVBPTR3 + 1)) / 4 + 1));  //在Derive中訪問Base虛表
    PrintVPTR(VPTR);
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

總結:

  1. 虛基類例項地址 = 派生類虛擬函式指標+派生類虛擬函式指標到虛基類例項地址的偏移量
  2. 可以通過虛擬繼承消除二義性,但是虛擬繼承的開銷是增加虛擬函式指標。