现在的位置: 首页 > 综合 > 正文

多重继承——《C++编程风格》读书笔记(七)

2017年12月16日 ⁄ 综合 ⁄ 共 9774字 ⁄ 字号 评论关闭

     当我们希望在类之间对多个“是一种”(is-a)的关系进行建模时需要用到多重继承,例如,一艘游艇(houseboat)既是一条船(boat),也可以是一座房子(houseboat)。然而多重继承很难以被高效的使用;多重继承机制有时带来的作用也是有限的。分清什么时候需要用到多重继承,什么时候不要,也是一个难点。

 

1.多重继承的二义性

 

 

 


 

程序1 带有二义性的多重继承示例

 

 


 

 

    基类成员函数Base1::g()和Base2::h()都是没有二义性的。当我们通过Derived对象来调用这两个函数时,对于每个函数也都只有一种解释:

 

Derived d;

d.g();     // unambiguous

d.g();    //unambiguous

 

   但如果试图在Derived对象上调用f(),将会引发编译期的二义性错误:

 

d.g()      //compile-time error 

    由于函数有着同样的名字而在类中产生二义性,无论这些函数是否是公有成员函数,结果都是一样的。例如,如果一个基类的私有成员和另一基类的公有成员有着同样的名字,也将会产生二义性。也就是说,访问控制并不会影响编译器在作用域规则下对标识符的搜索方式。

 

程序2 访问控制并不能解读二义性


 


 

2.有向无环继承图

 

    在单重继承下,继承层次结构通常可以用类的树状图来描述;在多重继承下,我们需要使用有向无环图(directed acyclic graph)。“有向”意味着所画图中的每条边都有一个方向,用来区分哪一端是基类,哪一端是派生类。我们可以在画图时约定,基类总是出现在派生类的上方。“无环”意味着继承图中没有循环,这样,一个类就永远也不会成为自己的基类,即使间接的也不行。

 

程序3 共同的基类

 


 

 


 

 对程序3中类的结构进行研究可知:

  1. 派生类中的对象都包含每个基类的所有成员。因此Bottom包含两个Top部分,它可以通过两种不同的途径到达top部分,这就产生了一个潜在的二义性。
  2. 在默认的继承机制(public)中,所有从基类继承而来的数据成员都保持着独立的副本。
  3. 当Top作为Left和Right的虚基类(virtual base class)时,Bottom对象只包含一个Top部分,虚基类使得在每个派生类对象中只有唯一的基类部分。(在基类名字前加virtual和在类的成员函数前加virtual是不相干的)。

3. 分析虚基类

   

     将程序3的代码补充完整,并添加一些方便我们分析的语句。首先将Top作为非虚基类,然后再将Top做为虚基类。

 

程序4 在非虚基类下的赋值运算

 


 

 


 运行结果:

Left object assignment
        0012FF78 Left::operator=
        0012FF78 Top::operator=
Right object assignment
        0012FF68 Right::operator=
        0012FF68 Top::operator=
Bottom object assignment
        0012FF50 Left::operator=
        0012FF50 Top::operator=
        0012FF58 Right::operator=
        0012FF58 Top::operator=

 

    在Left和Right对象的赋值运算中,程序调用了Top::operator=来对派生类对象中的Top部分进行赋值。而对于在Bottom对象的赋值运算中,由于Bottom类并没有定义自己的operator=,因此调用编译器默认生成的Bottom::operator=,在编译器生成的Bottom::operator=中,将依次调用Left::operator=和Right::operator=对Bottom对象的Left部分和Right部分进行赋值,同时对其Top部分重复进行了两次相同的赋值。

 

程序5 虚基类下的赋值运算



运行结果:

Left object assignment
        0x22ff44 Left::operator=
        0x22ff4c Top::operator=
Right object assignment
        0x22ff2c Right::operator=
        0x22ff34 Top::operator=
Bottom object assignment
        0x22ff0c Left::operator=
        0x22ff1c Top::operator=
        0x22ff14 Right::operator=
        0x22ff1c Top::operator=

 

    观察结果并分析:程序5使用虚基类Top的唯一好处是每个Bottom对象只有一个Top部分,但是仍然不能解决Top部分重复进行两次相同赋值的问题。分析发现,在Left部分和Right部分的operator=中必须调用Top::operator=,因此对于Left对象和Right对象来说,所进行的赋值运算都是正确的。其中一个解决方案就是在Left和Right中增加一个赋值成员函数。它所执行的工作只是对类的自身的成员进行赋值,而不会去调用虚基类的operator=。命名为assignLocal。于是,我们就可以在Bottom对象中引入Bottom::operator=以提供不重复的赋值行为,而不是用编译器默认的赋值函数。

 

程序6 改进的虚基类下的赋值运算



运行结果:

Left object assignment
        0012FF74 Left::operator=
        0012FF7C Top::operator=
        0012FF74 Left::assignLocal
Right object assignment
        0012FF5C Right::operator=
        0012FF64 Top::operator=
        0012FF5C Right::assignLocal
Bottom object assignment
        0012FF3C Bottom::operator=
        0012FF4C Top::operator=
        0012FF3C Left::assignLocal
        0012FF44 Right::assignLocal

   现在,Bottom对象的赋值运算中,Top::operator=只会执行一次了。

 

4. 使用虚基类

   

    我们设计了一个继承层次:基类DomesticAnimal(家畜),类Cow(母牛)和Buffalo(水牛)派生于类DomesticAnimal,而类Beefalo(一种由肉用黄牛与北美洲野牛杂交而成的肉用牛,叫做皮弗娄牛)是从Cow和Buffalo派生出来的。

 

程序7 DomesticAnimal、Cow、Buffalo和Beefalo



运行结果:

The cow has the properties:
The weight = 1400
The price = $375
The color = Black and white
The Beefalo has the properties:
The weight = 1700
The price = $525
The color = Brown and black
The cow has the properties:
The weight = 1700
The price = $525
The color = Brown and black

 

    程序分析:DomesticAnimal是有意义的,Cow和Buffalo都是DomesticAnimal的特化形式。然而,在继承关系中存在着一个缺陷:Cow和Buffalo都是Beefalo的基类,若b是一个Beefalo对象,那么b.Cow::print()将是合法的函数调用,这表示Beefalo对象也可被认为是一个Cow对象。除了能够使Beefalo对象通过显式地调用基类虚函数来伪装身份外,将Beefalo从Cow继承下来没有其他的作用,因此,我们可以将Beefalo直接从DomesticAnimal中继承下来。

 

程序8 去掉虚基类



     生物学上的继承和C++中的继承有着重要的区别:生物学上的后代从它们的父母双方中“继承”特征,但这并不意味着在程序设计中,类继承就是对这种关系建模的好方法。如果用继承来对繁殖关系进行建模,那么我们就需要使每个对象都成为一个类,并且这个类只能被实例化一次。程序7就是由于没理解这两者的关系编写而成的。

 

    对于生物学上的继承,更好的模型是将每个物种都作为一个类,而每个动物都是从物种类中实例化出来的对象。后代对象则可以通过基因信息来进行初始化,其中这个基因信息时期父母基因信息的组合。在软件中对生物学上的继承关系进行建模时,它所对应的应该是对象与对象的关系,而不是类与类的关系。

 

参考文献:《c++编程风格》(c++ programming style )作者 Tom Cargill 译者 聂雪军 机械工业出版社2007.1

 

 

 

抱歉!评论已关闭.