虛繼承
轉自:http://zh.wikipedia.org/wiki/%E8%99%9A%E7%BB%A7%E6%89%BF
虛繼承[編輯]
- 對於繼承概念中的虛擬函式,請參閱虛擬函式。
虛繼承 是物件導向程式設計中的一種技術。當一個指定的基類,從繼承面上來講,在宣告時將其成員例項共享給也從這個基類繼承來的其它類。舉例來說:假如類A和類B是由類X繼承而來(非虛繼承且假設類X包含一些成員),且類C同時繼承了類A和B,那麼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
);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、... 個條目依次為該類虛繼承的最左父類、次左父類、...的記憶體地址相對於虛基類表指標自身地址(即
&vbtbl)的偏移值。
如果一個類同時有虛繼承的父類與祖父類,則虛祖父類放在虛父類前面。
引用[編輯]
- ^ Andrei Milea. Solving the Diamond Problem with Virtual Inheritance. http://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:"
- ^ 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."
相關文章
- 菱形繼承,虛繼承繼承
- C++虛繼承的概念C++繼承
- c++ 虛繼承詳解C++繼承
- 菱形繼承與虛基類繼承
- odoo 繼承(owl繼承、web繼承、view繼承)Odoo繼承WebView
- 多重繼承及虛繼承中物件記憶體的分佈繼承物件記憶體
- 虛擬繼承的意義 (轉)繼承
- C++ 虛擬函式和虛繼承淺析C++函式繼承
- 原型,繼承——原型繼承原型繼承
- 多繼承 與 多重繼承繼承
- C++繼承詳解:共有(public)繼承,私有(private)繼承,保護(protected)繼承C++繼承
- C++繼承二之虛擬函式C++繼承函式
- C++ 繼承、多型、虛擬函式C++繼承多型函式
- C++中的虛繼承的構造C++繼承
- 三種繼承的方法:public 繼承/private繼承/protected繼承詳解及區別繼承
- Javascript繼承4:潔淨的繼承者—-原型式繼承JavaScript繼承原型
- Javascript繼承2:建立即繼承—-建構函式繼承JavaScript繼承函式
- 繼承繼承
- C++知識點隨筆(五):虛繼承C++繼承
- C++ 虛繼承 物件記憶體佈局C++繼承物件記憶體
- C++虛繼承原理與類佈局分析C++繼承
- C++繼承一之公有繼承C++繼承
- JS原型繼承和類式繼承JS原型繼承
- C++中公有繼承、保護繼承、私有繼承的區別C++繼承
- 公有繼承、私有繼承和保護繼承之間的對比繼承
- day23:單繼承&多繼承&菱形繼承&__init__魔術方法繼承
- 類的繼承_子類繼承父類繼承
- C#虛基類繼承與介面的區別C#繼承
- JavaScript繼承JavaScript繼承
- JavaScript 繼承JavaScript繼承
- 多繼承繼承
- Java繼承Java繼承
- javascript:繼承JavaScript繼承
- swift繼承Swift繼承
- js繼承JS繼承
- 10 #### 繼承繼承
- C++單繼承、多繼承情況下的虛擬函式表分析C++繼承函式
- python 基礎之繼承、重寫、多繼承Python繼承