虚拟继承

: _對於繼承概念中的虛擬函式，請參閱虛擬函式。_

**虛繼承** 是物件導向程式設計中的一種技術，是指一個指定的基礎類別，在繼承體系結構中，將其成員資料實例共享給也從這個基本類型直接或間接衍生的其它類。

舉例來說：假如類 _A_ 和類 _B_ 各自從類 _X_ 衍生（非虛繼承且假設類 X 包含一些資料成員），且類 _C_ 同時多繼承自類 _A_ 和 _B_，那麼 _C_ 的物件就會擁有兩套 _X_ 的實例資料（可分別獨立存取，一般要用適當的消歧義限定符）。但是如果類 _A_ 與 _B_ 各自虛繼承了類 _X_，那麼 _C_ 的物件就只包含一套類 _X_ 的實例資料。對於這一概念典型實現的程式語言是 C++。

這一特性在多重繼承應用中非常有用，可以使得虛基礎類別對於由它直接或間接衍生的類來說，擁有一個共同的基礎類別物件實例。避免由於帶有歧義的組合而產生的問題（如「菱形繼承問題」）。其原理是，間接衍生類別（_C_）穿透了其父類別（上面例子中的 _A_ 與 _B_），實質上直接繼承了虛基礎類別 _X_。

這一概念一般用於「繼承」在表現為一個整體，而非幾個部分的組合時。在 C++ 中，基礎類別可以通過使用關鍵字 `virtual` 來聲明虛繼承關係。

## 問題的產生

考慮下面的類的層次和關係。

按照上面的定義，呼叫 `bat.eat()` 是有歧義的，因為在 `Bat` 中有兩個 `Animal` 基礎類別（間接的），所以所有的 `Bat` 物件都有兩個不同的 `Animal` 基礎類別的子物件。因此，嘗試直接參照 `Bat` 物件的 `Animal` 子物件會導致錯誤，因為該繼承是有歧義的：

要消除歧義，需要顯式的將 `bat` 轉換為每一個基礎類別子物件：

 
為了正確的呼叫 `eat()`，還需要相同的可以消歧義的語句：`static_cast<Mammal&>(bat).eat()` 或 `static_cast<WingedAnimal&>(bat).eat()`.

在這個例子中，我們可能並不需要 `Animal` 被繼承兩次，我們只想建立一個模型來說明這層關係（`Bat` 屬於 `Animal`）；`Bat` 是 `Mammal` 也是 `WingedAnimal` 並不意味著它是兩個 `Animal`：`Animal` 定義的功能由 `Bat` 來實現（上面「_是_」的屬性實際上是「_實現需求_」的含義），且一個 `Bat` 只實現一次。「只_是_一個」的真正含義是 `Bat` 只有一種實現 `eat()` 的方法，無論是從 `Mammal` 的角度還是從 `WingedAnimal` 的角度來看。（在上面的第一段代碼範例中我們看到 `eat()` 並沒有在 `Mammal` 或 `WingedAnimal` 中被多載，所以這兩個 `Animal` 子物件實際上是以相同的方式運作，但這只是一個不完善的例子，從 C++ 的角度來看二者之間正好沒有實際的區別。）

若將上面的關係以圖形方式表示看起來類似菱形，所以這一情況也被稱為**菱形繼承**。虛繼承可以解決這一問題。

## 解決方法

我們可以按如下方式重新聲明上面的類：

`Bat::WingedAnimal` 中的 `Animal` 部分現在和 `Bat::Mammal` 中的 `Animal` 部分是相同的了，這也就是說 `Bat` 現在有且只有一個共享的 `Animal` 部分，所以對於 `Bat::eat()` 的呼叫就不再有歧義了。另外，直接將 `Bat` 實例分派給 `Animal` 實例的過程也不會產生歧義了，因為現在只存在一種可以轉換為 `Animal` 的 `Bat` 實體了。

因為 `Mammal` 實例的起始位址和其 `Animal` 部分的記憶體偏移量直到程式執行分配記憶體時才會明確，所以虛繼承應用給 `Mammal` 和 `WingedAnimal` 建立了虛表（vtable）指標（「vpointer」）。因此「Bat」包含 `vpointer`, `Mammal`, `vpointer`, `WingedAnimal`, `Bat`, `Animal`。這裡共有兩個虛表指標，其中最衍生類別的物件位址所指向的虛表指標，指向了最衍生類別的虛表；另一個虛表指標指向了 `WingedAnimal` 的類的虛表。`Animal` 虛繼承而來。在上面的例子裡，一個分配給 `Mammal`，另一個分配給 `WingedAnimal`。因此每個物件占用的記憶體增加了兩個指標的大小，但卻解決了 `Animal` 的歧義問題。所有 `Bat` 類的物件都包含這兩個虛指標，但是每一個物件都包含唯一的 `Animal` 物件。假設一個類 `Squirrel` 聲明繼承了 `Mammal`，那麼 `Squirrel` 中的 `Mammal` 物件的虛指標和 `Bat` 中的 `Mammal` 物件的虛指標是不同的，儘管他們占用的記憶體空間大小是相同的。這是因為在記憶體中 `Mammal` 到 `Animal` 的距離是相同的。虛表不同而實際上占用的空間相同。

