C++常见面试题(五)——菱形继承和虚继承

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


1.菱形继承

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

B和C从A中继承,而D多重继承于B,C。那就意味着D中会有A中的两个拷贝。因为成员函数不体现在类的内存大小上,所以实际上可以看到的情况是D的内存分布中含有2组A的成员变量。如下代码:   

class A
    {
    public:
        A():a(1){};
        void printA(){cout<<a<<endl;}
        int a;
    };
     
    class B : public A
    {
    };
     
    class C : public A
    {
    };
     
    class D:  public B ,  public C
    {
    };
     
    int _tmain(int argc, _TCHAR* argv[])
    {
        D d;
        cout<<sizeof(d);
    }

输出d的大小为8。也就是d中有2个a成员变量。这样一来如果要使用a就蛋疼报错了。谁知道你要用哪一个?同样的访问成员函数也会出现“二义性”这个问题。      

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

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

 class A
    {
    public:
        A():a(1){};
        void printA(){cout<<a<<endl;}
        int a;
    };
     
    class B : public A
    {
    public:
        void printB(){cout<<"from B"<<endl;}
    };
     
    class C : public A
    {
    public:
        void printB(int v){cout<<"from C"<<endl;}
    };
     
    class D:  public B ,  public C
    {
    };
     
    int _tmain(int argc, _TCHAR* argv[])
    {
        D d;
        cout<<sizeof(d);
        d.printB();//这里编译会出现错误
    }


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

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


2.虚继承


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),析构的调用次序和构造调用次序相反。
————————————————
版权声明:本文为CSDN博主「偶尔e网事」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/jackystudio/article/details/17877219

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值