虚拟继承和菱形虚拟继承的剖析

      在上一篇已经讲了一些简单的继承方式,在最后的时候我们设计了一个菱形继承模型,最终引出了二义性问题,但是并没有对这个问题进行解决,这篇文章将会解决这个问题。


首先咱们先介绍一下一个新的继承方式:
虚继承
继承格式:class 派生类名字:virtual public/protected/private基类名字{...};

概念:C++使用虚拟继承,解决了从不同路径继承来的相同基类的数据成员在内存中有不同的拷贝造成数据不一致的问题,将共同基类设置为虚基类,这时从不同路径继承的虚基类在内存就只有一个映射。

接下来咱们写个代码来分析一下他的模型,代码如下:

#include
using namespace std;
class A
{
public:
 virtual void functest1()
 {
  cout << "A::functest1()" << endl;
 }
private:
 int _a;
};
class B:virtual public A
{
public:
 virtual void functest1()
 {
  cout << "B::functest1()" << endl;
 }
private:
 int _b;
};
int main()
{
 B b1;
 cout << sizeof(b1) << endl;
 return 0;
}

运行结果为:

虚拟继承和菱形虚拟继承的剖析_第1张图片


知道大小以后,我们可以从内存出取出对象b1的内容,如下图:

虚拟继承和菱形虚拟继承的剖析_第2张图片

上图就是对象b1的内存分布图,我们可以看到里面一个红色框和一个黑色框,有一个是我们的虚表地址,而另一个就是虚继承时产生的偏移表格的表的地址,我们通过观察他们的内容,来对他们进行区分:

虚拟继承和菱形虚拟继承的剖析_第3张图片

通过上图我们可以看到偏移表格放在上面,而通过继承而来的虚表在模型的最下面,虚表里面的内容就是虚函数的地址大家已经知道了,但是偏移表格里面的0和8是什么,大家可能还有疑惑,咱们在结合对象的内存分布进行分析:

虚拟继承和菱形虚拟继承的剖析_第4张图片


从上图我们可以看到偏移表格地址存放的位置就是对象的起始地址所以是0,而偏移表格地址存放的位置就相对于虚基类的起始地址向下偏移了8个字节,所以偏移表格的第二个内容存的是8。现在大家可能对偏移表格有了一定的了解了吧。接下来咱们按着非虚拟继承的方式继续往下看,在非虚拟的单继承中,派生类新定义的虚函数放在继承基类的虚表的后面。但是虚继承中,派生类新定义的虚函数会放在那里,咱们把派生类的代码稍微改动一下:

class B:virtual public A
{
public:
	//重写基类的虚函数
	virtual void functest1()
	{
		cout << "B::functest1()" << endl;
	}
	//派生类新定义的虚函数
	virtual void functest2()
	{
	cout << "B::functest2()" << endl;
	}
private:
	int _b;
};

咱们先测一下现在这个派生类的大小把:

从上图可以看到大小为20,比刚才多了4个字节,所以可以猜测一下或许派生类新定义的虚函数并没有跟在继承自基类虚表的后面,接下来咱们就打开内存来验证一下咱们的猜想:

虚拟继承和菱形虚拟继承的剖析_第5张图片



从上图我们可以看出偏移表格的位置发生了变化了,他的上面还存放了一个虚表的地址,偏移表格的第一个内容也变成了-4,因为他距离对象的起始位置偏移了4个字节所以也就可以解释为什么变成了-4;他距离虚基类的位置没有变,所以第二个内容还是8;分析完偏移表格之后,咱们在看看第一个虚表里面保存的是不是派生类新定义的虚函数。

咱们设计一个函数通过对象的首地址取得虚表的地址,进而访问虚表的内容,设计的函数代码如下:

void display(const B&b1)
{
	//把虚表的首地址放到pfun里面
	Pfun *pfun = (Pfun*)(*((int*)&b1));

	//遍历整个虚表
	while (*pfun)
	{
		(*pfun)();
		pfun++;
	}
}

运行结果如下:

虚拟继承和菱形虚拟继承的剖析_第6张图片

通过运行结果咱们可以看出咱们的分析是对的,虚拟继承的多继承可以用类似的方法分析,咱们就不再这分析了,由读者底下自行分析,咱们来分析一下菱形虚拟继承,代码如下:

#include
using namespace std;
class A
{
public:

	virtual void functest1()
	{
		cout << "A::functest1()" << endl;
	}
private:
	int _a;
};
class B:virtual public A
{
public:
	//重写基类的虚函数
	virtual void functest1()
	{
		cout << "B::functest1()" << endl;
	}
	派生类新定义的虚函数
	virtual void functest2()
	{
	cout << "B::functest2()" << endl;
	}
private:
	int _b;
};
class B1 :virtual public A
{
public:
	//重写基类的虚函数
	virtual void functest1()
	{
		cout << "B1::functest1()" << endl;
	}
	派生类新定义的虚函数
	virtual void functest3()
	{
		cout << "B1::functest2()" << endl;
	}
private:
	int _b0;
};
class C: public B, public B1
{
public:
	//重写A基类的虚函数
	virtual void functest1()
	{
		cout << "c::functest1()" << endl;
	}
	//重写B基类的虚函数
	virtual void functest2()
	{
		cout << "C::functest2()" << endl;
	}
	//重写B0基类的虚函数
	virtual void functest3()
	{
		cout << "C::functest3()" << endl;
	}
	//派生类新定义的虚函数
	virtual void functest4()
	{
		cout << "C::functest4()" << endl;
	}
private:
	int _c;
};
typedef void(*Pfun)();
void display(const B&b1)
{
	//把虚表的首地址放到pfun里面
	Pfun *pfun = (Pfun*)(*((int*)&b1));

	//遍历整个虚表
	while (*pfun)
	{
		(*pfun)();
		pfun++;
	}
}
int main()
{
	C c1;
	cout << sizeof(c1) << endl;
	display(c1);
	return 0;
}

运行结果如下:

虚拟继承和菱形虚拟继承的剖析_第7张图片

看完运行结果,咱们到内存看一下,内存分布图如下:

虚拟继承和菱形虚拟继承的剖析_第8张图片



从上图咱们看以看到虚拟菱形继承,虚基类只保存了一份,所以也就不存在数据的冗余,也就不会产生上一篇文章提到的二义性。B继承A和B1继承A都是虚拟单继承,通过在上面的分析,想必读者已经可以自行分析了,由C继承B和B1是非虚拟多继承,读者按照这种方法对虚拟菱形继承进行分析应该就不会有什么问题了。

总结:虚拟继承虽然解决了菱形继承的产生的二义性和数据冗余问题,但是他本身也存在一定的问题,访问虚基类数据时,要通过偏移表格进行间接访问,效率就会比较低,而且模型会更加复杂,理解起来也比较困难,所以非必要的时候尽量避免菱形继承,应该尝试换一种设计模式。










你可能感兴趣的:(C++,class,虚函数,虚拟继承,菱形虚拟继承)