多重继承
来源:互联网 发布:java监听器原理 编辑:程序博客网 时间:2024/05/16 12:17
C++多重继承一直是一个让人搞不太清楚的一个问题,但是有时候为了实现多个接口,多重继承是基本不可避免,当然在Windows下我们有强大的COM来帮我们搞定这个事情,不过如果你想自己实现一套类似于COM的东西出来的时候,麻烦事情就来了。
在COM里面,有两个很基础的,而且我们都会用到的特性:
1. 纯虚接口:一般使用一个只有纯虚函数的类来作为接口
2. 引用计数:在C++中一般都使用引用计数来管理对象的生命周期
这两个功能在一般设计C++接口的时候也经常用到。其实说到底,上面这两个特性牵扯到的是多重继承的二个表现:
1. 多重继承中的数据存储
2. 多重继承中的虚函数
在COM中,纯虚接口是使用的interface来定义的,引用计数是通过IUnknown接口来实现的,所有的接口都是从IUnknown这种接口中派生出来的。当我们需要某一个类实现多个接口的时候,就经常会遇见这样的多重继承:
multi-inheritance-com:
哦?!是不是很眼熟,ios,istream,ostream,iostream。。各种C++书籍最喜欢用的一个示例。好吧,现在我们先自己实现一个吧,看看到底要怎么使用多重继承。
多重继承中对象的的数据存储
这便是使用多重继承的时候经常产生的第一个问题:多副本的数据存储。
当然这个问题很好解决,只需要使用虚继承即可解决。只需要在IA和IB的定义中,在public IBase前加入virtual关键字即可。
结果已经正确了,为什么会发生这种情况呢?虚继承到底干了些什么呢?
我们先来看看在没有使用虚继承的情况下,CImpl的在内存中是怎么样的:
cimpl-memory-normal:
对于普通的public继承(非虚继承),C++会将基类和派生类的内容放在一块,合起来组成一个完整的派生类,在内存上看,它们是连在一起的。按照这样的规则,在这里,IA和IB中就会各包含一个IBase,而IA,IB和CImpl中的部分内容又共同组成了CImpl。在将CImpl对象o的指针转型成IA的指针pA过程中,指针将被移动到IA所在的部分区域,同样在转型成IB的过程中,指针将被移动到IB所在的部分区域(也就是说,转型之后,指针的值都不一样)。在之后的操作中,pA操作的便是IA这个部分中的IBase,而pB操作的便是IB这个部分中的IBase,最后IA部分中的IBase变成了1,而IB部分中的IBase变成了-1。所以输出的结果也就变成了1和-1了。
之后我们修改成了虚继承,看看到底发生了什么?
cimpl-memory-virtual:
原来的IA和IB中的IBase部分变成了一个指向基类的指针,而基类也变成了一个单独的部分。这样一旦对基类做任何的修改,都会通过这个指针重定向到这个独立的基类上去,于是,就不存在多副本的数据存储了,这个诡异的问题也就解决了。但是当然从这个图上我们也可以看到,使用虚继承后,访问数据多了一次跳转,这多出的一次跳转将导致效率的下降一倍甚至更多,所以如果一个类使用的非常频繁,很明显应该尽量避免使用虚继承。
二义性
当然数据的存储只是使用多重继承中遇到的一个问题,现在我们来看另外一个问题,函数的二义性。
首先我们先把数据的存储抛开,单纯的来看一个只有函数的继承关系。
出错了!杯具。。为什么?错误还这么奇怪,神马叫做可以是IBase中的foo又可以是IBase中的foo呢?
这就是使用多重继承的时候经常产生的第二个问题:二义性。
在使用多重继承时,如果有两个被继承的类拥有共同的基类,那么就很容易出现这种情况。那什么是二义性呢?
我们先来看一个更简单的继承关系:
既然直接使用多重继承会有如此多的问题,那么我们能不能通过虚函数来解决这个问题呢?
这里小小的提一下,刚刚二义性里面说到两个类的距离,对于编译器来说,一般是找离当前的类距离最近的函数实现来调用(或者数据来访问),而虚函数则是让编译器做相反的事情:找一个离当前类反向距离最远的函数实现来调用。
好,我们先把上面的程序做一点点小改变,把foo()函数变成一个虚函数,看看有什么变化。
因为我们是用的.运算符来访问的,而不是用指针,所以这里虚函数和普通函数没有任何区别。=.=。。。
好,我们再来小小的修改一下,把他变成指针,让他通过虚表去访问,看看行不行。
CImpl *p = &o;
p->foo();
好吧,我们可以把调用foo()的几句话都去掉,来看看CImpl中生成的虚表到底是个什么样子。
debug-result-vptr-1:
在这个实例中,IA和CImpl部分公用一个虚表,而IB则使用另外的一个虚表(两个虚表这个特性主要是在指针类型转换的时候有用,这里就不说了)。
在这IA的虚表中存在一个指向IBase::foo()的指针,在IB的虚表中也存在一个指向IBase::foo()的指针,所以在CImpl中,可以找到两个IBase::foo()函数的指针,这样,编译器就无法确定到底应该使用哪一个IBase::foo()函数作为他自己的foo()函数了。二义性也就产生了。
在多重继承中编译器对this指针的修正
这里再让我们来看看这次编译出来的虚表,看看还有什么发现。
debug-result-vptr-2:
0x004112a3 [thunk]:CImpl::foo`adjustor{4}’ (void) *
这个看上去很怪的函数是什么呢?我们反汇编一下他看看。
virtual-function-wrapper:
这里可以看到有一句汇编指令:sub ecx, 4。这条指令的左右其实是在修正this指针。
因为从IB的虚表来的请求,this指针都是指向CImpl中IB的部分,而当调用CImpl中的foo函数时,如果还使用IB的this指针,那么程序就会出错,所以这里需要先将this指针修正为CImpl所在的地址,才能调用CImpl的foo函数。
在程序运行的时候,this指针一般被存储在ecx寄存器中,或者当前函数的第一个参数传递进去,不过不同的语言或者不同的编译器编译出来的代码可能会不一样。
我们这里的析构函数都是虚函数,所以我们还可以在截图中看到,编译器会对析构函数做同样的处理。
如何同时解决数据访问和二义性问题呢
貌似到现在都只提到最简单的一种多重继承的情况,但是实际上我们已经遇到了很多的问题了,既然多重继承中会有这么多问题,那我们有没有什么比较通用的方法能把他们一起解决了呢?
方法肯定是有的:
1. 使用虚继承
这算是一种确实可行的方法,只是说会带来额外的时间和空间的开销,访问任何一个数据,都需要通过虚继承表进行跳转,不过一般来说够用了。
这个思想就类似于COM了,只是说COM用的是纯虚函数,对于那些会产生二义性的类,我们在最后都实现一边,这样就不会有问题了。这样带来的时间开销也仅仅是调用时查询一次虚表。但是麻烦的地方就是,有时候继承一下,你可能就要实现一下了,比如引用计数神马的,当然你也可以通过模版来简化你的代码。
class
IBase
{
public
:
virtual
~IBase() {}
virtual
void
show() = 0;
};
class
IA :
public
IBase
{
public
:
virtual
~IA() {}
virtual
int
inc() = 0;
};
class
IB :
public
IBase
{
public
:
virtual
~IB() {}
virtual
int
dec() = 0;
};
class
CImpl :
public
IA,
public
IB
{
public
:
CImpl() : n(0) {}
virtual
~CImpl() {}
int
inc() {
return
++n; }
int
dec() {
return
--n; }
void
show() {
printf
(
"%dn"
, n); }
private
:
int
n;
};
这种实现比较复杂,wtl中用的比较多,一般是用在引用计数上,好处很明显,就是可以继承,不用每个类都实现一个引用计数,而只用将新的基类的引用计数转移至原本存在的类上就可以了。
class
IBase
{
public
:
virtual
~IBase() {}
void
foo() {}
};
class
IA :
public
IBase
{
public
:
virtual
~IA() {}
};
class
IShifter
{
public
:
virtual
~IShifter() {}
void
foo() { do_foo(); }
protected
:
virtual
void
do_foo() = 0;
};
class
IB :
public
IShifter
{
public
:
virtual
~IB() {}
};
class
CImpl :
public
IA,
public
IB
{
public
:
virtual
~CImpl() {}
void
foo() { IA::foo(); }
protected
:
virtual
void
do_foo() { IA::foo(); }
};
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- 多重继承
- Search for a Range
- Android UI性能优化实战 识别绘制中的性能问题
- 【译】Unity3D Shader 新手教程(1/6)
- poj 2993 Emag eht htiw Em Pleh 暑假第11题 模拟 大水
- JS检查浏览器类型和版本
- 多重继承
- 十道海量数据处理面试题与十个方法大总结
- linux 网络编程-基础篇
- 初学数据结构之堆栈
- Linux的netstat命令
- Spring框架之Filter应用
- 【译】Unity3D Shader 新手教程(2/6) —— 积雪Shader
- java 开发内存不足的问题——gc overhead limit exceeded eclipse
- TP 纯sql写法