## 虛基礎類別的初始化

由於虛基礎類別是多個衍生類別共享的基礎類別，因此由誰來初始化虛基礎類別必須明確。C++ 標準規定，由最衍生類別直接初始化虛基礎類別。因此，對間接繼承了虛基礎類別的類，也必須能直接存取其虛繼承來的祖先類，也即應知道其虛繼承來的祖先類的位址偏移值。

例如，常見的「菱形」虛繼承例子中，兩個衍生類別、一個最衍生類別的建構函式的初始化列表中都可以給出虛基礎類別的初始化；但只由最衍生類別的建構函式實際執行虛基礎類別的初始化。

## g++ 與虛繼承

g++ 編譯器生成的 C++ 類別實例，虛擬函式與_虛基礎類別位址偏移值_共享一個虛表（vtable）。類別實例的開始處即為指向所屬類的**虛指標**（vptr）。實際上，一個類與它的若干祖先類（父類別、祖父類別、...）組成部分共享一個虛表，但各自使用的虛表部分依次相接、不相重疊。

g++ 編譯下，一個類別實例的虛指標指向該類虛表中的第一個虛擬函式的位址。如果該類沒有虛擬函式（或者虛擬函式都寫入了祖先類的虛表，覆蓋了祖先類的對應虛擬函式），因而該類自身虛表中沒有虛擬函式需要填入，但該類有虛繼承的祖先類，則仍然必須要存取虛表中的虛基礎類別位址偏移值。這種情況下，該類仍然需要有虛表，該類別實例的虛指標指向類虛表中一個值為 0 的條目。

該類其它的虛擬函式的位址依次填在虛表中第一個虛擬函式條目之後（記憶體位址自低向高方向）。虛表中第一個虛擬函式條目之前（記憶體位址自高向低方向），依次填入了 typeinfo（用於 RTTI）、虛指標到整個物件開始處的偏移值、虛基礎類別位址偏移值。因此，如果一個類虛繼承了兩個類，那麼對於 32 位元程式，虛繼承的左父類別位址偏移值位於 vptr-0x0c，虛繼承的右父類別位址偏移值位於 vptr-0x10.

一個類的祖先類有複雜的虛繼承關係，則該類的各個虛基礎類別偏移值在虛表中的儲存順序尊重自該類到祖先的深度優先遍歷次序。

## Microsoft Visual C++ 與虛繼承

Microsoft Visual C++ 與 g++ 不同，把類的虛擬函式與虛基礎類別位址偏移值分別放入了兩個虛表中，前者稱為虛擬函式表 vftbl，後者稱虛基礎類別表 vbtbl。因此一個類別實例可能有兩個虛指標分別指向類的虛擬函式表與虛基礎類別表，這兩個虛指標分別稱為虛擬函式表指標 vftbl 與虛基礎類別表指標 vbtbl。當然，類別實例也可以只有一個虛指標，或者沒有虛指標。虛指標總是放在類別實例的資料成員之前，且虛擬函式表指標總是在虛基礎類別表指標之前。因而，對於某個類別實例來說，如果它有虛基礎類別指標，那麼虛基礎類別指標可能在類別實例的 0 位元組偏移處，也可能在類別實例的 4 位元組偏移處（對於 32 位元程式來說），這給類別成員函式指標的實現帶來了很大麻煩。

一個類的虛基礎類別指標指向的虛基礎類別表的首個條目，該條目的值是虛基礎類別表指標所在的位址到該類別的實例的記憶體首位址的偏移值。即 `&(obj.vbtbl) - &obj`。虛基礎類別第 2、第 3、... 個條目依次為該類的最左虛繼承父類別、次左虛繼承父類別、... 的記憶體位址相對於虛基礎類別表指標自身位址，即 &(obj.vbtbl) 的偏移值。

如果一個類同時有虛繼承的父類別與祖父類別，則虛祖父類別放在虛父類別前面。

另外需要注意的是，類的虛擬函式表的第一項之前的項（即 \*(obj.vftbl-1)）為最衍生類別實例的記憶體首位址到當前虛擬函式表指標的偏移值，即 mostDerivedObj-obj.vftbl。衍生類別的虛擬函式覆蓋基礎類別的虛擬函式時，在基礎類別的虛擬函式表的對應條目寫入的是一個「樁」(thunk) 函式的入口位址，以調整 this 指標指向到衍生類別實例的位址，再呼叫衍生類別的對應的虛擬函式。例如：this -= offset; call DerivedClass:virtFunc;

## 虛繼承的應用：不可衍生的 finally 類

一個類如果不希望被繼承，類似於 Java 中的具有 finally 性質的類，這在 C++ 中可以用虛繼承來實現：

## 參見

多型 (電腦科學)

## 參照