A
/ \
B C
\ /
D
普通繼承下,D 含兩份 A 子物件:
D::m_a 歧義(來自 B 路徑還是 C 路徑?)class A { protected: int m_a; };
class B : public A { protected: int m_b; };
class C : public A { protected: int m_c; };
class D : public B, public C {
void seta(int a) { m_a = a; } // 編譯錯誤:m_a 歧義
// 必須顯式指定:B::m_a = a; 或 C::m_a = a;
};
在繼承方式前加 virtual:
class B : virtual public A { ... }; // 虛繼承
class C : virtual public A { ... }; // 虛繼承
class D : public B, public C { // 正常繼承
void seta(int a) { m_a = a; } // 正常!只有一份 m_a
};
D 中僅保留一份 A 的成員m_aA 稱為虛基類D 物件佈局 (虛繼承): +------------------+ | B 部分 (m_b) | +------------------+ | C 部分 (m_c) | +------------------+ | D 部分 (m_d) | +------------------+ | A 部分 (m_a) | ← 共享單一實例,通常置於最後 +------------------+ B/C 內含指向 A 的指針/偏移量 (vptr 或隱藏指針)
編譯器通過虛基類指針/偏移量表在運行時定位共享的 A 子物件。
假設 B 定義成員 x,D 中直接訪問 x:
| 情況 | 結果 |
|---|---|
B、C 均無 x | 解析為 B::x (首基類優先),無歧義 |
B 或 C 之一有 x | 派生類 x 優先於虛基類 x,無歧義 |
B、C 皆有 x | 歧義,需顯式指定 B::x 或 C::x |
虛繼承必須在中間類(B/C)聲明時指定,而非在最終類(D)需要時才決定。若 B/C 未虛繼承,D 仍會有兩份 A。
這意味著:基類設計者必須預見未來可能的菱形共享需求。
ios_base (虛基類)
/ \
istream ostream
\ /
iostream
istream、ostream 虛繼承 ios_baseiostream 繼承兩者,僅有一份 ios_base 狀態(格式旗標、緩衝區等)| 代價 | 說明 |
|---|---|
| 記憶體開銷 | 每個虛繼承分支需額外指針/偏移量定位虛基類 |
| 存取開銷 | 虛基類成員存取需間接查找,比直接存取慢 |
| 構造複雜 | 最派生類強制初始化,順序規則特殊 |
| 設計耦合 | 中間類必須知曉虛基類角色,違反封裝 |
虛繼承(virtual public Base)通過共享虛基類子物件,解決菱形繼承的命名衝突與資料冗余。但引入間接存取開銷、構造規則複雜、設計時提前承諾等代價。iostream 是教科書級應用。實務中:極度克制使用,優先考慮組合與介面設計。