天天看點

C++虛繼承和虛基類詳解

多繼承

多繼承(Multiple Inheritance)是指從多個直接基類中産生派生類的能力,多繼承的派生類繼承了所有父類的成員。盡管概念上非常簡單,但是多個基類的互相交織可能會帶來錯綜複雜的設計問題,命名沖突就是不可回避的一個。

多繼承時很容易産生命名沖突,即使我們很小心地将所有類中的成員變量和成員函數都命名為不同的名字,命名沖突依然有可能發生,比如典型的是菱形繼承,如下圖所示:

C++虛繼承和虛基類詳解

圖1:菱形繼承

類 A 派生出類 B 和類 C,類 D 繼承自類 B 和類 C,這個時候類 A 中的成員變量和成員函數繼承到類 D 中變成了兩份,一份來自 A–>B–>D 這條路徑,另一份來自 A–>C–>D 這條路徑。

在一個派生類中保留間接基類的多份同名成員,雖然可以在不同的成員變量中分别存放不同的資料,但大多數情況下這是多餘的:因為保留多份成員變量不僅占用較多的存儲空間,還容易産生命名沖突。假如類 A 有一個成員變量 a,那麼在類 D 中直接通路 a 就會産生歧義,編譯器不知道它究竟來自 A -->B–>D 這條路徑,還是來自 A–>C–>D 這條路徑。下面是菱形繼承的具體實作:

//間接基類A
class A{
protected:
    int m_a;
};

//直接基類B
class B: public A{
protected:
    int m_b;
};

//直接基類C
class C: public A{
protected:
    int m_c;
};

//派生類D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //命名沖突
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

int main(){
    D d;
    return 0;
}      

這段代碼實作了上圖所示的菱形繼承,第 25 行代碼試圖直接通路成員變量 m_a,結果發生了錯誤,因為類 B 和類 C 中都有成員變量 m_a(從 A 類繼承而來),編譯器不知道選用哪一個,是以産生了歧義。

為了消除歧義,我們可以在 m_a 的前面指明它具體來自哪個類:

void seta(int a){ B::m_a = a; }      

這樣表示使用 B 類的 m_a。當然也可以使用 C 類的:

void seta(int a){ C::m_a = a; }      

虛繼承(Virtual Inheritance)

為了解決多繼承時的命名沖突和備援資料問題,C++ 提出了虛繼承,使得在派生類中隻保留一份間接基類的成員。

在繼承方式前面加上 virtual 關鍵字就是虛繼承,請看下面的例子:

//間接基類A
class A{
protected:
    int m_a;
};

//直接基類B
class B: virtual public A{  //虛繼承
protected:
    int m_b;
};

//直接基類C
class C: virtual public A{  //虛繼承
protected:
    int m_c;
};

//派生類D
class D: public B, public C{
public:
    void seta(int a){ m_a = a; }  //正确
    void setb(int b){ m_b = b; }  //正确
    void setc(int c){ m_c = c; }  //正确
    void setd(int d){ m_d = d; }  //正确
private:
    int m_d;
};

int main(){
    D d;
    return 0;
}      

這段代碼使用虛繼承重新實作了上圖所示的菱形繼承,這樣在派生類 D 中就隻保留了一份成員變量 m_a,直接通路就不會再有歧義了。

虛繼承的目的是讓某個類做出聲明,承諾願意共享它的基類。其中,這個被共享的基類就稱為虛基類(Virtual Base Class),本例中的 A 就是一個虛基類。在這種機制下,不論虛基類在繼承體系中出現了多少次,在派生類中都隻包含一份虛基類的成員。

現在讓我們重新梳理一下本例的繼承關系,如下圖所示:

C++虛繼承和虛基類詳解

圖2:使用虛繼承解決菱形繼承中的命名沖突問題

觀察這個新的繼承體系,我們會發現虛繼承的一個不太直覺的特征:必須在虛派生的真實需求出現前就已經完成虛派生的操作。在上圖中,當定義 D 類時才出現了對虛派生的需求,但是如果 B 類和 C 類不是從 A 類虛派生得到的,那麼 D 類還是會保留 A 類的兩份成員。

換個角度講,虛派生隻影響從指定了虛基類的派生類中進一步派生出來的類,它不會影響派生類本身。

在實際開發中,位于中間層次的基類将其繼承聲明為虛繼承一般不會帶來什麼問題。通常情況下,使用虛繼承的類層次是由一個人或者一個項目組一次性設計完成的。對于一個獨立開發的類來說,很少需要基類中的某一個類是虛基類,況且新類的開發者也無法改變已經存在的類體系。

C++标準庫中的 iostream 類就是一個虛繼承的實際應用案例。iostream 從 istream 和 ostream 直接繼承而來,而 istream 和 ostream 又都繼承自一個共同的名為 base_ios 的類,是典型的菱形繼承。此時 istream 和 ostream 必須采用虛繼承,否則将導緻 iostream 類中保留兩份 base_ios 類的成員。

C++虛繼承和虛基類詳解

圖3:虛繼承在C++标準庫中的實際應用

虛基類成員的可見性

因為在虛繼承的最終派生類中隻保留了一份虛基類的成員,是以該成員可以被直接通路,不會産生二義性。此外,如果虛基類的成員隻被一條派生路徑覆寫,那麼仍然可以直接通路這個被覆寫的成員。但是如果該成員被兩條或多條路徑覆寫了,那就不能直接通路了,此時必須指明該成員屬于哪個類。

以圖2中的菱形繼承為例,假設 A 定義了一個名為 x 的成員變量,當我們在 D 中直接通路 x 時,會有三種可能性:

如果 B 和 C 中都沒有 x 的定義,那麼 x 将被解析為 A 的成員,此時不存在二義性。

如果 B 或 C 其中的一個類定義了 x,也不會有二義性,派生類的 x 比虛基類的 x 優先級更高。

如果 B 和 C 中都定義了 x,那麼直接通路 x 将産生二義性問題。

參考

繼續閱讀