【C++】菱形繼承與虛擬菱形繼承的對比分析

    在繼承中經常會遇到這種情況:有一個超類A,子類B1,B2繼承了A,而類D又繼承了父類B1,B2。在這種情況下如果按照我們以前的正常的菱形繼承的話會有一個問題是子類C會繼承兩次超類A中的成員,當在C中想訪問繼承來自B1,B2中A的元素會出現兩個問題:

    問題一、數據的冗餘

    問題二、訪問的二意性

出現了這種問題那麼我們該如何解決呢?

    C++中爲了解決這個問題引入了虛擬菱形繼承,那麼虛擬菱形繼承是怎麼解決的呢?

首先給大家先畫兩個圖比較下菱形繼承和虛擬菱形繼承在繼承時在內存中的成員分佈情況:

   一、 菱形繼承:

沒有繼承以前的超類A和父類B1,B2;

wKiom1cNmZKgmmKmAAAuXCSwYqg368.png

繼承超類A以後的B1,B2;

wKiom1cNmpPy48ivAAApVk8ruhY766.png

子類D繼承B1,B2以後的內存分佈情況

wKiom1cNnnTTyUf1AAA61Wbog04930.png

    通過圖我們可以看出菱形繼承存在很多的數據冗餘,如超類A的成員ia,ca都有兩份,訪問時也會出先二義性的錯誤。


    二、虛擬菱形繼承

沒有繼承以前的超類A和父類B1,B2;

wKiom1cNmZKgmmKmAAAuXCSwYqg368.png

虛擬繼承超類A以後的B1,B2;

wKioL1cWBHyAcFWLAAAyVRbvLI0886.png

虛擬繼承B1,B2後的D;

wKioL1cWF3DAhrnEAAAjWHER6tE886.png

看完分佈圖以後,我們看下代碼和D在內存中的分佈

#include<iostream>
using namespace std;

class A
{
public:
    int ia;
    char ca;
public:
    A()
    :ia(0)
    , ca('A')
    {}

    virtual void f()
    {
        cout << "A::f()" << endl;
    }

    virtual void Bf()
    {
        cout << "A::Af()" << endl;
    }
};

class B1:virtual public A
{
public:
    int ib1;
    char cb1;
public:
    B1()
    :ib1(1)
    , cb1('1')
    {}
    virtual void f()
    {
        cout << "B1::f()" << endl;
    }

    virtual void f1()
    {
        cout << "B1::f1()" << endl;
    }

    virtual void B1f()
    {
        cout << "B1::B1f()" << endl;
    }

};

class B2:virtual public A
{
public:
    int ib2;
    char cb2;
public:
    B2()
    :ib2(2)
    , cb2('2')
    {}

    virtual void f()
    {
        cout << "B2::f()" << endl;
    }

    virtual void f2()
    {
        cout << "B2::f2()" << endl;
    }

    virtual void B2f()
    {
        cout << "B2::B2f()" << endl;
    }

};

class D :public B1,public B2
{
public:
    int id;
    char cd;
public:
    D()
        :id(3)
        , cd('D')
    {}

    virtual void f()
    {
        cout << "D::f()" << endl;
    }

    virtual void f1()
    {
        cout << "D::f1()" << endl;
    }

    virtual void f2()
    {
        cout << "D::f2()" << endl;
    }

    virtual void Df()
    {
        cout << "D::Df()" << endl;
    }

};

typedef void(*Fun)();
void PrintVTable(int* VTable)
{
    cout << " 虛表地址>" << VTable << endl;

    for (int i = 0; VTable[i] != 0; ++i)
    {
        printf(" 第%d個虛函數地址 :0X%x,->", i, VTable[i]);
        Fun f = (Fun)VTable[i];
        f();
    }
}



void test()
{
    A a;
    B1 b1;
    B2 b2;
    D d1;

    cout << "sizeof(A)::" << sizeof(a) << endl;
    cout << "sizeof(B1)::" << sizeof(b1) << endl;
    cout << "sizeof(B2)::" << sizeof(b2) << endl;
    cout << "sizeof(D)::" << sizeof(d1) << endl;

    int* VTable = (int*)(*(int*)&d1);
    PrintVTable(VTable);
    cout << "        虛基表指針->: " << (int*)((int*)&d1 + 1) << endl;
    cout << "         B1::ib1 = " << *(int*)((int*)&d1 + 2) << endl;
    cout << "         B1::cb1 =" << (char)*((int*)&d1 + 3) << endl;

    VTable = (int*)*((int*)&d1 + 4);
    PrintVTable(VTable);
    cout << "        虛基表指針->:" << (int*)((int*)&d1 + 5) << endl;
    cout << "         B2::ib2 =" << *(int*)((int*)&d1 + 6) << endl;
    cout << "         B2::cb2 =" << (char)*((int*)&d1 + 7) << endl;

    cout << "         D::ID =" << *((int*)&d1 + 8) << endl;
    cout << "         D::cd =" << (char)*((int*)&d1 + 9) << endl;
    cout << " 虛基表的偏移地址->:"<<(int*)((int*)&d1 + 10) << endl;
    VTable = (int*)*((int*)&d1 + 11);
    PrintVTable(VTable);
    cout << "         A::ia =" << *(int*)((int*)&d1 + 12) << endl;
    cout << "         A::ca =" << (char)*((int*)&d1 + 13) << endl;
    
}

int main()
{
    test();
    system("pause");
    return 0;
}

一、父類b1的內存分佈情況

wKioL1cWF-nzUh7eAAAxLk4yito290.png


二、父類b2的內存分佈情況

wKiom1cWF1jBI1U2AAAl-Opm3OU513.png


三、子類d1的內存分佈情況

wKiom1cWF4bCPmJAAAA3MZ3BfCo802.png


這些都跟前面畫圖分析的一樣。我們再看一下每個類的大小:

wKiom1cWGESRz5noAABvGibnQiM459.png


將菱形繼承與虛擬菱形繼承做比較:

wKioL1cWH-nhDUYTAAAfnmTQYro878.png


  按照正常情況下:在菱形繼承與虛擬菱形繼承時,超類大小一樣,但從父類開始大小發生區別,父類多了12個字節,子類多了8個字節。


  由於要想消除二義性與冗餘性,就得將B1、B2中的A部分變爲一份,那隻能將B1、B2中A中共同的部分變爲指針指向Base部分。爲什麼會這樣呢?


  一、對於父類B1、B2來說因爲多產生了三個指針,前圖中沒畫出來,可以參照子類D的圖,通過虛擬繼承,多增加了一個虛基表指針,一個虛基表的偏移地址,另外還繼承了A的虛表,作用是指向一個地址,地址中保存着父類增加的指針的地址與超類的地址偏移值,通過地址與偏移值相加,找到超類成員部分,並且兩個父類指針都指向的是同一塊空間。所以多了12個字節。


  二、同理,對於子類D來說在同時多了這些東西的同時減去重複繼承的超類的成員最後就只是多了8字節


  通過這種處理子類中父類與超類公共部分都是同一塊存儲空間,就可以解決菱形繼承的二義性與數據冗餘問題了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章