条款32:确定你的public继承塑模出is-a关系
public inheritance (公开继承)意味着“is- a”(是一种)的关系
举个例子,企鹅是一种鸟,或许我们用以下方式描述企鹅与鸟的关系:
class Bird{
public:
virtual void fly();//鸟可以飞
//...
};
class Penguin:public Bird{//企鹅是一种鸟
//...
};
这个继承说企鹅会飞,但实际上并不是。鸟会飞,但有数种鸟不会飞。因此,我们来到以下的继承关系,它塑模出较佳的真实性:
class Bird{
public:
//... 没有声明fly函数
};
class FlyingBird:public Bird{//企鹅是一种鸟
virtual void fly();//鸟可以飞
//...
};
class Penguin:public Bird{//企鹅是一种鸟
//... //... 没有声明fly函数
};
这样的继承体系比原先的设计更能忠实反映我们真正的意思。
另有一种思想派别处理我所谓“所有的鸟都会飞,企鹅是鸟,但是企鹅不会飞,喔欧”的问题,就是为企鹅重新定义fly函数,令它产生一个运行期错误:
void error(const std::string& msg);//定义于另外某处
class Penguin:public Bird{//企鹅是一种鸟
virtual void fly(){ error("Attempt to make a penguin fly!")};
//...
};
很重要的是,你必须认知这里所说的某些东西可能和你所想的不同。这里并不是说“企鹅不会飞”,而是说“企鹅会飞,但尝试那么做是一种错误”。
为了表现“企鹅不会飞,就这样”的限制,你不可以Penguin定义fly函数:
class Bird{
public:
//... 没有声明fly函数
};
class Penguin:public Bird{//企鹅是一种鸟
//... //... 没有声明fly函数
};
现在,如果你试图让企鹅飞,编译器会对你的背信加以谴责:
Penguin p;
p.fly();//错误!
这和采取“令程序于运行期发生错误”的解法极为不同。若以那种做法,编译器不会对p.fly调用式发出任何抱怨。
下面举个例子,class Square应该以public形式继承class Rectangle吗?
可能你会说,当然应该如此,每个人都知道正方形是一种矩形,反之则不一定。
考虑这段代码:
class Rectangle{
public:
virtual void setHeight(int newHeight);
virtual void setWidth(int newWidth);
virtual int height() const;//返回当前值
virtual int width() const;//
};
void makeBigger(Rectangle& r)
{
int oldHeight =r.height();
r.setWidth(r.width()+10);//为r的宽度加10
assert(r.height()==oldHeight);//判断r的高度是否未曾改变
}
显然,上述的assert结果永远为真。因为makeBigger只改变r的宽度,r的高度从未被更改。
现在考虑这段代码,其中使用public继承,允许正方形被视为一种矩形:
class Square:public Rectangle{
//...
};
Square s;
//...
assert(s.height()==s.width());//这对所有正方形一定为真。由于继承,s是一种(is-a)
//矩形,所以我们可以增加其面积
makeBigger(s);
assert(s.height()==s.width());//对所有正方形应该仍然为真
这很明显,第二个assert结果应该永远为真。因为根据定义,正方形的宽度和高度相同。
但现在我们遇上了一个问题,我们如何调解下面各个assert判断式:
1、调用makeBigger之前,s的高度和宽度相同;
2、在makeBigger函数内,s的宽度改变,但高度不变;
3、makeBigger返回之后,s的高度再度和其宽度相同。(注意s是以by reference 方式传给makeBigger,所以makeBigger修改的是s自身,不是s的副本。)
is-a并非是唯一存在于classes之间的关系。另两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据某物实现出)。
请记住:
1、“public继承”意味着is-a。适用于base classes身上的每一件事情也一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。
条款33:避免遮掩继承而来的名称
下面一个base class和derived class类的例子:
class Base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf2();
void mf3();
};
class Derived:public Base{
public:
virtual void mf1();
void mf4();
//...
};
void Derived::mf4(){
//...
mf2();
//...
}
下面是类与函数关系图。
这次让我们重载mf1和mf3,并且添加一个新版的mf3到Derived去。Derived重载了mf3,那是一个继承而来的non-virtual函数。这会使得整个设计立刻显得疑云重重,但为了充分认识继承体系内的“名称可见性”,我们暂时安之如素。
class Base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived:public Base{
public:
virtual void mf1();
void mf3();
void mf4();
//...
};
类的函数关系图:
以此作用域为基础的“”名称遮掩规则“并没有改变,因此base class内所有名为mf1和mf3的函数都被derived class内的mf1和mf3函数遮掩掉了。从名称查找观点来看,Base::mf1和Base::mf3不再被Derived继承!
Derived d;
int x;
//...
d.mf1();//没问题,调用Derived::mf1
d.mf1(x);//错误!因为Derived::mf1遮掩了Base::mf1
d.mf2();//没问题,调用Base::mf2
d.mf3();//没问题,调用Derived::mf3
d.mf3(x);//错误!因为Derived::mf3遮掩了Base::mf3
如今Derived 内的函数mf3遮掩了一个名为mf3但类型不同的Base函数。
这里行为背后的基本理由是为了防止你在程序库或应用框架内建立新的derived class时附带地从疏远的base class继承重载函数。你可以使用using 声明式达成目标:
class Base{
private:
int x;
public:
virtual void mf1()=0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
class Derived:public Base{
public:
using Base::mf1;//让Base class内名为mf1和mf3所有东西在Derived 作用域内都可见
using Base::mf3;
virtual void mf1();
void mf3();
void mf4();
//...
};
现在,继承机制一如往昔地运作:
Derived d;
int x;
//...
d.mf1();//没问题,调用Derived::mf1
d.mf1(x);//没问题,调用Base::mf1
d.mf2();//没问题,调用Base::mf2
d.mf3();//没问题,调用Derived::mf3
d.mf3(x);//没问题,调用Base::mf3
这意味着如果你继承base class并加上重载函数,而你又希望重新定义或覆写(推翻)其中一部分,那么你必须为那些原本会被遮掩的每个名称引入一个using 声明式,否则某些你希望继承的名称会被遮掩。
例如假设Derived以private形式继承Base,而Derived唯一想继承的mf1是那个无参数版本。using声明式在这里派不上用场,因为using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。不我们需要不同的技术,即一个简单的转交函数:
class Base{
public:
virtual void mf1()=0;
virtual void mf1(int);
//...//与前同
};
class Derived:private Base{
public:
virtual void mf1()//转交函数暗自成为inline
{Base::mf1();}
//...
};
//...
Derived d;
int x;
d.mf1();//很好,调用的是Derived::mf1
d.mf1(x);//错误!因为Base::mf3 被遮掩了
inline转交函数的另一个用途是为那些不支持using声明式的老旧编译器另辟一条新路,将继承而得的名称汇入derived class作用域内。
请记住:
1、derived class 内的名称会遮掩base class内的名称。在public继承下从来没有人希望如此。
2、为了让被遮掩的名称再见天日,可以使用using声明式或转交函数。
条款34:区分接口继承和实现继承
表面上直截了当的public继承概念,由两部分组成:函数接口继承和函数实现继承。在继承类的时候,有时候希望只继承成员函数的接口(也就是声明);有时候希望同时继承函数的接口和实现,但又希望能够覆写它们继承的实现;又有时候希望同时继承函数的接口和实现,并且不允许覆写任何东西。
下面我们考虑一个展现绘图程序中的各种几何形状的class继承体系:
class Shape{
public:
virtual void draw() const =0;
virtual void error(const std::string& msg);
int objectID() const;
//...
};
class Rectangle:public Shape{
//...
};
class Ellipse:public Shape{
//...
};
Shape是个抽象class;它的pure virtual函数draw使它成为一个抽象class。所以客户不能够创建Shape class的实体,只能创建其derived class的实体。尽管如此,Shape还是强烈影响了所有以public形式继承它的derived class,因为:
1、成员函数的接口总是会被继承。
首先考虑pure virtual函数draw:
class Shape{
public:
virtual void draw() const =0;
//...
};
pure virtual函数有两个最突出的特性;它们必须被任何“继承了它们”的具象class 重新声明,而且它们在抽象class中通常没有定义。把这两个性质摆在一起,你就会明白:
2、声明一个pure virtual函数的目的是为了让derived class只继承函数接口。
下面调用方法是:调用时明确指出其class名称:
Shape *ps=new Shape;//错误 Shape是抽象的
Shape *ps1=new Rectangle;//没问题
ps1->draw();//调用Rectangle::draw
Shape *ps2=new Ellipse;//没问题
ps2->draw();//Ellipse::draw
ps1->Shape::draw();//Shape::draw
ps2->Shape::draw();//Shape::draw
一如往常,derived class继承其函数接口,但impure virtual函数会提供一份实现代码,derived class可能覆写(override)它。
3、声明简朴的(非纯)impure virtual函数的目的,是让derived class继承该函数的接口和缺省实现。
考虑Shape::error这个例子:
class Shape{
public:
virtual void error(const std::string& msg);
//...
};
你必须支持一个error函数,但如果你不想自己写一个,可以使用Shape class提供的缺省版本。
但是,允许impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。例如,某航空公司有A、B两种飞机,两者都以相同方式飞行,因此设计这样的继承体系:
class Airport{
//...
};//用以表现机场
class Airplane{
public:
virtual void fly(const Airport& destination);
//...
};
void Airplane::fly(const Airport &destination)
{
//缺省代码,将飞机飞至指定目的地
}
class ModelA:public Airplane{
//...
};
class ModelB:public Airplane{
//...
};
为了表示所有飞机都一定能飞,并阐明“不同型号飞机原则上需要不同的fly实现”,Airplane::fly被声明为virtual。然而为了避免ModelA和ModelB中撰写相同代码,缺省飞机行为由Airplane::fly提供,它同时被ModelA和ModelB继承。
现在打算对C型飞机(飞行方式和A、B机型不一样)新增一个class,但是忘记了重新定义fly函数:
class ModelC:public Airplane{
//... //未声明fly函数
};
然后代码有下面的代码:
Airport PDX(...);
Airplane *pa=new ModelC;
//...
pa->fly(PDX);//调用Airplane::fly
飞行方式不一样,这会造成严重问题。
我们做到提供缺省实现给derived class,但除非它们明白要求否则免谈。此间伎俩在于切断“virtual 函数接口”和其“缺省实现”之间的连接。下面是一种做法:
class Airplane{
public:
virtual void fly(const Airport& destination);
//...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport &destination)
{
//缺省代码,将飞机飞至指定目的地
}
若想使用缺省实现(例如ModelA和ModelB),可以在其fly函数中对defaultFly做一个inline调用:
class ModelA:public Airplane{
//...
public:
virtual void fly(const Airport& destination)
{
defaultFly(destination);
}
};
class ModelB:public Airplane{
//...
public:
virtual void fly(const Airport& destination)
{
defaultFly(destination);
}
};
class ModelC:public Airplane{
//...
public:
virtual void fly(const Airport& destination);
};
void ModelC::fly(const Airport &destination)
{
//将C型飞机飞至指定目的地
}
上面的类实现中以不同的函数分别提供接口和缺省实现,例如fly和defaultFly。
因此,提出另外一种方案,利用pure virtual函数必须在derived class中重新声明,但它们也可以拥有自己的实现。下面便是Airplane继承体系如何给pure virtual函数一份定义:
class Airplane{
public:
virtual void fly(const Airport& destination)=0;
//...
};
void Airplane::fly(const int &destination)//pure virtual函数实现
{
//缺省行为,将飞机飞至指定的目的地
}
class ModelA:public Airplane{
public:
virtual void fly(const Airport& destination){
Airplane::fly(destination);
}
//...
};
class ModelB:public Airplane{
public:
virtual void fly(const Airport& destination){
Airplane::fly(destination);
}
//...
};
class ModelC:public Airplane{
public:
virtual void fly(const Airport& destination);
//...
};
void ModelC:fly(const Airport& destination)
{
//将C型飞机飞至指定的目的地
}
这几乎和前一个设计一模一样,只是pure virtual函数Airplane::fly替换了独立函数Airplane::defaultFly。本质上,现在的fly被分割为两个基本要素:其声明部分表现的是接口,其定义部分则表现出缺省行为。
最后我们看看Shape的non-virtual函数objectID:
class Shape{
public:
int objectID() const;
//...
};
如果成员函数是个non-virtual函数,意味着它并不打算在derived class中有不同的行为。
声明non-virtual函数的目的是为了令derived class继承函数的接口及一份强制性实现。
pure virtual函数、simple (impure )virtual函数、non-virtual 函数之间的差异,使你得以精确指定你想要derived class继承的东西:只继承接口、或是继承接口和一份缺省实现,或是继承接口和一份强制实现。
经验不足的class的设计者最常见犯的两个错误。
1、将所有函数声明为non-virtual。
2、将所有函数声明为virtual。
请记住:
1、接口继承和实现继承不同。在public继承之下,derived class总是继承base class的接口。
2、pure virtual函数只具体指定接口继承。
3、简朴的(非纯)impure virtual函数具体指定接口继承及缺省实现继承。
4、non-virtual函数具体指定接口继承以及强制性实现继承。
条款35:考虑virtual函数以外的其它选择
假设你定义一个游戏角色类GameCharacter,提供一个healthValue成员函数,它会返回一个整数,表示人物的健康程度。
class GameCharacter{
public:
virtual int healthValue() const;// 返回人物健康指数;derived class可重新定义它
//...
};
healthValue并未声明为pure virtual,这暗示我们将会有个计算健康指数的缺省算法。
藉由Non-virtual interface 手法实现Template Method手法
保留healthValue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数(例如doHealthValue)进行实际工作:
class GameCharacter{
public:
int healthValue() const// 返回人物健康指数;derived class不重新定义它
{
//... 做一些事前工作
int retVal= doHealthValue();//做真正的工作
//... 做一些事后工作
return retVal;
}
private:
virtual int doHealthValue() const//derived class可重新定义它
{
//...缺省算法,计算健康指数
}
};
这一基本设计,令客户通过public non-virtual成员函数间接调用private virtual函数,称为non-virtual interface(NVI)手法。它是所谓的Template Method 设计模式的一个独特表现形式。把这个non-virtual函数(healthValue)称为virtual 函数的外覆器。
“做一些事前工作”和“做一些事后工作”那些代码在virtual函数进行真正工作之前和之后被调用。
derived class可重新定义继承而来的private virtual函数。
藉由Function pointer 实现Strategy模式
例如我们可能会要求每个人物的构造函数接受一个指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:
//以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);//函数指针
explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc)
:healthFunc(hcf)
{}
int healthValue() const// 返回人物健康指数;
{
return healthFunc(*this);
}
//...
private:
HealthCalcFunc healthFunc;
};
这种做法是常见的Strategy设计模式的简单应用。拿他和“植基于GameCharacter继承体系内之virtual函数”的做法比较,它提供了某些有趣弹性:
同一人物类型之不同实体可以有不同的健康计算函数,例如:
class EvilBadGuy:public GameCharacter{
public:
explicit EvilBadGuy(HealthCalcFunc hcf=defaultHealthCalc)
:GameCharacter(hcf)
{
//...
}
//...
};
int loseHealthQuickly(const GameCharacter&);//健康指数计算函数1
int loseHealthSlowly(const GameCharacter&);//健康指数计算函数2
EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthSlowly);
某已知人物之健康指数计算函数可在运行期变更。例如GameCharacter可提供一个成员函数setHealthCalculator,用来替换当前的健康指数计算函数。
健康指数计算函数不再是GameCharacter继承体系的内的成员函数。例如defaultHealthCalc并未访问EvilBadGuy的non-public成分。
唯一能够解决“想要以non-member函数访问class的non-public成分”办法就是弱化class封装。
藉由tr1::function完成Strategy模式
下面不再使用函数指针,改为tr1::function:
class GameCharacter;//前置声明
//以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter{
public:
// HealthCalcFunc可以是任何“可调用物”,可被调用并接受
//任何兼容于GameCharacter之物,返回任何兼容于int的东西。详情如下
typedef std::tr1::function <const GameCharacter&> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf=defaultHealthCalc)
:healthFunc(hcf)
{}
int healthValue() const// 返回人物健康指数;
{
return healthFunc(*this);
}
//...
private:
HealthCalcFunc healthFunc;
};
**std::tr1::function <const GameCharacter&>**表示tr1::function具现体的目标签名式。这个签名代表的函数是“接受一个reference指向const GameCharacter,并返回Int”。这个tr1::function类型(也就是我们所定义的HealthCalcFunc类型)产生的对象可以持有(保存)任何与此签名式兼容的可调用物。所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为Int。
以下有几种类似的方式:
short calcHealth(const GameCharacter&);//健康计算函数,注意其返回类型为non-int
struct HealthCalculator{//为计算健康而设计的函数对象
int operator()(const GameCharacter&) const
{
//...
}
};
class GameLevel{
public:
float health(const GameCharacter&) const;//成员函数,用以计算健康
//注意其返回类型为non-int
//...
};
class EvilBadGuy:public GameCharacter{//同前
//...
};
class EyeCandyCharacter:public GameCharacter{//另一个人物类型;假设其构造函数与EvilBadGuy同
//...
};
EvilBadGuy ebg1(calcHealth);//人物1,使用某个函数计算健康指数
EyeCandyCharacter ecc1(HealthCalculator());//人物2,使用某个函数对象计算健康指数
//...
GameLevel currentLevel;
EvilBadGuy ebg2(std::tr1::bind(&GameLevel::health,currentLevel,_1));//人物3,使用某个成员函数计算健康指数
上面的bind行为:指出ebg2的健康计算函数应该总是以currentLevel作为GameLevel对象。
古典的Strategy模式
设计模式的一般设计UML图,如下所示:
这张图的大概意思是:GameCharacter是某个继承体系的根类,体系中的EvilBadGuy和EyeCandyCharacter都是derived class;HealthCalcFunc是另一个继承体系的根类,体系中的SlowHealthLoser和FastHealthLoser都是derived class,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。
下面是对应的代码骨干:
class GameCharacter;//前置声明
class HealthCalcFunc{
public:
//...
virtual int calc(const GameCharacter& gc) const
{
//...
}
//...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter{
public:
explicit GameCharacter(HealthCalcFunc* hcf=&defaultHealthCalc)
:pHealthCalc(hcf)
{}
int healthValue() const// 返回人物健康指数;
{
return pHealthCalc->calc(*this);
}
//...
private:
HealthCalcFunc *pHealthCalc;
};
这里提供“将一个既有的健康算法纳入使用”的可能性——只要HealthCalcFunc继承体系的添加一个derived class即可。
我们验证过的几个替代方案:
1、使用non-virtual interface(NVI)手法,那是Template Method 设计模式的一种特殊形式。它以public non-virtual 成员函数包裹降低访问性(private或protected)的virtual函数。
2、将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
3、以tr1::function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。
4、将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。
请记住:
1、vritual函数的替代方案包括NVI手法Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
2、将机能从成员函数转移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
3、tr1::function对象的行为就像一般函数指针。这样的对象可接纳“与给定之目标签名式兼容”的所有可调用物。
条款36:绝不重新定义继承而来的non-virtual函数
假设有以下代码
class B{
public:
void mf();
//...
};
class D:public B{
//...
};
//虽然我们对B ,D和mf一无所知,但面对一个类型为D的对象x;
D x;
//如果以下行为:
B* pB=&x;//获得一个指针指向x
pB->mf();//经由该指针调用mf
//异与以下行为
D* pD=&x;//获得一个指针指向x
pD->mf();//经由该指针调用mf
pB和pD虽然都指向mf,但可能结果不一样,因为两个类的mf函数可能不一样。
如果mf是个non-virtual函数而D有自己定义的mf版本:
class D:public B{
//...
void mf();//遮掩了B::mf
};
pB->mf();//经由该指针调用B::mf
pD->mf();//经由该指针调用D::mf
non-virtual函数如B::mf和D::mf都是静态绑定,执行的mf版本与其定义时指向的类有关。
另外一方面,virtual函数是动态绑定,pB和pD都指向x,因此都会调用D::mf函数。
如果mf是non-virtual函数,当mf被调用,任何一个D对象都可能表现出B或D的行为:决定因素不在对象自身,而在于“指向对象之指针”当初声明的类型。
public意味着is-a关系,在class内声明一个non-virtual函数会为该class建立起一个不变性,凌驾其特异性。将这个观点施行于两个class B和D以及non-virtual成员函数B::mf身上,那么:
1、适用于B对象的每一件事,也适用于D对象,因为每个D对象都是一个B对象;
2、B的derived class一定会继承mf的接口与实现,因为mf是B的一个non-virtual函数。
任何情况下都不该重新定义一个继承而来的non-virtual函数。
请记住:
绝不要重新定义继承而来的non-virtual函数。
条款37:绝不重新定义继承而来的缺省参数值
virtual函数系动态绑定,而缺省参数值却是静态绑定。
//一个用以描述几何形状的class
class Shape{
public:
enum ShapeColor{Red,Green,Blue};
//所有形状都必须提供一个函数,用来绘出自己
virtual void draw(ShapeColor color=Red) const=0;
//...
};
class Rectangle:public Shape{
public:
//注意,赋予不同的缺省参数值。这是真糟糕
virtual void draw(ShapeColor color=Green) const;
//...
};
class Circle:public Shape{
public:
//注意,赋予不同的缺省参数值。这是真糟糕
virtual void draw(ShapeColor color) const;
//请注意,以上这么写则当客户以对象调用此函数,一定要指定参数值
//因为静态绑定下这个函数并不从其base继承缺省参数值。
//但若以指针(或reference)调用此函数,可以不指定参数值,
//因为动态绑定下这个函数会从其base继承缺省参数值。
//...
};
这个继承体系图示如下:
现在考虑这些指针:
Shape* ps;//静态类型为Shape
Shape* pc=new Circle;//静态类型为Shape
Shape* pr=new Rectangle;//静态类型为Shape
本例中,ps,pc,pr都被声明为pointer-to-Shape类型,所以它们都以它为静态类型。注意,无论它们真正指向什么,它们的静态类型都是Shape*。
对象的所谓动态类型则是指“目前所指对象的类型”。也就是,动态类型可以表现出一个对象将会有什么行为。以上例而言,pc的动态类型是Circle* ,pr的动态类型是Rectangle* ,ps没有动态类型,因为它尚未指向任何对象。
动态类型一如其名称所示,可在程序执行过程中改变(通常是经由赋值动作):
ps = pc;//ps的动态类型如今是Circle*
ps = pr;//ps的动态类型如今是Rectangle*
virtual函数系动态绑定而来,意思是调用一个virtual函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个动态类型:
pc->draw(Shape::Red);//调用Circle::draw(Shape::Red)
pr->draw(Shape::Red);//调用Rectangle::draw(Shape::Red)
Rectangle::draw函数的缺省参数值应该是Green,但由于pr的静态类型是Shape * ,所以此一调用的缺省参数值来自Shape class 而非Rectangle class!
假如同时提供缺省值给base 和devived class的用户。如下:
//一个用以描述几何形状的class
class Shape{
public:
enum ShapeColor{Red,Green,Blue};
//所有形状都必须提供一个函数,用来绘出自己
virtual void draw(ShapeColor color=Red) const=0;
//...
};
class Rectangle:public Shape{
public:
//注意,赋予不同的缺省参数值。这是真糟糕
virtual void draw(ShapeColor color=Red) const;
//...
};
以上代码重复带着相依性,造成重复定义一个继承而来的缺省参数值。
以下替代方法是NVI(non-virtual interface)手法:令base class内的一个public non-virtual函数调用private virtual函数,后者可被derived class 重新定义。这里我们可以让non-virtual函数指定缺省参数,而private virtual 函数负责真正的工作:
//一个用以描述几何形状的class
class Shape{
public:
enum ShapeColor{Red,Green,Blue};
//所有形状都必须提供一个函数,用来绘出自己
void draw(ShapeColor color=Red) const{//如今它是Non -virtual
doDraw(color);//调用一个virtual
}
//...
private:
virtual void doDraw(ShapeColor color)const =0;//真正的工作在此完成
};
class Rectangle:public Shape{
public:
//...
private:
virtual void doDraw(ShapeColor color)const ;//无须指定缺省参数值
};
由于non-virtual函数应该绝对不被derived class 覆写,这个设计很清楚地使得draw函数的color缺省参数值总是Red 。
请记住:
1、绝不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定,而virtual函数
条款38:通过复合塑模has-a或“根据某物实现出”
复合是一种类型之间的关系,当某种类型的对象内含它种类型的对象,便是这种关系。例如:
class Address{//某人的住址
//...
};
class PhoneNumber
{
};
class Person
{
std::string name;//合成成分物
Address address;//同上
PhoneNumber voiceNumber;//同上
PhoneNumber faxNumber;//同上
};
Person对象由string,Address,PhoneNumber构成。上述的Person class示范has-a关系。Person有一个名称,一个地址,以及语音和传真电话。
比较麻烦的是区分is-a(是一种)和is-implemented-in-terms-of(根据某物实现出)这两种对象关系。
以下是,is-implemented-in-terms-of(根据某物实现出)这种对象关系的代码实现。
set的底层采用linked list,而标准程序库有一个list template ,因此下面是利用list对象实现set。
template<class T>
class Set
{
public:
bool member(const T& item) const;
void insert(const T& item);
void remove(const T& item);
std::size_t size() const;
private:
std::list<T> rep;
};
template<typename T>
bool Set<T>::member(const T& item) const
{
return std::find(rep.begin(),rep.end(),item)!=rep.end();
}
template<typename T>
void Set<T>::insert(const T& item)
{
if(!member(item)) rep.push_back(item);
}
template<typename T>
void Set<T>::remove(const T& item)
{
typename std::list<T>::iterator it=std::find(rep.begin(),rep.end(),item);
if(it!=rep.end())
rep.erase(it);
}
template<typename T>
std::size_t Set<T>::size() const
{
return rep.size();
}
请记住:
1、复合的意义和public继承完全不同
2、在应用领域,复合意味着has-a(有一个)。在实现领域,复合意味着is-implemented-in-terms-of(根据某物实现出)。
条款39:明智而审慎地使用private继承
如果class之间的继承关系是private,编译器不会自动将一个derived class对象转换为base class对象。由private base class继承而来的所有成员,在derived class中都会变成private 属性,纵使它们在base class中原本是protected 或public 属性。
private继承意味只有实现部分被继承,接口部分应略去。
private 继承意味着is-implemented-in-terms-of(根据某物实现出),尽可能使用复合,必要时才使用private继承。
以下是一个Timer对象,可调整为以我们需要的任何频率滴答前进,每一次滴答就调用某个virtual函数。我们可以重新定义那个virtual函数,让后者取出Widget的当时状态。
为了让Widget重新定义Timer内的virtual函数,Widget必须继承自Timer。但public继承在此例并不适当,因为Widget并不是个Timer。我们必须以private形式继承Timer。
class Timer
{
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;//定时器每滴答一次,此函数就被自动调用一次
};
class Widget:private Timer
{
private:
virtual void onTick() const;//查看Widget的数据,等等
//...
};
private继承并非绝对必要,我们尝试以复合取而代之。只要在Widget内声明一个嵌套式private class,后者以public 形式继承Timer 并重新定义onTick ,然后放一个这种类型的对象于Widget 内。
class Widget
{
private:
class WidgetTimer:public Timer
{
virtual void onTick() const;
//...
};
WidgetTimer timer;
//...
};
选择public继承加复合的方式的原因是:
1、设计Widget使它得以拥有derived class,但同时你可能会想阻止derived class重新定义onTick。如果Widget继承自Timer ,上面的想法就不可能实现,即使是private继承也不可能。但如果WidgetTimer 是Widget内部的一个private 成员并继承Timer, Widget的derived class 将无法取用WidgetTimer,因此无法继承它或重新定义它的virtual函数。
2、将Widget的编译依存性降至最低。
private继承主要用于“当一个意欲成为derived class者想访问一个一个意欲成为base class者的protected成分,或为了重新定义一个或多个virtual函数”。但这时候两个class之间的概念关系其实就是is-implemented-in-terms-of(根据某物实现出)而非is-a。
当你定义一个empty class的时候,如下:
class Empty{};//没有数据,所以其对象应该不使用任何内存
class HoldsAnInt//应该只需要一个Int空间
{
private:
int x;
Empty e;//应该不需要任何内存
};
你会发现sizeof(HoldsAnInt) > sizeof(Int) ,一个Empty成员变量竟然要求内存。在大多数编译器中 sizeof(Empty) 获得1,因为面对“大小为零之独立(非附属对象)”,通常C++官方勒令默默安插一个char 到空对象内。
如果你继承Empty,而不是内含一个那种类型的对象。
class HoldsAnInt:private Empty
{
private:
int x;
};
几乎可以确定sizeof(HoldsAnInt) == sizeof(Empty) ,这是所谓的EBO(empty base optimization:空白基类最优化)
当你面对“并不存在is-a关系”的两个class,其中一个需要访问另一个的protected成员,或需要重新定义其一或多个virtual 函数,private继承极有可能成为正统设计策略。
请记住:
1、private 继承意味着is-implemented-in-terms-of(根据某物实现出)。它通常比复合的级别低。但是当derived class 想要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的。
2、和复合不同,private继承可以造成empty base 最优化。这对致力于“对象尺寸最小化”的程序库开发者而言,可能很重要。
条款40:明智而谨慎地使用多重继承
下面一个多重继承的例子:
class BorrowableItem{//图书馆允许你借某些东西
public:
void checkOut();//离开时检查
//...
};
class ElectronicGadget{//
private:
void checkOut();//执行自我检测,返回是否测试成功
//...
};
class MP3Player://注意这里的多重继承
public BorrowableItem,//某些图书馆愿意借出MP3
public ElectronicGadget
{
//。。。
}; //这里的class 定义不是我们关心的重点
MP3Player mp;
mp.checkOut();//歧义,调用的是哪个checkOut?
C++用来解析重载函数调用的规则:在看到是否有个函数可取用之前,C++首先确认这个函数对此调用之言是最佳匹配。找出最佳匹配函数后才检验其可取用性。本例的两个checkOut有相同的匹配程度,没有所谓的最佳匹配。
为了解决歧义,你必须明确指出调用的哪一个base class内的函数:
mp.BorrowableItem::checkOut();
当然你可以明确调用mp.ElectronicGadget::checkOut(),但你会获得“尝试调用private 成员函数”的错误。
多重继承还会出现“钻石型多重继承”,如下:
class File{
//。。。
};
class InputFile:public File{
// 。。。
};
class OutputFile:public File{
// 。。。
};
class IOFile:public InputFile,
public OutputFile
{
// 。。。
};
类型关系如下:
上面这种关系会造成继承的父类的成员变量出现歧义。
因此,采用virtual 继承,代码例子如下:
class File{
//。。。
};
class InputFile:virtual public File{
// 。。。
};
class OutputFile:virtual public File{
// 。。。
};
class IOFile:public InputFile,
public OutputFile
{
// 。。。
};
使用virtual 继承的那些class 所产生的对象往往比使用Non-virtual 继承的兄弟们体积大,访问virtual base class 的成员变量时,也比访问non-virtual base class成员变量慢。
virtual base 的初始化责任是由继承体系中最底层(most derived )class负责,这按时(1)class 若派生自virtual base 而需要初始化,必须认知其virtual base ——不论那些base 距离多远,(2)当一个新的derived class加入继承体系中,它必须承担其virtual base (不论直接或间接)的初始化责任。
对于使用virtual 继承:1、非必要不使用virtual base ,平时尽可能使用non-virtual继承。2、如果必须使用base class ,尽可能避免在其中放置数据。
下面有一个IPerson的接口类。
class IPerson
{
public:
virtual ~IPerson();
virtual std::string name() const=0;
virtual std::string birthDate() const=0;
};
为了创建一些可被当做IPerson来使用的对象,IPerson的客户使用工厂函数将“派生自IPerson的具象class”实体化。
//factory function(工厂函数),根据一个独一无二的数据库ID创建一个Person对象
//条款18告诉你为什么返回类型不是原始指针
std::tr1::shared_ptr<IPerson> makePerson(DatabaseID personIdentifier);
//这个函数从使用者手上取得一个数据库ID
DatabaseID askUserForDatabaseID();
DatabaseID id(askUserForDatabaseID());
std::tr1::shared_ptr<IPerson> pp(makePerson(id));
//创建一个对象支持Iperson接口,藉由Iperson成员函数处理*pp
下面有一个数据库相关class,PersonInfo类。提供IPerson所需要的实质东西:
class PersonInfo
{
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const=0;
//。。。
private:
virtual const char* valueDelimOpen() const; //详下
virtual const char* valueDelimClose() const; //详下
//。。。
};
const char* PersonInfo::theName() const
{
//保留缓冲区给返回值使用:由于缓冲区是static 因此会被自动初始化为 “全部都是0”
static char value[Max_Formatted_Field_Value_Length];
//写入起始符号
std::strcpy(value,valueDelimOpen());
//现在将 value 内的字符串添附到这个对象的name成员变量中(小心,避免缓冲区超限)
//写入结尾符号
std::strcat(value,valueDelimClose());
return value;
}
const char* PersonInfo::valueDelimOpen() const //详下
{
return "[";//缺省的起始符号
}
const char* PersonInfo::valueDelimClose() const //详下
{
return "]";//缺省的结尾符号
}
令CPerson以private 形式继承PersonInfo。将 “Public继承自某接口”和 “private继承自某实现”结合在一起。
class IPerson//这个class 指出需要实现的接口
{
public:
virtual ~IPerson();
virtual std::string name() const=0;
virtual std::string birthDate() const=0;
};
class DatabaseID
{
//。。。
};
class PersonInfo //这个类用若干有用函数,可用以实现IPerson 接口
{
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const=0;
virtual const char* valueDelimOpen() const; //详下
virtual const char* valueDelimClose() const; //详下
//。。。
};
class CPerson:public IPerson,private PersonInfo//注意多重继承
{
public:
explicit CPerson(DatabaseID pid);
virtual ~CPerson();
virtual const char* theName() const//实现必要的IPerson成员函数
{
return PersonInfo::theName();
}
virtual const char* birthDate() const
{
return PersonInfo::theBirthDate();
}
private:
const char* valueDelimOpen() const { return "";}//重新定义继承而来的virtual“界限函数”
const char* valueDelimClose() const { return "";} //详下
};
UML图如下
请记住:
1、多重继承比单一继承复杂。它可能导致新的歧义,以及对virtual 继承的需要。
2、virtual继承会增加大小、速度、初始化(及赋值)复杂度等等成本。如果virtual base class不带任何数据,将是最具实用价值的情况。
3、多重继承的确有正当用途。其中一个情节涉及“public 继承某个interface class”和“private 继承某个协助实现的class ”的两相组合。