虛繼承
- 對於繼承概念中的虛擬函式,請參閱虛擬函式。
虛繼承 是物件導向程式設計中的一種技術,是指一個指定的基礎類別,在繼承體系結構中,將其成員數據實例共用給也從這個基本類型直接或間接衍生的其它類。
舉例來說:假如類A和類B各自從類X衍生(非虛繼承且假設類X包含一些數據成員),且類C同時多繼承自類A和B,那麼C的對象就會擁有兩套X的實例數據(可分別獨立訪問,一般要用適當的消歧義限定符)。但是如果類A與B各自虛繼承了類X,那麼C的對象就只包含一套類X的實例數據。對於這一概念典型實現的程式語言是C++。
這一特性在多重繼承應用中非常有用,可以使得虛基礎類別對於由它直接或間接衍生的類來說,擁有一個共同的基礎類別對象實例。避免由於帶有歧義的組合而產生的問題(如「菱形繼承問題」)。其原理是,間接衍生類別(C)穿透了其父類別(上面例子中的A與B),實質上直接繼承了虛基礎類別X。[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、... 個條目依次為該類的最左虛繼承父類別、次左虛繼承父類別、...的主記憶體地址相對於虛基礎類別表指標自身地址,即 &(obj.vbtbl)的偏移值。
如果一個類同時有虛繼承的父類別與祖父類別,則虛祖父類別放在虛父類別前面。
另外需要注意的是,類的虛擬函式表的第一項之前的項(即*(obj.vftbl-1))為最衍生類別實例的主記憶體首地址到當前虛擬函式表指標的偏移值,即mostDerivedObj-obj.vftbl。衍生類別的虛擬函式覆蓋基礎類別的虛擬函式時,在基礎類別的虛擬函式表的對應條目寫入的是一個「樁」(thunk)函數的入口地址,以調整this指標指向到衍生類別實例的地址,再呼叫衍生類別的對應的虛擬函式。例如:this -= offset; call DerivedClass:virtFunc;
虛繼承的應用:不可衍生的finally類
編輯一個類如果不希望被繼承,類似於Java中的具有finally性質的類,這在C++中可以用虛繼承來實現:
template<typename T> class MakeFinally{
private:
MakeFinally(){};//只有MakeFinally的友类才可以构造MakeFinally
~MakeFinally(){};
friend T;
};
class MyClass:public virtual MakeFinally<MyClass>{};//MyClass是不可派生类
//由于虚继承,所以D要直接负责构造MakeFinally类,从而导致编译报错,所以D作为派生类是不合法的。
class D: public MyClass{};
//另外,如果D类没有实例化对象,即没有被使用,实际上D类是被编译器忽略掉而不报错
int main()
{
MyClass var1;
// D var2; //这一行编译将导致错误,因为D类的默认构造函数不合法
}
參見
編輯參照
編輯- ^ Andrei Milea. Solving the Diamond Problem with Virtual Inheritance. http://www.cprogramming.com/: Cprogramming.com. [2010-03-08]. (原始內容存檔於2021-03-04).
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]. (原始內容存檔於2010-01-10).
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.