您的位置:首页 > 编程语言 > C语言/C++

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

2010-03-20 15:25 309 查看
     当我们希望在类之间对多个“是一种”(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中类的结构进行研究可知:

派生类中的对象都包含每个基类的所有成员。因此Bottom包含两个Top部分,它可以通过两种不同的途径到达top部分,这就产生了一个潜在的二义性。

在默认的继承机制(public)中,所有从基类继承而来的数据成员都保持着独立的副本。

当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

 

 

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息