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

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

 

1.多重继承的二义性

 

 

 

 

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

 

class Base1{ //.... public: void f(); void g(); //.... }; class Base2{ //.... public: void f(); void h(); //.... }; class Derived:public Base1,public Base2{ //.... };

 

 

 

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

 

Derived d;

d.g();     // unambiguous

d.g();    //unambiguous

 

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

 

d.g()      //compile-time error 

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

 

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

class Base1{ //.... public: void f(); void g(); //.... }; class Base2{ void f(); //.... public: void h(); //.... }; class Derived:public Base1,public Base2{ //.... }; int main() { Derived d; d.f();//compile-time errorr:ambiguous f //... return 0; }

 

 

2.有向无环继承图

 

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

 

程序3 共同的基类

 

 

class Top { int x; //... }; class Left:public Top { int y; //... }; class Bottom:public Left,public Right { //... };

 

 

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

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

3. 分析虚基类

   

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

 

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

 

 

#include <iostream> using namespace std; void trace(const char *funcName,void *objAddr) { cout<< "/t" << objAddr << " " << funcName << "/n"; } class Top { int x; public: Top & operator = (const Top&); }; Top& Top::operator=(const Top& rhs) { trace("Top::operator=",this); if(this != &rhs) x = rhs.x; return *this; } class Left:public Top { int y; public: Left& operator=(const Left&); }; Left& Left::operator=(const Left& rhs) { trace("Left::operator=",this); if(this != &rhs) { this->Top::operator=(rhs); y = rhs.y; } return *this; } class Right:public Top { int z; public: Right& operator=(const Right& ); }; Right& Right::operator=(const Right& rhs) { trace("Right::operator=",this); if(this != &rhs) { this->Top::operator=(rhs); z = rhs.z; } return *this; } class Bottom:public Left,public Right { }; int main() { Left L1,L2; Right R1,R2; Bottom B1,B2; cout << "Left object assignment/n"; L1 = L2; cout << "Right object assignment/n"; R1 = R2; cout << "Bottom object assignment/n"; B1 = B2; return 0; }

 

