虛繼承

pamxy發表於2013-09-10

轉自:http://zh.wikipedia.org/wiki/%E8%99%9A%E7%BB%A7%E6%89%BF

虛繼承[編輯]

維基百科,自由的百科全書
對於繼承概念中的虛擬函式,請參閱虛擬函式

虛繼承 是物件導向程式設計中的一種技術。當一個指定的基類,從繼承面上來講,在宣告時將其成員例項共享給也從這個基類繼承來的其它類。舉例來說:假如類A和類B是由類X繼承而來(非虛繼承且假設類X包含一些成員),且類C同時繼承了類AB,那麼C就會擁有兩套和X相關的成員(可分別獨立訪問,一般要用適當的消歧義修飾符)。但是如果類A虛繼承自類X,那麼C將會只包含一組類X的成員資料。對於這一概念應用最好的程式語言是C++

這一特性在多重繼承應用中非常有用,可以使得虛基類對於它所繼承的類和所有由它繼承而來的類來說變成一個普通的子物件。還可以用於避免由於帶有歧義的組合而產生的問題(如“平行四邊形問題”)。其原理是通過說明使用了哪一個父類來消除歧義,具體來講,虛類(V)穿透了其父類(相當於上面例子中的B),相當於它就是B的直接基類而不是通過間接繼承的它真正的基類(A)。[1][2]


這一概念一般用於“繼承”在表現為一個整體,而非幾個部分的組合時。在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<Mammal&> (b); 
Animal &winged = static_cast<WingedAnimal&> (b);

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

在這個例子中,我們可能並不需要Animal被繼承兩次,我們只想建立一個模型來說明這層關係(Bat 屬於 Animal);BatMammal也是WingedAnimal並不意味著它是兩個AnimalAnimal定義的功能由Bat來實現(上面“”的屬性實際上是“實現需求”的含義),且一個Bat只實現一次。“只一個”的真正含義是Bat只有一種實現eat()的方法,無論是從Mammal的角度還是從WingedAnimal的角度來看。(在上面的第一段程式碼示例中我們看到eat()並沒有在MammalWingedAnimal中被過載,所以這兩個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例項的過程也不會產生歧義了,因為現在只存在一個可以轉換為AnimalBat實體了。

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

虛基類的初始化[編輯]

由於虛基類是多個派生類共享的基類,因此由誰來初始化虛基類必須明確。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、... 個條目依次為該類虛繼承的最左父類、次左父類、...的記憶體地址相對於虛基類表指標自身地址(即 &vbtbl)的偏移值。

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

引用[編輯]

  1. ^ Andrei Milea. Solving the Diamond Problem with Virtual Inheritancehttp://www.cprogramming.com/: Cprogramming.com. [2010-03-08]. "One of the problems that arises due to multiple inheritance is the diamond problem. A classical illustration of this is given by Bjarne Stroustrup (the creator of C++) in the following example:"
  2. ^ Ralph McArdell. C++/What is virtual inheritance?http://en.allexperts.com/: All Experts. 2004-02-14 [2010-03-08]. "This is something you find may be required if you are using multiple inheritance. In that case it is possible for a class to be derived from other classes which have the same base class. In such cases, without virtual inheritance, your objects will contain more than one subobject of the base type the base classes share. Whether this is what is the required effect depends on the circumstances. If it is not then you can use virtual inheritance by specifying virtual base classes for those base types for which a whole object should only contain one such base class subobject."

相關文章