在C++中,什么叫做鑽石問題(也可以叫菱形繼承問題),怎么避免它?
下面的圖表可以用來解釋鑽石問題。
假設我們有類B和類C,它們都繼承了相同的類A。另外我們還有類D,類D通過多重繼承機制繼承了類B和類C。因為上述圖表的形狀類似於鑽石(或者菱形),因此這個問題被形象地稱為鑽石問題(菱形繼承問題)。現在,我們將上面的圖表翻譯成具體的代碼:
- /*
- Animal類對應於圖表的類A
- */
- class Animal { /* ... */ }; // 基類
- {
- int weight;
- public:
- int getWeight() { return weight;};
- };
- class Tiger : public Animal { /* ... */ };
- class Lion : public Animal { /* ... */ }
- class Liger : public Tiger, public Lion { /* ... */ };
在上面的代碼中,我們給出了一個具體的鑽石問題例子。Animal類對應於最頂層類(圖表中的A),Tiger和Lion分別對應於圖表的B和C,Liger類(獅虎獸,即老虎和獅子的雜交種)對應於D。
現在,問題是如果我們有這種繼承結構會出現什么樣的問題。
看看下面的代碼后再來回答問題吧。
- int main( )
- {
- Liger lg ;
- /*編譯錯誤,下面的代碼不會被任何C++編譯器通過 */
- int weight = lg.getWeight();
- }
在我們的繼承結構中,我們可以看出Tiger和Lion類都繼承自Animal基類。所以問題是:因為Liger多重繼承了Tiger和Lion類,因此Liger類會有兩份Animal類的成員(數據和方法),Liger對象"lg"會包含Animal基類的兩個子對象。
所以,你會問Liger對象有兩個Animal基類的子對象會出現什么問題?再看看上面的代碼-調用"lg.getWeight()"將會導致一個編譯錯誤。這是因為編譯器並不知道是調用Tiger類的getWeight()還是調用Lion類的getWeight()。所以,調用getWeight方法是不明確的,因此不能通過編譯。
鑽石問題的解決方案:
我們給出了鑽石問題的解釋,但是現在我們要給出一個鑽石問題的解決方案。如果Lion類和Tiger類在分別繼承Animal類時都用virtual來標注,對於每一個Liger對象,C++會保證只有一個Animal類的子對象會被創建。看看下面的代碼:
- class Tiger : virtual public Animal { /* ... */ };
- class Lion : virtual public Animal { /* ... */ }
你可以看出唯一的變化就是我們在類Tiger和類Lion的聲明中增加了"virtual"關鍵字。現在類Liger對象將會只有一個Animal子對象,下面的代碼編譯正常:
- int main( )
- {
- Liger lg ;
- /*既然我們已經在Tiger和Lion類的定義中聲明了"virtual"關鍵字,於是下面的代碼編譯OK */
- int weight = lg.getWeight();
- }
- Class Mule implements Horse,Donkey
- {
- /* Horse和Donkey是接口*/
- }
虛繼承
2.1.概念
這時候虛繼承就挺身而出,扛下搞定此問題的重擔了。虛繼承是一種機制,類通過虛繼承指出它希望共享虛基類的狀態。對給定的虛基類,無論該類在派生層次中作為虛基類出現多少次,只繼承一個共享的基類子對象,共享基類子對象稱為虛基類。虛基類用virtual聲明繼承關系就行了。這樣一來,D就只有A的一份拷貝。如下:
- class A
- {
- public:
- A():a(1){};
- void printA(){cout<<a<<endl;}
- int a;
- };
- class B : virtual public A
- {
- };
- class C : virtual public A
- {
- };
- class D: public B , public C
- {
- };
- int _tmain(int argc, _TCHAR* argv[])
- {
- D d;
- cout<<sizeof(d);
- d.a=10;
- d.printA();
- }
輸出d的大小是12(包含了2個4字節的D類虛基指針和1個4字節的int型整數)。而a和printA()都可以正常訪問。最典型的應用就是iostream繼承於istream和ostream,而istream和ostream虛繼承於iOS。
- class istream : virtual public ios{...};
- class ostream : virtual public ios{...};
- class iostream : public istream, public ostream{...};
2.2.注意
(1)支持到基類的常規轉換。也就是說即使基類是虛基類,也照樣可以通過基類指針或引用來操縱派生類的對象。
(2)虛繼承只是解決了菱形繼承中派生類多個基類內存拷貝的問題,並沒有解決多重繼承的二義性問題。
(3)通常每個類只會初始化自己的直接基類,如果不按虛繼承處理,那么在菱形繼承中會出現基類被初始兩次的情況,在上例中也就是A→B→A→C→D。為了解決這個重復初始化的問題,虛繼承對初始化進行了特殊處理。在虛繼承中,由最底層派生類的構造函數初始化虛基類。體會一下下面這個例子:
輸出構造和析構順序:
- C()
- E()
- A()
- B()
- D()
- F()
- ~F()
- ~D()
- ~B()
- ~A()
- ~E()
- ~C()
可以看出,首先按聲明順序檢查直接基類(包括其子樹),看是否有虛基類,先初始化虛基類(例中首先初始化C和E)。一旦虛基類構造完畢,就按聲明順序調用非虛基類的構造函數(例中ABDF),析構的調用次序和構造調用次序相反