5D空间

学习总结与经验交流

   :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理 ::
  在学习多重继承、二义性、虚基类的时候遇到了一些困惑。经过一定的学习摸索,虽然在底层机制上还不太清楚,但是在抽象层面上有了一定理解。
  书上只有一个虚基类的概念,即在继承的时候加上关键字virtual。这里我们姑且把这种继承方式叫做虚继承。现在先来说一下虚继承和一般继承的区别。
  要解释这一系列问题,我们首先要搞清楚这一系列概念意味着什么。多重继承不用赘述。现在先就二义性和虚继承谈谈我的看法。

  在一般的继承中(非虚继承),每一个派生类都保存了一份完整的基类副本。考虑以下继承:
class A
{
   
void print();
}
;

class B : public A
{
   
void print();
}
;

class C : public C
{
   
void print();
}
;

在这样一系列继承体系中,A包含一份print(),B包含了两份,而C则包含了三分prin()。这里总共有6份独立的print()函数。虽然在C中调用B::print()感觉和B中调用print()效果一样,但他们确实是作为两个副本存在。  而在虚继承中,考虑如下继承:
class A
{
   
void print();
}
;

class B : virtual public A
{
   
void print();
}
;

class C : virtual public C
{
   
void print();
}
;
B只含有一份print()副本,但是却可以通过A::print()调用A的print()函数。同理,C也只包含了一份print()副本。这里总共只有3分print()副本。虚继承中基类的数据并没有变多一份给派生类,而只是使用权移交了,就好像A有一栋楼,虚继承给B,名义上B也拥有了这栋楼,可以使用,但是并没有真正为B另外建一栋一模一样的楼。

  二义性:要解释二义性,最好先定义一个概念:名字间隔。笼统地表达,一个名字的间隔就是某个数据的名字从继承层次中首次出现到达最后派生类时中间隔了多少相同的名字。间隔越少,这个名字的优先级越高。当然直接在最终类里面声明的名字具有最高的优先级。比如考虑一开始的普通继承:
class A
{
   
void print();
}
;

class B : public A
{
   
void print();
}
;

class C : public C
{
   
void print();
}
;

如果使用C的对象,那么A中的pirnt与C间隔最大,C中的print与C的间隔最短,所以如果直接调用C对象的print函数,那么将调用C版本的print。如果C没有定义一个print函数,那么B中的print函数与C间隔最小,那么调用C对象的print函数时,将调用B版本的print函数。
  有了这个概念,现在来解释二义性:如果存在两个及其以上的名字距离最终派生类的距离最短(长度一样),那么,根据刚才由名字间隔定义的优先级别,在直接调用这个派生类对象的相应数据时,便不知道该调用哪个版本了(注意直接两个字,因为可以通过二元::来分辨具体的版本以调用,所以即使名字存在二义性,如果未调用这些名字,编译器可能不会报错)。有两种情况(到目前为止我看到的)可能导致二义性:1、在类中声明了两个名字一样的成员:这是最糟糕的情况,因为如果这样做了,没有办法弥补,但这也是最好的情况,因为编译器根本不会让你这么做。2、多继承的时候继承了两个间隔一样的名字:通常难以对付的是这种情况。
  关于上述第二种情况(多继承),这些具有二义性的名字可能1、来自两个基类各自的声明,2、也可能来自两个基类继承自更高层次的同一基类(菱形继承),3、也可能其中一个名字来自基类声明,另一个名字来自另一个基类对更高层次基类的继承。无论如何,只要同时存在两个及其以上具有如果存在两个及其以上的名字距离最终派生类的距离最短(长度一样),那么就存在二义性。

1、来自两个基类各自声明
class B1
{
   
void print();
}
;

class B2 :
{
   
void print();
}
;

class C : public B1, public B2
{
}
;

2、菱形继承
class A
{
   
void print();
}
;

class B1 : public A
{
}
;

class B2 : public A
{
}
;

class C : public B1, public B2
{
}
;

3、其中一个名字来自基类声明,另一个名字来自另一个基类对更高层次基类的继承
class A
{
   
void print();
}
;

class B1 : public A
{
}
;

class B2 :
{
   
void print();   
}
;

class C : public B1, public B2
{
}
;
(注意:虽然A版本的print是通过B1到达C的,但是A->B1->C的过程中,A版本的print与C之间并没有间隔其他的print,这与B2版本的print一样,所以他们具有相同的名字间隔,因此具有二义性)

  二义性的解决办法:
  1、在最终派生类中定义一个相同名字的成员,这样这个名字距离最终派生类最近,所以就会调用这个名字下的数据(通常教材里叫做这个名字把其他名字隐藏了)。这个名字(如果是函数)你可以自己定义新的方法,也可以通过二元::调用你已知的存在二义性的名字中的某一个(注意:如果你选择的调用版本不是该派生类的直接基类,那么该如何调用呢?比如A->B->C,那么从C的对象c调用A的print函数,c.A::print()是否可行?我在vs2010上,虽然报错但是编译通过且正常运行。如果各位有任何见解或建议,希望不吝赐教。)
  2、使用虚继承(针对菱形继承等):回想一下虚继承和普通继承,通过虚继承的方法可以消除重复副本带来的二义性问题。比如在某一继承层次上,这个某两个名字具有二义性,然而顺着继承层次向上分析,却发现这两个名字其实是同一个东西的两个副本,这个时候如果使用虚继承,那么就使得这两个副本变为一个副本(准确地说,两个副本都没有了,因为只存在他们公共基类的那份数据,虚基类得到的不过是使用权)。

写在后面:
  注意虚函数和虚继承的区别:虚函数并没有减少任何数据的存在,仅仅相当于在基类指针层面上建立了一种“调用最靠近对象类型的函数”的机制。然而虚继承则是一种类的继承方式,即,只创建派生类特有部分的数据,继承的数据按需从基类索取。所以虽然他们都是用virtual关键字,但似乎意思上联系不大。
  另外,是用虚继承能够解决的问题相当有限。而且虚继承面临一个开销问题,虽然从继承层面上看,这是一个消除二义性的好方法,而且似乎对编程没有什么副作用。这个道理与虚函数带来的好处与开销权衡问题差不多。一些书希望把这个问题留个程序员自己权衡,一些书则建议一律使用虚函数。不过应该指出,现在硬件设备能力的提升速度似乎在不断削弱我们对开销问题的顾忌(只要算法上不存在问题),所以即使你不打算从现在开始就全盘使用虚函数以及虚继承(而且对于一般的小程序,即使不断加上这些关键字也会使人厌烦吧,况且有些类似乎一辈子也不会成为基类呢?),但是请至少保持这样一个念头,多一种打算,多一条路嘛。
posted on 2011-04-04 17:24 今晚打老虎 阅读(3411) 评论(2)  编辑 收藏 引用 所属分类: 学习笔记

评论

# re: 多重继承、二义性、虚基类(虚继承)之我见 2012-05-29 18:18 自己继承自己
孩子,代码打错了。

class C : public C
自己继承自己?  回复  更多评论
  

# re: 多重继承、二义性、虚基类(虚继承)之我见 2012-07-20 13:14 CL
可以啊,自慰.@自己继承自己
  回复  更多评论
  


只有注册用户登录后才能发表评论。
网站导航: 博客园   IT新闻   BlogJava   博问   Chat2DB   管理