分享

【C++基础之二十一】菱形继承和虚继承

 it程序猿修养 2014-12-30


偶尔e网事的原创作品,转载请标明:http://blog.csdn.net/jackystudio/article/details/17877219


菱形继承是多重继承中跑不掉的,Java拿掉了多重继承,辅之以接口。C++中虽然没有明确说明接口这种东西,但是只有纯虚函数的类可以看作Java中的接口。在多重继承中建议使用“接口”,来避免多重继承中可能出现的各种问题。


1.菱形继承

先看一下菱形继承长什么样。



B和C从A中继承,而D多重继承于B,C。那就意味着D中会有A中的两个拷贝。因为成员函数不体现在类的内存大小上,所以实际上可以看到的情况是D的内存分布中含有2组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 : public A  
  10. {  
  11. };  
  12.   
  13. class C : 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的大小为8。也就是d中有2个a成员变量。这样一来如果要使用a就蛋疼报错了。谁知道你要用哪一个?同样的访问成员函数也会出现“二义性”这个问题。

  1. d.a=10;//error C2385: 对“a”的访问不明确  
  2. d.printA();//error C2385: 对“printA”的访问不明确  

即使成员函数有不同的形参表也不行,如下。

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

当然你可以无耻地使用作用域来规避这个问题。但这并不是一个好主意。

  1. d.B::a=10;  
  2. d.C::printB(1);  


2.虚继承


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),析构的调用次序和构造调用次序相反。

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多