C++ primer多继承&虚继承(4)

这里写图片描述
1.多继承
在本文中,我们解释由gcc编译器实现多继承和虚继承的对象的布局。虽然在理想的C++程序中不需要知道这些编译器内部细节,但不幸的是多重继承(特别是虚拟继承)的实现方式有各种各样的不太明确的结论(尤其是,关于向下转型指针,使用指向指针的指针,还有虚拟基类的构造方法的调用命令)。 如果你了解多重继承是如何实现的,你就能预见到这些结论并运用到你的代码中。而且,如果你关心性能,理解虚拟继承的开销也是非常有用的。最后,这很有趣。
这里写图片描述

多重继承
首先我们考虑一个(非虚拟)多重继承的相对简单的例子。看看下面的C++类层次结构。
1 class Top
2 { public: int a;
3 }; class Left : public Top
4 { public: int b;
5 }; class Right : public Top
6 { public: int c;
7 }; class Bottom : public Left, public Right
8 { public: int d;
9 };
使用UML图,我们可以把这个层次结构表示为:

注意Top被继承了两次(在Eiffel语言中这被称作重复继承)。这意味着类型Bottom的一个实例bottom将有两个叫做a的元素(分别为bottom.Left::a和bottom.Right::a)。
Left、Right和Bottom在内存中是如何布局的?让我们先看一个简单的例子。Left和Right拥有如下的结构:
Left Right
Top::a Top::a
Left::b Right::c
请注意第一个属性是从Top继承下来的。这意味着在下面两条语句后
1 Left* left = new Left();
2 Top* top = left;
left和top指向了同一地址,我们可以把Left Object当成Top Object来使用(很明显,Right与此也类似)。那Buttom呢?GCC的建议如下:
Bottom
Left::Top::a
Left::b
Right::Top::a
Right::c
Bottom::d
如果我们提升Bottom指针,会发生什么事呢?
1 Bottom* bottom = new Bottom();
2 Left* left = bottom;
这段代码工作正常。我们可以把一个Bottom的对象当作一个Left对象来使用,因为两个类的内存部局是一样的。那么,如果将其提升为Right呢?会发生什么事?
1 Right* right = bottom;
为了执行这条语句,我们需要判断指针的值以便让它指向Bottom中对应的段。
Bottom
Left::Top::a
Left::b
right Right::Top::a

Right::c
Bottom::d

经过这一步,我们可以像操作正常Right对象一样使用right指针访问bottom。虽然,bottom与right现在指向两个不同的内存地址。出于完整性的缘故,思考一下执行下面这条语句时会出现什么状况。
1 Top* top = bottom;
是的,什么也没有。这条语句是有歧义的:编译器将会报错。
1 error: Top' is an ambiguous base ofBottom’
两种方式可以避免这样的歧义
1 Top* topL = (Left*) bottom;
2 Top* topR = (Right*) bottom;
执行这两条语句后,topL和left会指向同样的地址,topR和right也会指向同样的地址。

2.问题:为什么要有虚继承?是为了解决什么问题?
现象:假如我们有类A是父类,类B和类C继承了类A,而类D既继承类B又继承类C(这种菱形继承关系)。当我们实例化D的对象的时候,每个D的实例化对象中都有了两份完全相同的A的数据。因为保留多份数据成员的拷贝,不仅占用较多的存储空间,还增加了访问这些成员时的困难,容易出错,而实际上,我们并不需要有多份拷贝。
针对这种情况,C++提供虚基类(virtual base class)的方法,使得在继承间接共同基类时只保留一份成员。
现在,将上述类A申明为虚基类,方法如下:
class A //声明基类A
{
// 代码
};
class B: virtual public A //声明类B是类A的公用派生类,A是B的虚基类
{
// 代码
};
class C: virtual public A //声明类C是类A的公用派生类,A是C的虚基类
{
// 代码
};
class D: public B, public C //类D中只有一份A的数据
{
// 代码
};
[注意]:虚基类并不是在声明基类时声明的,而是在声明派生类时,指定继承方式时声明的。因为一个基类可以在生成一个派生类时作为虚基类,而在生成另一个派生类时不作为虚基类。
声明虚基类的一般形式为:class 派生类名: virtual 继承方式 基类名
即在声明派生类时,将关键字 virtual 加到相应的继承方式前面,经过这样的声明后,当基类通过多条派生路径被一个派生类继承时,该派生类只继承该基类一次,也就是说,基类成员只保留一次。
[注意]:为了保证虚基类在派生类中只继承一次,应当在该基类的所有直接派生类中声明为虚基类,否则仍然会出现对基类的多次继承。比如:在上面的菱形继承关系中,如果在派生类B和C中将类A声明为虚基类,而在派生类D中没有将类A声明为虚基类,则如果再有一个派生类E继承自D,则在E中虽然从类B和C路径派生的部分只保留一份基类成员,但从类D路径派生的部分还保留一份基类成员。
问题:如何对虚基类进行初始化呢?
如果在虚基类中定义了带参数的构造函数,而且没有定义默认构造函数,则在其所有派生类(包括直接派生或间接派生的派生类)中,通过构造函数的初始化列表对虚基类进行初始化。如下:
class A //声明基类A
{
A(int i); //申明一个带有参数的构造函数
};
class B: virtual public A //A是B的虚基类
{
B(int n):A(n){ } //B类构造函数,在初始化列表中对虚基类A进行初始化
};
class C: virtual public A //A是C的虚基类
{
C(int n):A(n){ } //C类构造函数,在初始化列表中对虚基类A进行初始化
};
class D: public B, public C
{
D(int n):A(n),B(n),C(n){ } //D类构造函数,在初始化列表中对所有基类进行初始化
};[注意]:在定义类D的构造函数时,与以往使用的方法有所不同。以往,在派生类的构造函数中只需负责对其直接基类初始化,再由其直接基类负责对间接基类初始化。现在,由于虚基类在派生类中只有一份数据成员,所以这份数据成员的初始化必须由派生类直接给出。如果不由最后的派生类直接对虚基类初始化,而由虚基类的直接派生类(如类B和类C)对虚基类初始化,就有可能由于在类B和类C的构造函数中对虚基类给出不同的初始化参数而产生矛盾。所以规定:在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
有的读者会提出:类D的构造函数通过初始化表调了虚基类的构造函数A,而类B和类C的构造函数也通过初始化表调用了虚基类的构造函数A,这样虚基类的构造函数岂非被调用了3次?大家不必过虑,C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C) 对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值