菱形继承是继承里面比较复杂的有一种,在这里我分别对菱形继承、菱形虚拟继承、菱形虚拟继承含虚函数这几种情况
一、菱形继承
看下图就是一个菱形继承
#include
using namespace std;
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public C, public B
{
public:
int _d;
};
int main()
{
D dd;
cout << sizeof(dd) << endl;
dd.B::_a = 1;
dd._b = 3;
dd.C::_a = 2;
dd._c = 4;
dd._d = 5;
B bb;
C cc;
cout << sizeof(bb) << endl;
return 0;
}
从内存监视口可以发现dd占用20个字节,但是通过视窗口可以明显发现C类和B类继承的同一个A类的成员_a的值不一样,这就产生了二义性,同一个成员怎可么可以同时又两个值呢?
所以为了解决在菱形继承或者多继承中都会出现的二义性和数据冗余的问题,就提出来虚继承,下面来看看虚继承是怎么实现的..
二.菱形虚拟继承
在子类继承基类时,通过virtual关键字就可以虚继承,下面我们通过内存布局来看看虚继承是怎么处理数据二义性的...
#include
using namespace std;
class A
{
public:
int _a;
};
class B :virtual public A
{
public:
int _b;
};
class C :virtual public A
{
public:
int _c;
};
class D : public C, public B
{
public:
int _d;
};
int main()
{
D dd;
cout << sizeof(dd) << endl;
dd._a = 1;
dd._b = 3;
dd._a = 2;
dd._c = 4;
dd._d = 5;
B bb;
C cc;
cout << sizeof(bb) << endl;
return 0;
}
这里可以发现 虚继承的dd对象和bb对象都比普通继承之前多了四个字节的空间,说明这四个字节的空间肯定和虚继承有关,下面我们看看内存分布来了解虚继承...
从监视窗口就可以发现C类和B类继承的同一个A类的成员_a的值现在的值是一样的,都是第二次赋值后的值2. 下面我们看看虚继承是怎么实现C类和B类公用一个成员_a
从上图可以发现对象dd的内存布局与虚继承之前有很大的区别,首先cc对象和bb对象的内存空间中都分别多了一个存储着一个地址的空间,而把它们_a变量放在了成员变量的最底下,使_a成为一个公共的变量.
那么cc对象内存中的0x011ceae8和 bb对象内存中的0x011cdbb0这两个地址指向的空间存的是什么呢?看下图这两个地址就是cc和bb对象的虚基表地址,虚基表里存储的是偏移量,cc对象和bb对象就可以通过它们自己的虚基表中的偏移量,来找到公共的基类,然后调用A里的_a。
总结:
1.虚继承解决了在菱形继承里面子类对象包含多分父类对象的数据冗余现象和二义性。
2.虚拟继承体系看起来好复杂,但是我们实际应用中很少会定义如此复杂的菱行虚拟继承体系,一般不到万不得已不要定义菱形虚拟继承结构体系,虽然解决了二义性,但也带来了数据冗余的现象。
三.菱形虚拟继承中包含虚函数
在看看下面代码,我在上面代码的基础上再加了些虚函数,让我我们来理解菱形继承里的虚表
#include
using namespace std;
//定义一个可以指向对象里函数的函数指针
typedef void(*func)();
//打印虚函数表
void printvtable(int* vtable)
{
cout << "虚表地址>" << vtable << endl;
for (int i = 0; vtable[i] != 0; ++i)
{
printf("第%d个虚函数地址:0x%x,->", i, vtable[i]);
func f = (func)vtable[i];
f();
}
cout << endl;
}
class A
{
public:
int _a;
virtual void func1()
{
cout << "A::func1()" << endl;
}
};
class B :virtual public A
{
public:
int _b;
virtual void func2()
{
cout << "B::func2()" << endl;
}
virtual void func1()
{
cout << "B::func1()" << endl;
}
};
class C :virtual public A
{
public:
int _c;
virtual void func1()
{
cout << "C::func1()" << endl;
}
};
class D : public C,public B
{
public:
int _d;
virtual void func1()
{
cout << "D::func1()" << endl;
}
virtual void func3()
{
cout << "D::func3()" << endl;
}
};
int main()
{
D dd;
B bb;
C cc;
A aa;
cout << sizeof(dd) << endl;
dd._a = 1;
dd._b = 3;
dd._a = 2;
dd._c = 4;
dd._d = 5;
cout << sizeof(bb) << endl;
//D类里继承B类的虚表
cout << "D:bb:0x" << ⅆ
int* vtabledd = (int*)(*(int*)&dd);
printvtable(vtabledd);
//D类里继承C类的虚表
cout << "D:cc:0x" << ((int*)&dd + 3);
int* vtablecc = (int*)(*((int*)&dd + 3));
printvtable(vtablecc);
//D类里继承A类的虚表
cout << "D:aa:0x" << ((int*)&dd + 6);
int* vtableaa = (int*)(*((int*)&dd +6));
printvtable(vtableaa);
return 0;
}
要清楚虚表和虚基表是两个完全不同的概念
虚表:虚函数表,存的是虚函数->多态
虚基表:存的是偏移量,解决二义性和数据冗余性。
好了,那么我上面我还提出了一个问题,就是虚基表里上面的地址为什么存着一个寻找对象自己的虚表的偏移量?下面我们解决
提醒:因为我代码改动后,再次编译内存中对象的地址数值会变化,但不影响大家看内存是怎样分布的和虚基表的作用。(仅下面这个图的地址数值和上面输出结果不一样)
下面我们来看看菱形继承中含虚函数后dd对象的内存分布是什么样子?是sizeof(dd)为什么是32看下图就知道了
从上图可以发现,当给bb对象里加了个fun2函数后发现dd的对象模型又多了四个字节,这四个字节是在dd对象中继承的bb的内存空间中多出来的,就是用来存放bb对象自己的虚表地址。
因为aa对象被两个对象所继承,所以aa的虚表被被公用,因此bb和cc对象的虚函数就不能放在他们公共的虚表里,只能创建自己的虚表,把自己的虚函数放在自己的虚表。
然后dd对象里自己的虚函数(非继承来的)存在第一个继承来的对象的虚表里。
下面看看菱形继承dd对象里的虚函数都分别在什么地方存储的:
通过上面的内存分布我总结:
1.dd对象继承重写aa对象的虚函数存储在继承来的aa对象的虚函数表里。
2.dd对象里自己的虚函数(非继承来的)存在第一个继承来的对象的虚表里。
3.因为aa对象被两个对象所继承,所以aa的虚表被被公用,因此bb和cc对象的虚函数就不能放在他们公共的虚表里,只能创建自己的虚表,把自己的虚函数放在自己的虚表。
下面我根据总结上面总结画出了一份菱虚拟继承的对象模型
如有错误之处,请多多指点