理解虚基类多重继承的问题

Posted Redamanc

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了理解虚基类多重继承的问题相关的知识,希望对你有一定的参考价值。

什么是多重继承

多重继承,很好理解,一个派生类如果只继承一个基类,称作单继承
一个派生类如果继承了多个基类,称作多继承
如图所示:
在这里插入图片描述

多重继承的优点

这个很好理解:
多重继承可以做更多的代码复用!
派生类通过多重继承,可以得到多个基类的数据和方法,更大程度的实现了代码复用。

关于菱形继承的问题

凡事有利也有弊,对于多继承而言,也有自己的缺点。
我们先通过了解菱形继承来探究多重继承的缺点:
菱形继承是多继承的一种情况,继承方式如图所示:
在这里插入图片描述
从图中我们可以看到:
类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关键字的使用场景:

  1. 修饰成员方法时:产生虚函数
  2. 修饰继承方式时:产生虚基类

对于被虚继承的类,称作虚基类
比如说:

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;
};

我们做出修改:
BC的继承方式都改为虚继承;接下来继续运行代码:
此时会报错:
在这里插入图片描述
提示说:"A::A" : 没有合适的默认构造函数可用
为什么会这样呢?
我们可以这么理解:
刚开始BC单继承A的时候,实例化对象时,会首先调用基类的构造函数,也就是A的构造函数,到了D,由于多继承了BC,所以在实例化D的对象时,会首先调用BC的构造函数,然后调用自己(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的内存布局来理解:
在这里插入图片描述
我们把他们放在一起对比可以看到:
在这里插入图片描述
继承虚基类的类(BC)会把自己从虚基类继承而来的数据ma放在自己内存的最末尾(偏移量最大),并在原来ma的位置填充一个vbptr(虚基类指针),这个指针指向了vbtable(虚基类表)。
理解了B,我们可以看看更为复杂的D
在这里插入图片描述
同样的,和为使用虚基类前的内存布局进行对比:
在这里插入图片描述
可以看到,将ma移动到了末尾处,并在含有ma的地方,都用vbptr进行填充。
这样一来,就只有一个ma了!解决了多重继承的重复问题。

以上是关于理解虚基类多重继承的问题的主要内容,如果未能解决你的问题,请参考以下文章

多重继承,虚基类

作用域分辨操作符,虚基类,赋值兼容规则。哪些可以解决多重继承二义

C++ 虚基类

19.理解虚基类虚继承

C++ 继承和多态

C++ 继承和多态