 运行结果:

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 虚基类下的赋值运算

#include <iostream> using namespace std; void trace(const char *funcName,void *objAddr) { cout<< "/t" << objAddr << " " << funcName << "/n"; } class Top { int x; public: Top & operator = (const Top&); }; Top& Top::operator=(const Top& rhs) { trace("Top::operator=",this); if(this != &rhs) x = rhs.x; return *this; } class Left:public virtual Top { int y; public: Left& operator=(const Left&); }; Left& Left::operator=(const Left& rhs) { trace("Left::operator=",this); if(this != &rhs) { this->Top::operator=(rhs); y = rhs.y; } return *this; } class Right:public virtual Top { int z; public: Right& operator=(const Right& ); }; Right& Right::operator=(const Right& rhs) { trace("Right::operator=",this); if(this != &rhs) { this->Top::operator=(rhs); z = rhs.z; } return *this; } class Bottom:public Left,public Right { }; int main() { Left L1,L2; Right R1,R2; Bottom B1,B2; cout << "Left object assignment/n"; L1 = L2; cout << "Right object assignment/n"; R1 = R2; cout << "Bottom object assignment/n"; B1 = B2; return 0; }

运行结果:

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 改进的虚基类下的赋值运算

#include <iostream> using namespace std; void trace(const char *funcName,void *objAddr) { cout<< "/t" << objAddr << " " << funcName << "/n"; } class Top { int x; public: Top & operator = (const Top&); }; Top& Top::operator=(const Top& rhs) { trace("Top::operator=",this); if(this != &rhs) x = rhs.x; return *this; } class Left:public virtual Top { int y; protected: void assignLocal(const Left& rhs); public: Left & operator=(const Left &); }; void Left::assignLocal(const Left& rhs) { trace("Left::assignLocal",this); y = rhs.y; } Left& Left::operator=(const Left& rhs) { trace("Left::operator=",this); if(this != &rhs) { this->Top::operator=(rhs); assignLocal(rhs); } return *this; } class Right:public virtual Top { int z; protected: void assignLocal(const Right&); public: Right& operator=(const Right& ); }; void Right::assignLocal(const Right& rhs) { trace("Right::assignLocal",this); z = rhs.z; } Right& Right::operator=(const Right& rhs) { trace("Right::operator=",this); if(this != &rhs) { this->Top::operator=(rhs); assignLocal(rhs); } return *this; } class Bottom:public Left,public Right { public: Bottom & operator=(const Bottom &); }; Bottom & Bottom::operator=(const Bottom &rhs) { trace("Bottom::operator=",this); if(this != &rhs) { Top::operator=(rhs); Left::assignLocal(rhs); Right::assignLocal(rhs); } return *this; } int main() { Left L1,L2; Right R1,R2; Bottom B1,B2; cout << "Left object assignment/n"; L1 = L2; cout << "Right object assignment/n"; R1 = R2; cout << "Bottom object assignment/n"; B1 = B2; return 0; }

运行结果:

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

#include <stdlib.h> #include <iostream> using namespace std; class DomesticAnimal { protected: int weight; float price; char color[20]; public: DomesticAnimal(void) { weight = 0; price = 0; strcpy(color,"None"); } DomesticAnimal(int aWeight,float aPrice,char *aColor) { weight = aWeight; price = aPrice; strcpy(color,aColor); } virtual void print(void) { cout << "The weight = " << weight << "/n"; cout << "The price = $" << price << "/n"; cout << "The color = " << color << "/n"; } }; class Cow:public virtual DomesticAnimal { public: Cow(void) { } Cow(int aWeight,float aPrice,char *aColor) { weight = aWeight; price = aPrice; strcpy(color,aColor); } void print(void) { cout << "The cow has the properties:/n"; DomesticAnimal::print(); } }; class Buffalo:public virtual DomesticAnimal { public: Buffalo(void) { } Buffalo(int aWeight,float aPrice,char *aColor) { weight = aWeight; price = aPrice; strcpy(color,aColor); } void print(void) { cout << "The Buffalo has the properties:/n"; DomesticAnimal::print(); } }; class Beefalo:public Cow,public Buffalo { public: Beefalo(int aWeight,float aPrice,char *aColor) { weight = aWeight; price = aPrice; strcpy(color,aColor); } void print(void) { cout << "The Beefalo has the properties:/n"; DomesticAnimal::print(); } }; int main() { Cow aCow(1400,375.0,"Black and white"); Beefalo aBeefalo(1700,525.0,"Brown and black"); DomesticAnimal& myCow = aCow; DomesticAnimal& myBeefalo = aBeefalo; myCow.print(); myBeefalo.print(); aBeefalo.Cow::print();//Beefalo对象也可以被认为是一个Cow对象 return 0; }

运行结果:

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 去掉虚基类

#include <stdlib.h> #include <iostream> using namespace std; class DomesticAnimal { int weight; float price; char color[20]; public: DomesticAnimal(void) { weight = 0; price = 0; strcpy(color,"None"); } DomesticAnimal(int aWeight,float aPrice,char *aColor) { weight = aWeight; price = aPrice; strcpy(color,aColor); } virtual void print(void) { cout << "The weight = " << weight << "/n"; cout << "The price = $" << price << "/n"; cout << "The color = " << color << "/n"; } }; class Cow:public DomesticAnimal { public: Cow(int aWeight,float aPrice,char *aColor) : DomesticAnimal(aWeight,aPrice,aColor) {} void print(void) { cout << "The cow has the properties:/n"; DomesticAnimal::print(); } }; class Buffalo:public DomesticAnimal { public: Buffalo(int aWeight,float aPrice,char *aColor) : DomesticAnimal(aWeight,aPrice,aColor) {} void print(void) { cout << "The Buffalo has the properties:/n"; DomesticAnimal::print(); } }; class Beefalo:public DomesticAnimal { public: Beefalo(int aWeight,float aPrice,char *aColor) : DomesticAnimal(aWeight,aPrice,aColor) {} void print(void) { cout << "The Beefalo has the properties:/n"; DomesticAnimal::print(); } }; int main() { Cow aCow(1400,375.0,"Black and white"); Beefalo aBeefalo(1700,525.0,"Brown and black"); DomesticAnimal& myCow = aCow; DomesticAnimal& myBeefalo = aBeefalo; myCow.print(); myBeefalo.print(); return 0; }

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

 

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

 

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

 

 

 

你可能感兴趣的:(编程,C++,properties,object,读书,Class)