C++虛基類與虛繼承
阿新 • • 發佈:2019-01-22
為什麼有虛繼承
多繼承(Multiple Inheritance)是指從多個直接基類中產生派生類的能力,多繼承的派生類繼承了所有父類的成員。儘管概念上非常簡單,但是多個基類的相互交織可能會帶來錯綜複雜的設計問題,命名衝突就是不可迴避的一個。 多繼承時很容易產生命名衝突,即使我們很小心地將所有類中的成員變數和成員函式都命名為不同的名字,命名衝突依然有可能發生,比如典型的是菱形繼承,如下圖所示:圖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;
- }
- void seta(int a){ B::m_a = a; }
- void seta(int a){ C::m_a = a; }
- //間接基類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;
- }
圖2:使用虛繼承解決菱形繼承中的命名衝突問題 觀察這個新的繼承體系,我們會發現虛繼承的一個不太直觀的特徵:必須在虛派生的真實需求出現前就已經完成虛派生的操作。在上圖中,當定義 D 類時才出現了對虛派生的需求,但是如果 B 類和 C 類不是從 A 類虛派生得到的,那麼 D 類還是會保留 A 類的兩份成員。 換個角度講,虛派生隻影響從指定了虛基類的派生類中進一步派生出來的類,它不會影響派生類本身。 在實際開發中,位於中間層次的基類將其繼承宣告為虛繼承一般不會帶來什麼問題。通常情況下,使用虛繼承的類層次是由一個人或者一個專案組一次性設計完成的。對於一個獨立開發的類來說,很少需要基類中的某一個類是虛基類,況且新類的開發者也無法改變已經存在的類體系。 C++標準庫中的 iostream 類就是一個虛繼承的實際應用案例。iostream 從 istream 和 ostream 直接繼承而來,而 istream 和 ostream 又都繼承自一個共同的名為 base_ios 的類,是典型的菱形繼承。此時 istream 和 ostream 必須採用虛繼承,否則將導致 iostream 類中保留兩份 base_ios 類的成員。
圖3:虛繼承在C++標準庫中的實際應用 虛基類成員的可見性 因為在虛繼承的最終派生類中只保留了一份虛基類的成員,所以該成員可以被直接訪問,不會產生二義性。此外,如果虛基類的成員只被一條派生路徑覆蓋,那麼仍然可以直接訪問這個被覆蓋的成員。但是如果該成員被兩條或多條路徑覆蓋了,那就不能直接訪問了,此時必須指明該成員屬於哪個類。 以圖2中的菱形繼承為例,假設 B 定義了一個名為 x 的成員變數,當我們在 D 中直接訪問 x 時,會有三種可能性:
- 如果 B 和 C 中都沒有 x 的定義,那麼 x 將被解析為 B 的成員,此時不存在二義性。
- 如果 B 或 C 其中的一個類定義了 x,也不會有二義性,派生類的 x 比虛基類的 x 優先順序更高。
- 如果 B 和 C 中都定義了 x,那麼直接訪問 x 將產生二義性問題。