C++知識點隨筆(五):虛繼承

吃不夠的圓兒寶發表於2015-08-20

虛繼承的出現就是為了解決多繼承中訪問不明確的問題。
首先讓我們先看一下虛繼承的程式碼:

#include<iostream>
using namespace std;

class AA
{
public:
    int m_a;
    AA()
    {
        m_a = 100;
    }
};

class BB : virtual public AA
{
public:
    int m_b;
    BB()
    {
        m_a = 200;
    }
};

class CC : virtual public AA
{
public:
    int m_c;
    CC()
    {
        m_a = 300;
    }
};

class DD : public BB, public CC
{
public:
    int m_d;
    DD()
    {
        m_a = 400;
    }
};

int main()
{
    DD dd;
    cout << sizeof(dd) << endl;
    cout << &dd << endl;
    cout << &dd.m_a << endl;
    cout << &dd.m_b << endl;
    cout << &dd.m_c << endl;
    cout << &dd.m_d << endl;

    cout << dd.AA::m_a << endl;
    cout << dd.BB::m_a << endl;
    cout << dd.CC::m_a << endl;
    cout << dd.m_a << endl;

    system("pause");
    return 0;
}

輸出結果:
這裡寫圖片描述
dd物件在記憶體中的結構如下:
這裡寫圖片描述
我們看到虛繼承中,記憶體的分配是與多繼承不同的,我們不再是賦值兩份AA物件,而是將虛基類放在了最後面,然後原來的BB和CC所繼承的AA的地址存放的是指向AA物件的指標,這樣做的另一個好處是還可以節約記憶體空間,因為如果AA物件的大小是100個位元組,那我們也只需要一個4位元組的指標指向它就可以了,而不用賦值一份兒佔用空間。

注意:虛基類只有一份,並且所有繼承虛基類的子類都要虛繼承,否則如果有一個不是虛繼承的那也會再複製一份AA,記憶體中同樣存在了兩份AA,那麼訪問不明確的問題就還是存在。

虛基類為什麼要放在最後?
因為我們在使用這個虛基類的時候首先要知道是哪一個物件(就是指向虛基類的指標)呼叫的它,我們把虛基類放在最後就是要保證:前面的所有的物件使用虛基類的時候都是通過指向虛基類的指標找到的。如果我們不放在後面而是放在前面,那麼找到虛基類就有了兩種方式:直接使用物件的地址 or 指向虛基類的指標,這樣我們在使用虛基類的時候還要加一個判斷,來判斷到底是誰呼叫的呢?這樣顯然更麻煩了。所以我們要把虛基類放在後面,讓所有子類都是通過指向虛基類的指標來找到它。

含有虛擬函式的虛繼承

我們首先看一道題:下列程式的結果是什麼?

#include <iostream>
#include <memory.h>
#include <assert.h>
using namespace std;

class A
{
    char k[3];
public:
    virtual void aa(){};
};

class B : public virtual A
{
    char j[3];
public:
    virtual void bb(){};
};

class C : public virtual B
{
    char i[3];
public:
    virtual void cc(){};
};

int main(int arge,char *argv[])
{
    cout<<"sizeof(A):"<<sizeof(A)<<endl;
    cout<<"sizeof(B):"<<sizeof(B)<<endl;
    cout<<"sizeof(C):"<<sizeof(C)<<endl;

    system("pause");
    return 0;
}

輸出結果:
這裡寫圖片描述
我們來看一下c物件在記憶體中的結構:
這裡寫圖片描述
從圖中我們可以看出:每個類最初始的4個位元組都是指向虛擬函式列表的指標,由於A是虛基類,沒有父類,所以A只有8個位元組大小;而B虛繼承了A,所以B要有一個指向A類地址的指標,所以B類是12+8(A的大小)=20;同樣,C類虛繼承了B,所以C要有一個指向B類地址的指標,所以C類是12+20(B的大小)=32。

什麼情況下會產生新的虛擬函式列表呢?
為了便於理解,我們首先來看一下上一個例子中,改為普通繼承情況下的輸出:
這裡寫圖片描述
我們現在再看一下c物件在記憶體中的結構:
這裡寫圖片描述
那麼我們是通過什麼方式來確定他們的結構的呢?就是分別用不同型別的指標去指向c這個物件。如下:

C c;
A* a = &c;
B* b = &c;

當我們使用普通繼承的時候a、b、c所返回的地址都是相同的,即 0C 地址。所以在實現多型的時候,父類將虛擬函式加到了這個虛表中,而他的子類在重寫虛擬函式的時候也是首先複製了父類的虛擬函式列表,然後用自己重寫的虛擬函式的指標覆蓋父類所存入的指標。這樣我們在父類呼叫多型的時候就可以直接從虛表裡找到新的虛擬函式的地址了。

相關文章