虛繼承

"對於繼承概念中的虛擬函式，請擬函式。"虛繼承 是面向物件程式設計中的一種技術，是指一個指定的基類，在繼承體系結構中，將其成員資料例項共享給也從這個基型別直接或間接派生的其它類。舉例來說：假如類"A"和類"B"各自從類"X"派生。但是如果類"A"與"B"各自虛繼承了類"X"，那麼"C"的物件就只包含一套類"X"的例項資料。對於這一概念典型實現的程式語言是C++。這一特性在多重繼承應用中非常有用，可以使得虛基類對於由它直接或間接派生的類來說，擁有一個共同的基類物件例項。避免由於帶有歧義的組合而產生的問題，實質上直接繼承了虛基類"X"。這一概念一般用於“繼承”在表現為一個整體，而非幾個部分的組合時。在C++中，基類可以透過使用關鍵字codice_1來宣告虛繼承關係。問題的產生.考慮下面的類的層次和關係。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 mammalclass Bat : public Mammal, public WingedAnimal {Bat bat;按照上面的定義，呼叫codice_2是有歧義的，因為在codice_3中有兩個codice_4基類，所以所有的codice_3物件都有兩個不同的codice_4基類的子物件。因此，嘗試直接引用codice_3物件的codice_4子物件會導致錯誤，因為該繼承是有歧義的：Bat b;Animal &amp;a = b; // error: which Animal subobject should a Bat cast into,// a Mammal::Animal or a WingedAnimal::Animal?要消除歧義，需要顯式的將codice_9轉換為每一個基類子物件：Bat b;Animal &amp;mammal = static_cast (b);Animal &amp;winged = static_cast (b);為了正確的呼叫codice_10，還需要相同的可以消歧義的語句：codice_11或codice_12.在這個例子中，我們可能並不需要codice_4被繼承兩次，我們只想建立一個模型來說明這層關係若將上面的關係以圖形方式表示看起來類似菱形，所以這一情況也被稱為菱形繼承。虛繼承可以解決這一問題。解決方法.我們可以按如下方式重新宣告上面的類：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 mammalclass Bat : public Mammal, public WingedAnimal {codice_31中的codice_4部分現在和codice_33中的codice_4部分是相同的了，這也就是說codice_3現在有且只有一個共享的codice_4部分，所以對於codice_37的呼叫就不再有歧義了。另外，直接將codice_3例項分派給codice_4例項的過程也不會產生歧義了，因為現在只存在一種可以轉換為codice_4的codice_3實體了。因為codice_17例項的起始地址和其codice_4部分的記憶體偏移量直到程式執行分配記憶體時才會明確，所以虛繼承應用給codice_17和codice_18建立了虛表。因此“Bat”包含codice_46, codice_17, codice_46, codice_18, codice_3, codice_4。這裡共有兩個虛表指標，其中最派生類的物件地址所指向的虛表指標，指向了最派生類的虛表；另一個虛表指標指向了codice_18的類的虛表。codice_4虛繼承而來。在上面的例子裡，一個分配給codice_17，另一個分配給codice_18。因此每個物件佔用的記憶體增加了兩個指標的大小，但卻解決了codice_4的歧義問題。所有codice_3類的物件都包含這兩個虛指標，但是每一個物件都包含唯一的codice_4物件。假設一個類codice_59宣告繼承了codice_17，那麼codice_59中的codice_17物件的虛指標和codice_3中的codice_17物件的虛指標是不同的，儘管他們佔用的記憶體空間大小是相同的。這是因為在記憶體中codice_17到codice_4的距離是相同的。虛表不同而實際上佔用的空間相同。虛基類的初始化.由於虛基類是多個派生類共享的基類，因此由誰來初始化虛基類必須明確。C++標準規定，由最派生類直接初始化虛基類。因此，對間接繼承了虛基類的類，也必須能直接訪問其虛繼承來的祖先類，也即應知道其虛繼承來的祖先類的地址偏移值。例如，常見的“菱形”虛繼承例子中，兩個派生類、一個最派生類的建構函式的初始化列表中都可以給出虛基類的初始化；但只由最派生類的建構函式實際執行虛基類的初始化。g++與虛繼承.g++編譯器生成的C++類例項，虛擬函式與"虛基類地址偏移值"共用一個虛表組成部分共用一個虛表，但各自使用的虛表部分依次相接、不相重疊。g++編譯下，一個類例項的虛指標指向該類虛表中的第一個虛擬函式的地址。如果該類沒有虛擬函式，因而該類自身虛表中沒有虛擬函式需要填入，但該類有虛繼承的祖先類，則仍然必須要訪問虛表中的虛基類地址偏移值。這種情況下，該類仍然需要有虛表，該類例項的虛指標指向類虛表中一個值為0的條目。該類其它的虛擬函式的地址依次填在虛表中第一個虛擬函式條目之後、虛指標到整個物件開始處的偏移值、虛基類地址偏移值。因此，如果一個類虛繼承了兩個類，那麼對於32位程式，虛繼承的左父類地址偏移值位於vptr-0x0c，虛繼承的右父類地址偏移值位於vptr-0x10.一個類的祖先類有複雜的虛繼承關係，則該類的各個虛基類偏移值在虛表中的儲存順序尊重自該類到祖先的深度優先遍歷次序。Microsoft Visual C++與虛繼承.Microsoft Visual C++與g++不同，把類的虛擬函式與虛基類地址偏移值分別放入了兩個虛表中，前者稱為虛擬函式表vftbl，後者稱虛基類表vbtbl。因此一個類例項可能有兩個虛指標分別指向類的虛擬函式表與虛基類表，這兩個虛指標分別稱為虛擬函式表指標vftbl與虛基類表指標vbtbl。當然，類例項也可以只有一個虛指標，或者沒有虛指標。虛指標總是放在類例項的資料成員之前，且虛擬函式表指標總是在虛基類表指標之前。因而，對於某個類例項來說，如果它有虛基類指標，那麼虛基類指標可能在類例項的0位元組偏移處，也可能在類例項的4位元組偏移處，這給類成員函式指標的實現帶來了很大麻煩。一個類的虛基類指標指向的虛基類表的首個條目，該條目的值是虛基類表指標所在的地址到該類的例項的記憶體首地址的偏移值。即codice_67。虛基類第2、第3、... 個條目依次為該類的最左虛繼承父類、次左虛繼承父類、...的記憶體地址相對於虛基類表指標自身地址，即 &amp;(obj.vbtbl)的偏移值。如果一個類同時有虛繼承的父類與祖父類，則虛祖父類放在虛父類前面。另外需要注意的是，類的虛擬函式表的第一項之前的項為最派生類例項的記憶體首地址到當前虛擬函式表指標的偏移值，即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類的預設建構函式不合法多型 (計算機科學)