鑽石問題(菱形繼承問題) 和虛繼承


 

在C++中,什么叫做鑽石問題(也可以叫菱形繼承問題),怎么避免它?


下面的圖表可以用來解釋鑽石問題。






假設我們有類B和類C,它們都繼承了相同的類A。另外我們還有類D,類D通過多重繼承機制繼承了類B和類C。因為上述圖表的形狀類似於鑽石(或者菱形),因此這個問題被形象地稱為鑽石問題(菱形繼承問題)。現在,我們將上面的圖表翻譯成具體的代碼:


 

  1. /* 
  2. Animal類對應於圖表的類A 
  3. */  
  4.                                   
  5. class Animal { /* ... */ }; // 基類  
  6. {  
  7. int weight;  
  8.   
  9. public:  
  10.   
  11. int getWeight() { return weight;};  
  12.   
  13. };  
  14.   
  15. class Tiger : public Animal { /* ... */ };  
  16.   
  17. class Lion : public Animal { /* ... */ }      
  18.                           
  19. class Liger : public Tiger, public Lion { /* ... */ };    

在上面的代碼中,我們給出了一個具體的鑽石問題例子。Animal類對應於最頂層類(圖表中的A),Tiger和Lion分別對應於圖表的B和C,Liger類(獅虎獸,即老虎和獅子的雜交種)對應於D。

現在,問題是如果我們有這種繼承結構會出現什么樣的問題。

看看下面的代碼后再來回答問題吧。

 

  1. int main( )  
  2. {  
  3. Liger lg ;  
  4.   
  5. /*編譯錯誤,下面的代碼不會被任何C++編譯器通過 */  
  6.   
  7. int weight = lg.getWeight();    
  8. }  


在我們的繼承結構中,我們可以看出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類的子對象會被創建。看看下面的代碼:

  1. class Tiger : virtual public Animal { /* ... */ };  
  2.   
  3. class Lion : virtual public Animal { /* ... */ }  

你可以看出唯一的變化就是我們在類Tiger和類Lion的聲明中增加了"virtual"關鍵字。現在類Liger對象將會只有一個Animal子對象,下面的代碼編譯正常:
  1. int main( )  
  2. {  
  3. Liger lg ;  
  4.   
  5. /*既然我們已經在Tiger和Lion類的定義中聲明了"virtual"關鍵字,於是下面的代碼編譯OK */  
  6.   
  7. int weight = lg.getWeight();    
  8. }  


因為Java不支持多繼承,所以不會出現菱形繼承問題。但是Java可以通過接口間接實現多重繼承。
[java] view plain copy
  1. Class Mule implements Horse,Donkey  
  2. {  
  3.     /* Horse和Donkey是接口*/  
 

虛繼承

2.1.概念

這時候虛繼承就挺身而出,扛下搞定此問題的重擔了。虛繼承是一種機制,類通過虛繼承指出它希望共享虛基類的狀態。對給定的虛基類,無論該類在派生層次中作為虛基類出現多少次,只繼承一個共享的基類子對象,共享基類子對象稱為虛基類。虛基類用virtual聲明繼承關系就行了。這樣一來,D就只有A的一份拷貝。如下:

  1. class A  
  2. {  
  3. public:  
  4.     A():a(1){};  
  5.     void printA(){cout<<a<<endl;}  
  6.     int a;  
  7. };  
  8.   
  9. class B : virtual public A  
  10. {  
  11. };  
  12.   
  13. class C : virtual public A  
  14. {  
  15. };  
  16.   
  17. class D:  public B ,  public C  
  18. {  
  19. };  
  20.   
  21. int _tmain(int argc, _TCHAR* argv[])  
  22. {  
  23.     D d;  
  24.     cout<<sizeof(d);  
  25.     d.a=10;  
  26.     d.printA();  
  27. }  

輸出d的大小是12(包含了2個4字節的D類虛基指針和1個4字節的int型整數)。而a和printA()都可以正常訪問。最典型的應用就是iostream繼承於istream和ostream,而istream和ostream虛繼承於iOS

  1. class istream : virtual public ios{...};  
  2. class ostream : virtual public ios{...};  
  3. class iostream : public istream, public ostream{...};  

2.2.注意

(1)支持到基類的常規轉換。也就是說即使基類是虛基類,也照樣可以通過基類指針或引用來操縱派生類的對象。

(2)虛繼承只是解決了菱形繼承中派生類多個基類內存拷貝的問題,並沒有解決多重繼承的二義性問題。

(3)通常每個類只會初始化自己的直接基類,如果不按虛繼承處理,那么在菱形繼承中會出現基類被初始兩次的情況,在上例中也就是A→B→A→C→D。為了解決這個重復初始化的問題,虛繼承對初始化進行了特殊處理。在虛繼承中,由最底層派生類的構造函數初始化虛基類。體會一下下面這個例子:


輸出構造和析構順序:

  1. C()  
  2. E()  
  3. A()  
  4. B()  
  5. D()  
  6. F()  
  7. ~F()  
  8. ~D()  
  9. ~B()  
  10. ~A()  
  11. ~E()  
  12. ~C()  

可以看出,首先按聲明順序檢查直接基類(包括其子樹),看是否有虛基類,先初始化虛基類(例中首先初始化C和E)。一旦虛基類構造完畢,就按聲明順序調用非虛基類的構造函數(例中ABDF),析構的調用次序和構造調用次序相反
 
 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM