博客 / 詳情

返回

每日一個C++知識點|菱形繼承

繼承是C++面向對象的核心特性之一,説明類與類之間的特性是可以繼承的,這大大提高了代碼的複用性,優化了程序結構。但是濫用繼承也會導致菱形繼承的多繼承問題。

菱形繼承

什麼是菱形繼承呢?指一個派生類同時繼承兩個直接基類,這兩個直接基類又繼承自同一個間接基類,最終形成 “菱形” 的繼承結構。

下面用代碼展示菱形繼承的結構示例:

// 頂層基類
class A {
public:
    int a;
    A(int val) : a(val) {}
};

// 中間基類 B,繼承 A
class B : public A {
public:
    B(int val) : A(val) {}
};

// 中間基類 C,繼承 A
class C : public A {
public:
    C(int val) : A(val) {}
};

// 最終派生類 D,同時繼承 B 和 C
class D : public B, public C {
public:
    // 問題1:初始化 A 時,B 和 C 都會分別初始化 A,導致 A 被初始化兩次
    D(int val1, int val2) : B(val1), C(val2) {}
};

int main() {
    D d(1, 2);
    // 問題2:訪問 a 時,編譯器無法確定是 B::A::a 還是 C::A::a,直接報錯
    // cout << d.a << endl; 
    // 必須顯式指定,但這違背了“單一繼承”的邏輯,且數據冗餘(d 中有兩個 a)
    cout << d.B::a << endl; // 輸出 1
    cout << d.C::a << endl; // 輸出 2
    return 0;
}

上述問題中,A為頂級基類,B和C繼承A,初始化 A 時,B 和 C 都會分別初始化 A,導致 A 被初始化兩次;訪問 a 時,編譯器無法確定是 B::A::a 還是 C::A::a,直接報錯

注:“B::A::a”的含義是有兩層:

  1. "A::a"表示 “類 A 中的成員變量 a
  2. "B::"B 是 A 的派生類

核心問題

菱形繼承的核心問題是間接基類的成員會被多次複製,導致數據冗餘、二義性,甚至邏輯錯誤。

數據冗餘表現在間接基類 A 的成員因為B和C的緣故會在最終派生類 D 中存在兩份,浪費內存;

二義性則表現在直接訪問 D 對象的 A 成員時,編譯器無法區分是 B 繼承的 A 還是 C 繼承的 A,就會造成編譯報錯;

邏輯錯誤:若 A 有虛函數,多態調用時可能因重複的基類指針導致行為異常。原因是非虛繼承的菱形結構中,最終派生類會包含兩份 A 的虛指針(vptr),多態調用時無法確定該用哪一個,導致調用結果不符合預期,甚至崩潰。

菱形繼承的內存佈局如下圖所示:

解決方案:虛繼承

由於多繼承會造成菱形繼承問題,那麼C++ 提供虛繼承機制就是解決菱形繼承的辦法。虛繼承通過讓中間基類(BC)共享同一個間接基類(A)的實例,從而消除數據冗餘和二義性

在中間基類繼承頂層基類時,添加 virtual 關鍵字,用代碼舉例如下:

// 頂層基類(不變)
class A {
public:
    int a;
    A(int val) : a(val) {}
};

// 中間基類 B:虛繼承 A
class B : virtual public A {
public:
    // 虛繼承下,B 的構造函數不再直接初始化 A(A 的初始化由最終派生類負責)
    B() {}
};

// 中間基類 C:虛繼承 A
class C : virtual public A {
public:
    C() {}
};

// 最終派生類 D:必須直接初始化虛基類 A
class D : public B, public C {
public:
    // 核心:虛基類 A 的構造由最終派生類 D 統一初始化,避免重複
    D(int val) : A(val), B(), C() {}
};

int main() {
    D d(10);
    // 無歧義:d 中只有一份 A::a
    cout << d.a << endl; // 輸出 10
    cout << d.B::a << endl; // 仍可顯式訪問,結果同上
    cout << d.C::a << endl; // 結果同上
    return 0;
}

通過上述代碼,我們深入分析虛繼承的底層原理,虛繼承是通過虛基類表(vbtable) 和虛基類指針(vbptr) 實現的:

  1. 中間基類(BC)的對象中會增加一個 vbptr 指針,指向虛基類表;
  2. 虛基類表存儲當前對象到虛基類(A)實例的偏移量;
  3. 最終派生類(D)中只保留一份 A 的實例,B 和 C 的 vbptr 都指向這同一個實例。

非虛繼承的內存佈局示意圖如下所示:

虛繼承的內存佈局示意圖如下所示:

雖然虛繼承可以解決菱形繼承的問題,但在現實開發中,為了減少不必要的麻煩,儘量避免使用多繼承。

接口多繼承的安全場景

若頂層基類是純虛類,即使是菱形繼承結構,也無數據冗餘(因為純虛類無成員變量),此時無需虛繼承,用代碼舉例如下:

// 純虛接口 A
class A {
public:
    virtual void func() = 0;
    virtual ~A() = default;
};

class B : public A {
public:
    void func() override { cout << "B::func" << endl; }
};

class C : public A {
public:
    void func() override { cout << "C::func" << endl; }
};

class D : public B, public C {
public:
    // 必須重寫 func,否則 D 仍是抽象類(解決二義性)
    void func() override { B::func(); }
};

int main() {
    D d;
    d.func(); // 輸出 B::func,無歧義
    return 0;
}

總結

菱形繼承的核心問題是間接基類成員重複,虛繼承通過共享基類實例解決該問題,實際開發中應優先避免多繼承。

以上就是本文的所有內容,如果本文對你有幫助的話歡迎點贊收藏哦~

感興趣的朋友也歡迎關注喲~我將會持續輸出編程開發的內容~

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.