什么是多重继承
多重继承,很好理解,一个派生类
如果只继承一个基类
,称作单继承;
一个派生类
如果继承了多个基类
,称作多继承。
如图所示:
多重继承的优点
这个很好理解:
多重继承可以做更多的代码复用!
派生类通过多重继承,可以得到多个基类
的数据和方法,更大程度的实现了代码复用。
关于菱形继承的问题
凡事有利也有弊,对于多继承而言,也有自己的缺点。
我们先通过了解菱形继承来探究多重继承的缺点:
菱形继承是多继承
的一种情况,继承方式如图所示:
从图中我们可以看到:
类B
和类C
从类A
单继承而来;
而类D
从类B
和类C
多继承而来。
那么这样继承会产生什么问题呢?
我们来看代码:
class A
{
public:
A(int data) :ma(data) { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
protected:
int ma;
};
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;
};
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;
};
int main()
{
D d(10);
return 0;
}
通过代码,我们将上述菱形继承完成了,接下来运行代码:
通过运行结果,我们发现了问题:
对于基类A
而言,构造了两次,析构了两次!
并且,通过分析各个派生类的内存布局我们可以看到:
对于派生类D
来说,间接继承的基类A
中的数据成员ma
重复了!
这对资源来说是一种浪费与消耗。
(如果多继承的数量增加,那么派生类中重复的数据也会增加!)
其他多重继承的情况
除了菱形继承
外,还有其他多重继承的情况,也会出现相同的问题:
比如说图中呈现的:半圆形继承
。
如何解决多重继承的问题
通过分析我们知道了,多重继承的主要问题是,通过多重继承,有可能得到重复
的基类数据,并且可能重复的构造和析构同一个基类对象。
那么如何能够避免重复现象的产生呢?
答案就是:=》虚基类。
什么是虚基类
要理解虚基类,我们首先需要认识virtual
关键字的使用场景:
- 修饰
成员方法
时:产生虚函数; - 修饰
继承方式
时:产生虚基类。
对于被虚继承的类,称作虚基类。
比如说:
class A
{
XXXXXX;
};
class B : virtual public A
{
XXXXXX;
};
对于这个示例而言,B虚继承
了A,所以把A称作虚基类。
虚基类如何解决问题
那么虚基类如何解决上述多重继承产生的重复问题呢?
我们来看代码:
class A
{
public:
A(int data) :ma(data) { cout << "A()" << endl; }
~A() { cout << "~A()" << endl; }
protected:
int ma;
};
class B :virtual public A
{
public:
B(int data) :A(data), mb(data) { cout << "B()" << endl; }
~B() { cout << "~B()" << endl; }
protected:
int mb;
};
class C :virtual public A
{
public:
C(int data) :A(data), mc(data) { cout << "C()" << endl; }
~C() { cout << "~C()" << endl; }
protected:
int mc;
};
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;
};
我们做出修改:
将B
和C
的继承方式都改为虚继承
;接下来继续运行代码:
此时会报错:
提示说:"A::A" : 没有合适的默认构造函数可用
;
为什么会这样呢?
我们可以这么理解:
刚开始B
和C
单继承A
的时候,实例化对象时,会首先调用基类的构造函数,也就是A
的构造函数,到了D
,由于多继承了B
和C
,所以在实例化D
的对象时,会首先调用B
和C
的构造函数,然后调用自己(D)的。
但是这样会出现A
重复构造的问题,所以,采用虚继承,把有关重复的基类A
改为虚基类
,这样的话,对于A构造的任务就落到了最终派生类D
的头上,但是我们的代码中,对于D的构造函数:D(int data) : B(data), C(data), md(data) { cout << "D()" << endl; }
并没有对A进行构造。
所以会报错。
那么我们就给D
的构造函数,调用A
的构造函数:
D(int data) :A(data), B(data), C(data), md(data) { cout << "D()" << endl; }
这一次再运行:
我们会发现,问题解决了。
查看虚基类的内存布局
我们上面只是介绍了可以通过虚基类
的方法来解决多重继承产生的问题,
但是并不知道虚基类
具体是如何做的,为什么使用它就能解决上述问题。
为了更好地理解虚基类
的工作原理,我们可以通过工具来查看它的内存布局:
通过VS
的工具
=> 命令行
=> 开发者命令提示(C)
:
接下来切换到当前源文件
的目录下:
(注意:当前源文件的目录可以通过右键当前源文件
=>打开所在的文件夹(O)
=>复制目录
)
接着输入命令:cl XXX.cpp /d1reportSingleClassLayoutX
(第一个XXX
表示源文件的名称,我这里就是继承与多态.cpp
,第二个X
表示想要查看的类的名称,我这里就是B
)
我们可以看到当前B
的内存空间:
当前B
的内存空间里,前四个字节是vbptr(这个就代表里虚基类指针:virtual base ptr
);
和vfptr
(虚函数指针)指向了vftable
(虚函数表)一样,
vbptr
(虚基类指针)指向了vbtable
(虚基类表)。
vbtable(虚基类表)的布局也如图所示,
首先是偏移量0
:表示了虚基类指针再内存布局中的偏移量;
接着是偏移量8
:表示从虚基类
中继承而来的数据成员在内存中的偏移量。
对比普通继承下的内存布局
我们可以对比没有虚继承
下的B
的内存布局来理解:
我们把他们放在一起对比可以看到:
继承虚基类
的类(B
和C
)会把自己从虚基类
继承而来的数据ma
放在自己内存的最末尾(偏移量最大),并在原来ma
的位置填充一个vbptr
(虚基类指针),这个指针指向了vbtable
(虚基类表)。
理解了B
,我们可以看看更为复杂的D
:
同样的,和为使用虚基类
前的内存布局进行对比:
可以看到,将ma
移动到了末尾处,并在含有ma
的地方,都用vbptr
进行填充。
这样一来,就只有一个ma
了!解决了多重继承的重复问题。