繼承是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”的含義是有兩層:
- "A::a"表示 “類
A中的成員變量a”- "B::"
B是A的派生類
核心問題
菱形繼承的核心問題是間接基類的成員會被多次複製,導致數據冗餘、二義性,甚至邏輯錯誤。
數據冗餘表現在間接基類 A 的成員因為B和C的緣故會在最終派生類 D 中存在兩份,浪費內存;
二義性則表現在直接訪問 D 對象的 A 成員時,編譯器無法區分是 B 繼承的 A 還是 C 繼承的 A,就會造成編譯報錯;
邏輯錯誤:若 A 有虛函數,多態調用時可能因重複的基類指針導致行為異常。原因是非虛繼承的菱形結構中,最終派生類會包含兩份 A 的虛指針(vptr),多態調用時無法確定該用哪一個,導致調用結果不符合預期,甚至崩潰。
菱形繼承的內存佈局如下圖所示:
解決方案:虛繼承
由於多繼承會造成菱形繼承問題,那麼C++ 提供虛繼承機制就是解決菱形繼承的辦法。虛繼承通過讓中間基類(B、C)共享同一個間接基類(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) 實現的:
- 中間基類(
B、C)的對象中會增加一個vbptr指針,指向虛基類表;- 虛基類表存儲當前對象到虛基類(
A)實例的偏移量;- 最終派生類(
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;
}
總結
菱形繼承的核心問題是間接基類成員重複,虛繼承通過共享基類實例解決該問題,實際開發中應優先避免多繼承。
以上就是本文的所有內容,如果本文對你有幫助的話歡迎點贊收藏哦~
感興趣的朋友也歡迎關注喲~我將會持續輸出編程開發的內容~