虛繼承

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

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

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

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

這一概念一般用於 “繼承” 在表現為一個整體，而非幾個部分的組合時。在 C++ 中，基類可以透過使用關鍵字 virtual 來宣告虛繼承關係。

問題的產生

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

class Animal {

public:

virtual void eat ;};

class Mammal : public Animal {

public:

virtual void breathe ;};

class WingedAnimal : public Animal {

public:

virtual void flap ;};

// A bat is a winged mammal

class Bat : public Mammal, public WingedAnimal {

};

Bat bat;

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

Bat b;

Animal &a = b; //error: which Animal subobject should a Bat cast into,

//a Mammal::Animal or a WingedAnimal::Animal?

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

Bat b;

Animal &mammal = static_cast (b);

Animal &winged = static_cast (b);

為了正確的呼叫 eat ，還需要相同的可以消歧義的語句：staticcast (bat).eat 或 staticcast (bat).eat .

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

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

解決方法

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

class Animal {

public:

virtual void eat ;};

// Two classes virtually inheriting Animal:

class Mammal : public virtual Animal {

public:

virtual void breathe ;};

class WingedAnimal : public virtual Animal {

public:

virtual void flap ;};

// A bat is still a winged mammal

class Bat : public Mammal, public WingedAnimal {

};

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++ 中可以用虛繼承來實現：

template class MakeFinally {

private:

MakeFinally ;// 只有 MakeFinally 的友類才可以構造 MakeFinally

~MakeFinally ;

friend T;

};

class MyClass:public virtual MakeFinally ;//MyClass 是不可派生類

// 由於虛繼承，所以 D 要直接負責構造 MakeFinally 類，從而導致編譯報錯，所以 D 作為派生類是不合法的。

class D: public MyClass ;

// 另外，如果 D 類沒有例項化物件，即沒有被使用，實際上 D 類是被編譯器忽略掉而不報錯

int main

{

MyClass var1;

// D var2; // 這一行編譯將導致錯誤，因為 D 類的預設建構函式不合法

}