多重继承

多重继承

在面向对象的领域中,对于多重继承的意义、是否需要在C++引入是有争论的(是的!多重继承在纯粹的对象思想看来有些怪异)。基本上撇开对于它的意义的探究,在下面着重阐述的是C++中多重继承的实现技术,以解释一些看来不易理解的地方。

考虑一个类C同时继承类A和类B。那么一个c对象(C的实例)将既包含一个a对象(A的实例),也包含一个b对象(B的实例)。我们知道,在单继承情况下,继承对象是在基类对象上自然扩张的,因为不能破坏基类对象的空间布局。那么多重继承立刻就带来了一个问题:c对象是在哪个子对象(ab)的基础上扩张而来?显然无论选择的是a,还是b,都存在对于另一个子对象的上溯造型的“指针调整”问题。设pc指向对象cpb是一个B的指针,执行:

pb = pc;

那么pb将指向对象b。如果c不是从b扩张而来,这势必要求在赋值的时候做出“修正”(需要计算出子对象bc中的相对偏移量),而不是pcpb的直接拷贝。结果,pbpc的值不再是相同的。当调用基类的成员函数时,可能会遇到同样的情况。设fb()B的一个成员函数,执行:

c.fb();

那么在调用fb()函数之前,压入栈中的所谓this指针应该指向对象b。结果再次(隐含)发生了指针调整的过程。

如果c对象既不选择从a,也不选择从b扩张而来,那么这只会增加指针调整的次数:无论上溯到a还是b,都需要进行指针调整。这不是高效的做法。

根据常见的做法,多重继承语法中基类出现的先后次序,决定对象布局中子对象排列的先后次序。例如在C的继承声明中,A在前,B在后,那么在c对象布局中,a在前,b在后(再后是c的成员数据)。

作为一般性的结论,对于多重继承下的第二个以及后面的基类,当发生上溯造型时都需要进行指针调整。而当上溯到第一个基类对象时则不需要调整。例如AC所继承的第一个基类,当执行:

pa = pc; 

或者:

c.fa();

其中pa是一个A的指针而fa()A的成员函数,那么是不用进行指针调整的,就像单继承下的情况一样。

但是多重继承最困难的地方,在于对虚机制的支持。作为个人体会,我学习多重继承不久就有一个不成文的想法,但很快被否定了。尽管这个想法是行不通的,但我想讨论一下还是能够加深对于问题的认识。下面来看一个并不复杂的例子,假设类A有虚函数,其虚函数表(部分)如下:

slot3 – A::fm()

slot4 – A::fn()

B的虚函数表(部分)如下:

slot2 – B::fx()

slot3 – B::fy()

C仍然是从AB继承的,并且覆盖了Afm()方法和Bfy()方法。

我的理解是,类C将拥有一个更大的虚函数表,就象单继承下的虚函数表“扩张”一样。这个虚函数表中将至少包含四项分别指向C::fm()A::fn()B::fx()C::fy()的函数指针。但是我们马上就注意到虚函数表项(slot)安排的冲突。在类A的虚函数表中,函数fm()占据第三个表项(slot3);而在类B中,函数fy()占据相同的位置。这点不难理解:毕竟在多重继承发生以前,不同类的虚函数表是各自安排的、彼此没有影响的,这样的“冲突”是会大量存在的。一个可能的解决办法是重新安排各个基类虚函数表的布局(受到了多重继承的影响)。例如,简单起见,我们让A的虚函数表不变,而调整B的虚函数表如下:

slot2 – B::fx()

slot5 – B::fy()

好了,通过移动有冲突的表项到不会发生冲突的位置,我们让冲突“消失”了。现在AB的虚函数表可以“叠合”在一起且彼此不会覆盖对方的表项,成为大的C的虚函数表的一部分。经过调整后C的虚函数表(部分)如下:

slot2 – B::fx()

slot3 – C::fm()

slot4 – A::fn()

slot5 – C::fy()

我称这种调整的手段,为虚函数表间的“调和”。如果另外有一个H类从BEF继承,就可能发现上述的“调和”还不够(冲突再次发生),再次进行更大范围的“调和”,直到使所有的多重继承都满足。

但是以这样的方式解决问题让人感到恐慌,因为多重继承的关系竟要求回过头来调整基类的虚函数表,实在有悖于常理,即使理论上是可行的。同时,这是极其拙劣、低效的做法,会导致类的虚函数表膨胀的很大,而且其中有很多“哑”表项。例如前面的类B,其第三,第四项是哑表项(不被使用),因为B“考虑”到有其它类(如A)的虚函数表使用这两项。另外,在上溯造型到第二个或以后的子对象时,还可能要顺带设置v-ptr指向多重继承后大的虚函数表。我自己也不相信这种做法会被实际采用,所以多重继承的虚机制实现对我来说长期是个谜。

但是问题究竟出在哪里?更好的做法是什么?原来,一个对象(如C的实例)可以关联多个虚函数表(多重继承下),没有必要强制一个对象只能关联到一个虚函数表(这迫使一个大的、统一的虚函数表的想法出现,受到单继承的影响)。经过多重继承后的类C可以认为有二个虚函数表,分别是:

slot2 – B::fx()

slot3 – C::fy()

slot3 – C::fm()

slot4 – A::fn()

前者相当于B的虚函数表的“覆盖”版本(部分)。后者相当于A的覆盖版本(部分),同时也是C“自己”的虚函数表。

对于c中的子对象b来说,它与一个单独的B的实例没有什么两样。它含有v-ptr,并且指向的虚函数表,是B的覆盖版本。一般地,对于第二个和后面的子对象,它们的v-ptr分别指向所属类的虚函数表或者其覆盖版本(如果有虚函数表或者其虚函数被覆盖的话)。对于第一个子对象,就象单继承的情形那样,与继承类对象(如c)“合用”同一个虚函数表。

很好,这样就摆脱了那个虚函数表“调和”的噩梦,基类再也不用担心受到多重继承的影响了,一切恢复了它们的原样。但是,前面困扰我们的指针调整问题将会再次出现,当上溯到第二或后面的子对象,并且调用被覆盖的方法时。例如执行:

pb = pc;

然后执行:

pb->fy();

那么由于fy()被覆盖,所以实际执行的应该是C::fy()。但是对于C::fy()的调用需要c对象的地址,只有b的地址是不能满足的。然而传递给C::fy()的,只有b的地址,怎么办?

通过所谓的Thunk技术可以地解决这个问题。此时,虚函数表中的对应fy()的表项填充的是“准”C::fy()入口。之所以说是准入口,是因为它做了以下工作:调整this指针,使之准确指向c;然后跳到C::fy()的入口执行。一切OK,多态得到了正确的贯彻。

我们看到,指针调整始终是多重继承挥之不去的问题。这种表面的、背后的不断的调整操作,正揭示了多重继承的困难和不自然。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值