• 钻石问题(菱形继承问题) 和虚继承


    在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可以通过接口间接实现多重继承。
    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),析构的调用次序和构造调用次序相反
     
     
  • 相关阅读:
    3.16 使用Zookeeper对HDFS HA配置自动故障转移及测试
    4、html的body内标签之input系列
    Gym
    Gym
    Gym
    Gym
    Gym
    Big Event in HDU HDU1171 (多重背包)
    Coins POJ 1742 多重背包部分和
    HDU 1059 Dividing 多重背包
  • 原文地址:https://www.cnblogs.com/sddai/p/6516668.html
Copyright © 2020-2023  润新知