多繼承與虛繼承

多繼承

一般情況下,派生類都只有一個基類,稱爲單繼承。除此之外,C++也支持多繼承,即一個派生類可以有兩個或多個基類。

多繼承的定義

多繼承的語法如下,假設已經聲明瞭了類A、類B和類C,那麼可以這樣來聲明派生類D:

class D: public A, private B, protected C{
    //類D新增加的成員
}

多繼承的構造函數

多繼承形式下的構造函數和單繼承形式基本相同,只是要在派生類的構造函數中調用多個基類的構造函數,上述類D的構造函數如下:

D(形參列表): A(實參列表), B(實參列表), C(實參列表){
    //其他操作
}

基類構造函數的調用順序和和它們在派生類構造函數中出現的順序無關,而是和聲明派生類時基類出現的順序相同。

深入思考

如果上述中的基類A、B、C中含有相同名字的成員變量或者成員函數時,可能就會出現命名衝突,導致編譯器不知道是操作哪一個成員變量或者成員函數。解決這個問題通常是使用類名::成員數據的訪問形式,如下的例子:

//基類
class BaseA{
public:
    BaseA(int a, int b);
    ~BaseA();
public:
    void show();
protected:
    int m_a;
    int m_b;
};
BaseA::BaseA(int a, int b): m_a(a), m_b(b){
    cout<<"BaseA constructor"<<endl;
}
BaseA::~BaseA(){
    cout<<"BaseA destructor"<<endl;
}
void BaseA::show(){
    cout<<"m_a = "<<m_a<<endl;
    cout<<"m_b = "<<m_b<<endl;
}

//基類
class BaseB{
public:
    BaseB(int c, int d);
    ~BaseB();
    void show();
protected:
    int m_c;
    int m_d;
};
BaseB::BaseB(int c, int d): m_c(c), m_d(d){
    cout<<"BaseB constructor"<<endl;
}
BaseB::~BaseB(){
    cout<<"BaseB destructor"<<endl;
}
void BaseB::show(){
    cout<<"m_c = "<<m_c<<endl;
    cout<<"m_d = "<<m_d<<endl;
}

//派生類
class Derived: public BaseA, public BaseB{
public:
    Derived(int a, int b, int c, int d, int e);
    ~Derived();
public:
    void display();
private:
    int m_e;
};
Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), m_e(e){
    cout<<"Derived constructor"<<endl;
}
Derived::~Derived(){
    cout<<"Derived destructor"<<endl;
}
void Derived::display(){
    BaseA::show();  //調用BaseA類的show()函數
    BaseB::show();  //調用BaseB類的show()函數
    cout<<"m_e = "<<m_e<<endl;
}

虛繼承

引入虛繼承

假如有如下代碼:

class Top
{ 
public: 
    int a;
};

class Left : public Top
{ 
public: 
    int b;
}; 

class Right : public Top
{ 
public: 
    int c;
}; 

class Bottom : public Left, public Right
{ 
public: 
    int d;
};

使用UML圖,我們可以把這個層次結構表示爲:

這裏寫圖片描述

注意Top被繼承了兩次,這意味着類型Bottom的一個實例bottom將有兩個叫做a的元素(分別爲bottom.Left::a和bottom.Right::a)。
Left、Right和Bottom在內存中是如何佈局的?
讓我們先看一個簡單的例子,Left和Right擁有如下的結構:
這裏寫圖片描述

請注意第一個屬性是從Top繼承下來的。這意味着在下面兩條語句後left和top指向了同一地址,我們可以把Left Object當成Top Object來使用(很明顯,Right與此也類似)。代碼如下:

Left* left = new Left(); 
Top* top = left;

那Buttom呢?GCC中該類的內存佈局如下
這裏寫圖片描述

如果我們提升Bottom指針,會發生什麼事呢?

Bottom* bottom = new Bottom();
Left* left = bottom;

這段代碼工作正常。我們可以把一個Bottom的對象當作一個Left對象來使用,因爲兩個類的內存部局是一樣的。那麼,如果將其提升爲Right呢?會發生什麼事?

Right* right = bottom;

這裏寫圖片描述

經過這一步,我們可以像操作正常Right對象一樣使用right指針訪問bottom。雖然,bottom與right現在指向兩個不同的內存地址。出於完整性的緣故,思考一下執行下面這條語句時會出現什麼狀況。

Top* top = bottom;

是的,什麼也沒有。這條語句是有歧義的,編譯器將會報錯:error: Top' is an ambiguous base ofBottom’

Top* topL = (Left*) bottom;
Top* topR = (Right*) bottom;

執行這兩條語句後,topL和left會指向同樣的地址,topR和right也會指向同樣的地址。

綜上所述,就引入的虛繼承解決這個問題,可以將上述的代碼改成如下的結構:

class Top
{ 
public: 
    int a;
}; 

class Left : virtual public Top
{ 
public: 
    int b;
}; 

class Right : virtual public Top
{ 
public: 
    int c;
}; 

class Bottom : public Left, public Right
{ 
public: 
    int d;
};

這就得到了如下的層次結構:
這裏寫圖片描述

雖然從程序員的角度看,這也許更加的明顯和簡便,但從編譯器的角度看,這就變得非常的複雜。重新考慮下Bottom的佈局,其中的一個(也許沒有)可能是:

這裏寫圖片描述

這個佈局的優點是,佈局的第一部分與Left的佈局重疊了,這樣我們就可以很容易的通過一個Left指針訪問 Bottom類。可是我們怎麼處理以下語句:

Right* right = bottom;

哪個地址是我們賦給right的呢?在賦值以後,我們本應該可以像使用普通right對象指針一樣使用right了。但這是不可能的!Right本身的內存佈局是完全不同的,因此我們不能像訪問”真正的”Right對象一樣,來訪問向上轉換的Bottom對象。而且,也沒有其它(簡單的)Bottom佈局可以使Bottom正常運作。解決辦法是複雜的。我們先給出解決方案,之後再來解釋它。

這裏寫圖片描述

你應該注意到了這個圖中的兩個地方。第一,字段的順序是完全不同的(事實上,差不多是相反的)。第二,有幾個vptr指針。這些屬性是由編譯器根據需要自動插入的(使用虛擬繼承,或者使用虛擬函數的時候)。編譯器也在構造器中插入了代碼,來初始化這些指針。

vptr (virtual pointers)指向一個 “虛擬表”。類的每個虛擬基類都有一個vptr指針。要想知道這個虛擬表 (vtable)是怎樣運用的,看看下面的C++ 代碼。

Bottom* bottom = new Bottom();
Left* left = bottom;
int p = left->a;

第二個賦值使left指向了bottom的所在地址(即,它指向了Bottom對象的“頂部”)。我們想想最後一條賦值語句的編譯情況(稍微簡化了):

movl left, %eax # %eax = left
movl (%eax), %eax # %eax = left.vptr.Left
movl (%eax), %eax # %eax = virtual base offset
addl left, %eax # %eax = left + virtual base offset
movl (%eax), %eax # %eax = left.a
movl %eax, p # p = left.a

用語言來描述的話,就是我們用left指向虛擬表,並且由它獲得了“虛擬基類偏移”(vbase)。這個偏移之後就加到了left,然後left就用來指向Bottom對象的Top部分。從這張圖你可以看到Left的虛擬基類偏移是20;如果假設Bottom中的所有字段都是4個字節,那麼給left加上20字節將會確實指向a字段。

經過這個設置,我們就可以同樣的方法訪問Right部分。

Bottom* bottom = new Bottom();
Right* right = bottom; 
int p = right->a;

這裏寫圖片描述

對top的賦值現在可以編譯成像前面Left同樣的方式。唯一的不同就是現在的vptr是指向了虛擬表的不同部位:取得的虛擬表偏移是12,這完全正確(確定!)。我們可以將其圖示概括:

這裏寫圖片描述

當然,這個例子的目的就是要像訪問真正Right對象一樣訪問升級的Bottom對象。因此,我們必須也要給Right(和Left)佈局引入vptrs:

這裏寫圖片描述

現在我們就可以通過一個Right指針,一點也不費事的訪問Bottom對象了。不過,這是付出了相當大的代價:我們要引入虛擬表,類需要擴展一個或更多個虛擬指針,對一個對象的一個簡單屬性的查詢現在需要兩次間接的通過虛擬表(即使編譯器某種程度上可以減小這個代價)。

構造順序

  1. 任何虛基類的構造函數按照它們被繼承的順序調用
  2. 任何非虛基類的構造函數按照它們被繼承的順序調用
  3. 任何成員對象的構造函數按照它們聲明的順序調用
  4. 類自己的苟構造函數

參考鏈接
C++ 多繼承和虛繼承的內存佈局
說說C++多重繼承

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