理解虚继承
本篇博客主要对继承的对象模型以及虚拟继承进行一个整理和总结。
单继承
//单继承
class A
{
public:
int _a;
};
class B :public A
{
public:
int _b;
};
int main()
{
B b;
b._a = 1;
b._b = 2;
return 0;
}
在上面的代码中,类B公有继承了类A,通过监视窗口和内存,可以看到b对象的内存模型:首先是基类的成员_a,其次是子类B自己的成员_b; sizeof(b)=8.
class A
{
public:
int _a;
};
class B :public A
{
public:
int _b;
};
class C :public B
{
public:
int _c;
};
int main()
{
C c;
c._a = 1;
c._b = 2;
c._b = 3;
return 0;
}
在这种情况下:A为基类,B公有继承A,C公有继承B。按照方式一的对象模型,我们可以推测,对于方式二,c的成员有三个,在内存中的排布方式应该为:最上层基类的成员、中间是B的成员_b,最下层是C类自己的成员_c。sizeof©=12。打开调试窗口,观察到内存布局与我们猜测的一样
多继承
//多继承
class B1
{
public:
int _b1;
};
class B2
{
public:
int _b2;
};
class C :public B1,public B2
{
public:
int _c;
};
int main()
{
C c;
c._b1 = 1;
c._b2 = 2;
c._c = 3;
return 0;
}
同样,有了前面对单继承分析的基础,对于多重继承,也不难理解。子类C继承了两个基类,分别是B1,B2,那么对于C类创建的对象c,它的对象模型一个包括三部分:按照继承的顺序,最上面一个是B1类的成员,中间是B2类的成员,最下面是C类自己的成员_c,sizeof©=12;
在引出“虚继承”这个概念之前,首先来看下面一段代码
//菱形继承
class B
{
public:
int _b;
};
class C1:public B
{
public:
int _c1;
};
class C2:public B
{
public:
int _c2;
};
class D :public C1,public C2
{
public:
int _d;
};
int main()
{
D d;
d._b = 1;//错误,对_b的访问不明确
d.C1::_b = 1;//明确指定是哪个作用域中的_b
d.C2::_b = 2;
return 0;
}
当用对象d直接对_b成员进行赋值时,出现错误。那么为什么会出现这样的问题呢?
这种菱形继承有一个很大的问题存在:二义性。 在这种继承关系中,D不仅继承了来自C1中的_b,还继承了C2中的_b,那么当定义D类的对象d时,直接对d中的_b进行赋值,究竟是C1中的_b呢?还是C2中的_b?
虚拟继承是多重继承中特有的概念,那么为什么要有虚拟继承?
-
首先要明白菱形继承的本质:
一个派生类有多个直接基类,这些直接基类又有一个共同基类。 -
这种方式的共同问题:
最终的派生类中会保留间接共同基类数据成员的多份同名成员,即“数据冗余”。且直接访问会产生二义性。 -
解决办法:
(1) 访问时加上类的作用域限定符
(2)只保留一份间接共同基类的同名成员
对于办法(1),虽然解决了“二义性”问题,但是“数据冗余”问题还存在,因为还是保存着多份基类的同名成员。
对于办法(2),从根本上解决了二义性问题和“数据冗余”问题
对于上面的菱形继承,由于类D继承自C1,C2,而C1和C2都继承自基类B,所以D中会保存两份变量_b,分别是C1与C2中的。sizeof(d)=20,也证明了我们的判断。实际上_b变量只需要保存一份。为了节约内存空间,可以将C1,C2对B的继承定义为虚拟继承,那么B就成了虚基类。
虚拟继承具体实现方式:在C1,C2对B的继承方式前添加关键字virtual
class B
{
public:
int _b;
};
class C1:virtual public B
{
public:
int _c1;
};
class C2:virtual public B
{
public:
int _c2;
};
class D :public C1,public C2
{
public:
int _d;
};
int main()
{
D d;
d._b = 1;
d._c1 = 2;
d._c2 = 3;
d._d = 4;
return 0;
}
首先来看,对于普通的多重继承,它的对象模型是怎样的?
可以看出,d的对象模型是:先是C1类中的成员,再是C2类中的成员,最后是D类自己的成员,sizeof(d)=20。如果换成虚继承,对象模型又有什么区别呢?
可以看到,_b成员只保留了一份,理论上我们推测sizeof(d)的大小为16(_b+_c1+_c2+_d),但实际上并非如此。虚继承的对象模型中,C1和C2都维护了一个指针,基类成员_b反而在最下层。思考这样一个问题:C1与C2中都有成员_b,但是在对象模型中_b不在其各自所占的内存区域,而在最下层,那C1和C2又是如何找到成员_b呢?
这里就不难理解虚基表指针的作用了,各自的虚基表指针指向一个偏移量表格,记录了相对于_b的偏移量
所以sizeof(d)的大小=24
为了更便于查看虚继承的对象模型,我们举一个单继承的例子(虽然虚拟继承是用在多重继承)
class A
{
public:
int _a;
};
class B:virtual public A
{
public:
int _b;
};
int main()
{
B b;
b._a = 1;
b._b = 2;
cout << "b:" << sizeof(b) << endl;
return 0;
}
在上面的代码中,B与A是单继承的关系,B虚拟继承A,按照我们最先看的单级派生对象模型:是基类成员在最上层,然后才是子类成员。如果变为虚继承,则变化就比较明显了–>子类在上,基类在下
总结
- 虚拟继承的对象模型倒立
- 虚拟继承对象中多了4个字节
- 编译器为派生类生成默认的构造函数有两个参数:空间首地址和1,其中1是虚拟继承标志
- 取对象前4个字节中的内容
- 以该内容为地址addr,取addr+4空间中的内容offset
- 给对象起始地址向后偏移offset字节位置赋值