系列汇总讲解,请移步:
C++语法|虚函数与多态详细讲解系列(包含多重继承内容)
多重继承很容易导致菱形继承的问题:
这里有一个很明显的问题,我们的D不仅从基类B中继承到了ma,还从基C中也继承到了ma。
这在软件设计过程中肯定是有问题的。
这就是所谓的菱形继承问题。
这种设计模式也有很明显的问题。
菱形继承的案例
构造间接基类A
class A {
public:
A(int data) : ma(data) { cout << "A()" << endl;}
~A() { cout << "~A()" << endl; }
protected:
int ma;
};
构造B、C
class B: public A {
public:
B(int data) : A(data), mb(data) { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
protected:
int mb;
};
class C: public A {
public:
C(int data) : A(data), mc(data) { cout << "C()" << endl; }
~C() { cout << "~C()" << endl; }
protected:
int mc;
};
构造,菱形继承中的D类:
class D: public B, public C {
public:
D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
~D() { cout << "~D()" << endl; }
protected:
int md;
};
main函数:
int main () {
D d(10);
return 0;
}
打印结果如下:
菱形继承的内存布局
我们一起探究一下D的内存布局。
首先他从B继承来,B有基类A::ma,然后是自己的变量mb;
然后他从C继承来,B也由基类A而来,有A::ma,然后是自己的变量mc;
最后是自己的变量md。
很显然D有20个字节。我们对比刚才的打印结果进行分析:
- 首先是A的构造
- 然后是B的构造
- 第三个又是A的构造
- 然后是C的构造
- 最后是D的构造
- 然后析构和构造的顺序相反进行构造。
很明显的问题就是,在内存占用上有很大的浪费,根本就不用存这么多份,这是软件设计上的问题,应该要杜绝。
那么我们应该如何解决呢?也就是上节课谈到的虚继承
使用虚继承解决菱形问题
所有从A继承的地方都采用虚继承,那么这样一来,A就称为虚基类了。
那么我们再来分析一下内存布局:
-
B从A虚继承而来,说明A成为虚基类了,虚基类的内存占用应该搬到派生类的最后面,然后在原位置补上一个vbptr(指向的是):
-
那么C也是从A虚继承而来,也要搬到最后面,不过搬运的过程中发现,最后面已经有一份虚基类的数据了,所以我们这一波虚基类数据直接不要了,不过我们还是要补一份vbptr:
我们可以发现,此时D的内存分布,只有一份A::ma数据了。
综上所述,虚继承就是用来解决菱形继承和半圆形继承中派生类存在多份间接基类的成员属性的问题。
在派生类中要初始化间接基类
从作用域的角度来说,现在A::ma的初始化必须由C来亲自完成(在A没有默认构造的情况下)。
class C: virtual public A {
public:
C(int data) : A(data), mc(data) { cout << "C()" << endl; }
~C() { cout << "~C()" << endl; }
protected:
int mc;
};
最终的打印结果如下:
A()
B()
C()
D()
~D()
~C()
~B()
~A()
顺利解决问题,很舒服。
总结:
多重继承的好处:
可以做更多代码的复用
但是多重继承的一个明显问题:
菱形继承、半圆形继承,这会导致派生类有多份间接基类的数据,这是程序设计的问题,解决方法就是从间接基类A继承的地方都改成虚继承。