六.继承与面向对象设计
“继承”可以是单一继承或多重继承,每一个继承链接可以是public,protected或private,也可以是virtual或non-virtual,pure virtual。然后是成员函数的各个选项:virtual?non-virtual?pure virtual?以及成员函数和其他语言特性的交互影响:缺省参数值与virtual函数有什么交互影响?继承如何影响C++的名称查找规则?设计选项有哪些?如果class的行为需要修改,virtual函数是最佳选择吗?
本章对这些问题全面讲解,还会解释C++不同特性的真正意义。例如,public继承意味着"is-a",virtual函数意味着接口必须被继承,non-virtual函数意味着“接口和实现都必须被继承”。
条款32:确定你的public继承塑模出is-a关系
以C++进行面向对象编程,最重要的一个规则是,public inheritance意味着"is-a"的关系。
试思考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(t.width() + 10);
assert(r.Height() == oldHeight);
}
显然,上述assert永远为真,现在考虑这段代码:
class Square: public Rectangle { ... };
Square s;
...
assert(s.width() == s.height()); //对正方形为真
makeBigger(s); //由于继承,s是矩形,所以可以增加其面积
assert(s.width() == s.height()); //对所有正方形依然为真
显然,第二个assert也永远为真,但出现了一个问题:
调用makeBigger之前,s的高度和宽度相同;
在makeBigger函数内,s的宽度改变,但高度不变。
makeBigger返回之后,s的高度再度和宽度相同。(s是以by-reference传递,因此修改的是自身)。
问题显而易见,本题的困难时某些可施行于矩形的方法却不可施行与正方形身上。但是public继承主张,能够施行于base class对象身上的每件事情都可以施加在derived class对象身上。但是在正方形和矩形的关系上,这样的主张无法实现,因此无法使用public关系继承。
is-a并非是存在于class之间的唯一关系,另两个常见的关系是has-a和is-implementation-in-terms-of.应该了解这些关系的相互差异并直到在C++中如何塑造他们。
请记住
"public继承"意味着is-a关系,适用于base-class身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象。
条款33.避免遮掩继承而来的名称
我们知道,当位于一个derived class成员函数内指涉base class内的某物,实际运作方式是derived class作用域嵌套在base 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();
...
};
//假设derived class内的mf4的实现码部分像这样:
void Derived::mf4()
{
...
mf2();
...
}
当编译器看到mf2(),会首先查找local作用域,若没有收获会继续查找其外围作用域,也就是class Derived覆盖的作用域,还是没有找到任何mf2()函数,之后会再往外围移动,本例为base class,在那里找到后便停止搜索,如果没有发现的话,会继续查找,首先查找内含Base的namespace作用域,最后查找到全局变量作用域。
1)
下面再次考虑前例,这次重载了mf1和mf3,并且添加一个新的mf3在derived去:
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::mf1和Base::mf3不再被Derived class继承:
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 class时附带地从疏远的base classes继承重载函数。实际上如果你正在使用public继承而又不继承那些重载函数,就是违反base和derived classes之间的is-a关系,而条款32说过这种关系是public继承的基石。因此有时你会想推翻C++对“继承而来的名称”的缺省遮掩行为,可以使用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;
using Base::mf3; //推翻默认缺省遮掩行为,让Base内名为mf1和mf3的所有东西在Derived作用域内都可见。
virtual void mf1();
void mf3();
void mf4();
...
};
解决了上述问题。
2)
然而有时候你并不想继承base class的所有函数,这在public继承中是不可能发生,但是在private继承之下却可能是有意义的。在这里使用using是无法完成的,这会使继承而来的给定名称之所有同名函数在derived class中都可见。需要新的技术:即一个简单的转交函数(forwarding function):
class Base{
public:
virtual void mf1() = 0;
virtual void mf1(int);
...
};
class Derived: private Base{
public:
virtual void mf1() //转交函数
{ Base::mf1(); } //默认inline函数
...
};
...
Derived d;
int x;
d.mf1(); //正确,调用的是Derived::mf1;
d.mf1(x); //错误,Base::mf1()被遮掩了
请记住
1.derived class内的名称会遮掩base classes内的名称,在public继承下从来没有人希望如此;
2.为了让被遮掩的名称重见天日,可以使用using声明式或转交函数。
条款34:区分接口继承和实现继承
表面上直截了当的public继承概念,经过更严密的检查之后,发现它由两部分组成:函数接口继承和函数实现继承。
有时你会希望derived classes只继承成员函数的接口;有时你又会希望derived class同时继承函数的接口和实现,但又希望能够重写他们所继承的实现;有时候你希望derived classes同时继承函数的接口和实现且不允许重写任何东西。为了更好的感受上述选择的差异,考虑一个展现绘图程序中各种几何形状的class继承体系:
class Shape{
public:
virtual void draw() const = 0; //纯虚函数使得Shape成为一个抽象类,导致Shape无法创建实体。
virtual void error(const std::string& msg);
int objectID() const;
...
};
class Rectangle: public Shape { ... };
class Ellipse: public Shape{ ... };
虽然客户只能创建Shape的derived classes的实体,但是Shape还是强烈影响了所有以public形式继承它的derived class,因为:
成员函数的接口总是会被继承。 因为public继承是一种is-a关系
1)
Shape class以不同的声明方式声明了三个函数,分别是pure virtual函数,impure virtual函数还有non-virtual函数,这些不同的声明意味着什么?
首先考虑pure virtual函数draw:
class Shape{
public:
virtual void draw( ) const = 0;
...
};
pure virtual函数有两个最突出的特征:他们必须被“任何继承了他们”的具象class重新声明,而且它们在抽象class中通常没有定义。把这两个性质摆在一起就明白:
声明一个pure virtual函数的目的是为了让derived classes只继承函数接口。 这对Shape::draw函数是再合理不过的事情,因为所有Shape对象都应该是可绘的。但是Shape class无法为此函数提供合理的缺省实现,毕竟椭圆形绘制与矩形绘制不一样,需要对具象的derived classes设计者说明“你必须提供一个draw函数,我不会干预你实现它的方式。”
令人意外的是,我们竟然可以为pure virtual函数提供定义,为Shape::draw供应一份实现代码,但调用它的唯一途径是“调用时明确指出其class名称”:
Shape* ps = new 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
这种方法可以实现一种机制,为简朴的impure virtual函数提供更平常更安全的缺省实现。
2)
简朴的impure virtual函数背后的故事和pure virtual函数有点不同。一同往常,derived classes继承其函数接口,但impure virtual函数会提供一份实现代码,derived classes可能覆写它。
声明简朴的impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。 考虑Shape::error这个例子:
class Shape{
public:
virtual void error(const std::string& msg);
...
};
该声明式的意义在于:你必须支持一个error函数,但如果你不想自己写一个,可以使用Shape class提供的缺省版本。但是,允许inmpure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。考虑下面这个例子:
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 { ... };
这是个典型的面向对象设计。两个classes共享一份相同性质,且共同性质被搬到base class中,然后被这两个classes继承。现在假设有不同于A,B飞行方式的飞机C:
class ModelC: public Airplane{
...
};
//然后对该类有如下使用
Airport PDX { ... };
Airplane* pa = new ModelC;
...
pa->fly(PDX); //调用Airplane::fly
这个程序试图以ModelA或者ModelB的飞行方式来飞ModelC。问题不在Airplane::fly有缺省行为,在于ModelC在未提出请求的情况下便继承了该缺省行为。幸运的是可以轻易做到“提供缺省实现给derived classes,但除非它们要求否则免谈。”此种做法在于切断“virtual函数接口”和其"缺省实现"之间的连接,下面是一种做法:
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
缺省行为...
}
请注意,Airplane::fly已成为pure virtual函数,只提供飞行接口,其缺省行为也出现在Airplane class中国,但此次是以独立函数defaultFly的姿态出现。若想使用缺省实现,可以在其fly函数中对defaultFly做一个inline调用,inline函数和virtual函数之间的交互关系是:
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型飞机行为;
}
本段代码中的pure virtual函数迫使ModelC必须提供自己的fly版本。Airplane::defaultFly是一个non-virtual函数,因为没有任何一个derived class应该重新定义此函数。有些人反对以不同的函数分别提供接口和缺省实现,像上述的fly和defaultFly那样,他们担心因过度雷同的函数名称而引起的class命名空间污染问题,但是他们也同意接口和缺省应该分开。应当如何解决这个问题呢。我们可以利用“pure virtual函数必须在derived class中重新声明,但他们也可拥有自己的实现”这一事实。下面是Airplane继承体系如何给pure virtual函数一份定义:
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
}
void Airplane::fly(const Airport& 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)
{
缺省行为;
}
这个设计几乎与前者一模一样,只不过pure virtual函数Airplane::fly替换了独立函数Airplane::defaultFly。本质上,现在fly被分割为两个基本要素:其声明部分表现的是接口,其定义部分表现出的是缺省行为。
3)
最后看看Shape的non-virtual函数objectID:
class Shape {
public:
int objectID( ) const;
...
};
如果成员函数是non-virtual函数,意味是它并不打算在derived classes中有不同的行为,实际上一个non-virtual函数表现的不变性凌驾其特异性。就其自身而言:声明non-virtual函数的目的是为了另derived classes继承函数的接口及一份强制性实现。
4)
pure virtual函数、impure virtual函数以及non-virtual函数之间的差异,使得你可以精确指定你想要derived classes继承的东西:只继承接口、或是继承接口和一份缺省实现、或是继承接口和一份强制性实现。
请记住
1.接口继承和实现继承不同。在public继承之下,derived classes总是继承base class的接口。
2.pure virtual函数只具体指定接口继承。
3.简朴的impure virtual函数具体指定接口继承和缺省实现继承。
4.non-virtual函数具体指定接口继承和强制性实现继承。
条款35:考虑virtual函数以外的其他选择
假设有这样一个类表示游戏人物的健康状态,其中一个成员函数healthValue会返回一个整数表示人物当前的健康状态。由于不同的人物可能以不同的方式计算他们的健康指数,将healthValue声明为virtual函数似乎是再明白不过的做法:
class GameCharacter {
public:
virtual int healthValue() const;
...
};
1)
healthValue并未被声明为pure virtual函数,这暗示我们将会有一个计算健康指数的缺省算法。这是非常明白不过的设计,但是我们或许可以考虑其他解法:使用non-virtual Interface手法实现Template Method模式。
我们从下面这个做法开始:
class GameCharacter {
public:
int healthValue() const //derived classes不重新定义
{
... //事前工作
int retVal = doHealthValue() ;
... //事后工作
return retVal;
}
...
private:
virtual int doHealthValue() const //derived classes可以重新定义
{
...
}
};
这个设计,也就是“令客户通过public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface手法。它是所谓的Template Method设计模式的一个独特表现形式,可以把这个non-virtual函数称为virtual函数的外覆器。NVI手法的一个优点隐身在上述代码注释“做些事情工作”和“做一些事后工作之中”。这些注释用来告诉你当时的代码保证在“virtual函数进行真正工作之前和之后”被调用。这意味着外覆器确保得以在一个virtual函数被调用之前设定好适当场景,并在调用结束之后清理场景。“事前工作”可以包括锁定互斥器、制造运转日志记录、验证class约束条件、验证函数先决条件等等。“事后工作”包括互斥器解除锁定、验证函数的事后条件、再次验证class的约束条件等。NVI手法涉及在derived class内重新定义private virtual函数。NVI手法允许derived class重新定义virtual函数,从而赋予它们如何实现机能的能力,但base clss保留何时调用的权力。
2)
使用Function Pointers实现Strategy模式
考虑下面这个做法:
class GameCharacter; //前置声明
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;
};
这个与第一个做法比较,提供了一些有趣弹性:
1. 同一人物类型之不同实体有不同的健康计算函数。例如:
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);
2. 某已知人物之健康指数函数可以在运行期变更。 即可以提供另外一个成员函数setHealthCalculator替换当前的函数。换句话说,健康指数计算函数不再是GameCharacter继承体系内的成员函数,这些计算函数并未特别访问计算对象的内部成分,例如defaultHealthCalc并未访问EvilBadGuy的non-public成分。如果需要non-public信息进行精确计算,这会带来问题。一般而言,解决这个问题的做法是:弱化class的封装。例如可将non-member的计算函数声明为friends。最后,运用函数指针替换virtual函数,其优点是否能够弥补缺点,是需要根据不同的设计情况而抉择的。
***3. 藉由tr1::function完成Strategy
一旦习惯了templates以及它们对隐式接口的使用,基于函数指针的做法看起来有些死板。为什么要求“健康指数之计算”是个函数,而不能是某种“像函数的东西”?如果一定得是函数,为什么不能是成员函数?为什么一定得返回int而不是任何可被转换为int的类型?
若我们改用类型为tr1::function的对象,就可以取消上述限制。这是因为这样的对象可持有任何可调用物(函数指针,函数对象,成员函数指针),只要其签名式兼容于需求端。如下使用tr1::function:
class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter {
public:
//HealthCalcFunc可以是任何“可调用物”,可被调用并接受任何兼容于GameCharacter的物,返回任何兼容int的东西:
typedef std::tr1::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc): healthFunc(hcf)
{}
int healthValue() const
{
return healthFunc(*this);
}
...
private:
HealthCalcFunc healthFunc;
};
如上,HealthCalcFunc是一个Typedef,用来表现tr1::function的具现化。std::tr1::function<int (const GameCharacter&)>这个签名式代表:接受一个指向const GameCharacter的引用,并返回int。这个类型可以保存任何与该签名兼容的可调用物,所谓兼容指的是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。与前一个设计相比,这个设计几乎相同,唯一不同的是如今GameCharacter持有一个tr1::function对象,相当于指向函数的泛化指针。
此外,下面阅读这段代码:
short calcHealth(const GameCharacter&); //健康计算函数,返回short
struct HealthCalculator { //为计算健康而设计的函数对象
int operator() (const GameCharacter&) const { ... }
};
class GameLevel {
public:
float health(const GameCharacter&) const; //成员函数用于计算健康,返回float
...
};
class EvilBadGuy: public GameCharacter {
...
};
class EyeCandyCharacter: public GameCharacter {
...
};
EvilBadGuy ebg1(calcHealth); //人物1,使用某个函数计算健康指数
EyeCandyCharacter ecc1(HealthCalculator()); //人物2,使用某个函数对象计算健康指数
GameLevel currentLevel;
...
EvilBadGuy ebg2 {
std::tr1::bind(&GameLevel::health, currentLevel, _l) //人物3,使用某个成员函数计算健康指数
};
在对ebg2人物的健康指数计算时,注意到GameLevel::Health表面上看只接受一个参数,实际接受两个参数:reference指向GameCharacter,this所指对象。此处tr1::bind将接受两个参数的GameLevel::health转换为只接受一个参数来计算bg2的健康指数函数。本处的根本重点是:***若以tr1::function替换函数指针,将因此允许客户在计算人物健康指数时使用任何兼容的可调用物。
4)
下面查看古典的Strategy模式在本例中的应用:
class GameCharacter:
class HealthCalFunc {
public:
...
virtual int calc(const GameCharacter& gc) const
{ ... }
...
};
HealthCalcFunc defaultHealthCalc;
class GameCharacter {
public:
explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc): pHealthCalc(phcf)
{}
int healthValue() const
{ return pHealthCalc->calc(*this); }
...
private:
HealthCalcFunc* pHealthCalc;
};
这个设计体系意味着:GameCharacter时某个继承体系的根系,体系中的EvilBadGuy和EyeCandyCharacter都是derived class。HealthCalcFunc是另一个继承体系的根类,体系中的SlowHealthLoser和FastHealthLoser都是derived class,每一个GameCharacter对象都内含一个指针,指向来自HealthCalcFunc继承体系的对象。
5)
本条款的根本忠告是,当你为解决问题而寻找某个设计方法是,不妨考虑virtual函数的替代方案。下面回溯我们之前验证过的几个方案:
- 使用non-virtual interface手法,那是Template Method设计模式的一种特殊形式,它以public non-virtual成员函数包裹较低访问性的non-virtual函数.
- 将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
- 以tr1::funtion成员变量替换virtual函数,因而允许任何使用可调用物搭配一个兼容于需求的签名式,这也是Strategy设计模式的某种形式。
- 将继承体系内的virtual函数替换为另一个继承体系内的virtual函数,这是*Strategy设计模式的传统实现手法。
请记住
1.virtual函数的替代方案包括NVI手法及Strategy设计模式的多种形式。NVI手法自身是一个特殊形式的Template Method设计模式。
2.将机能从成员函数移到class外部函数,带来的一个缺点是,非成员函数无法访问class的non-public成员。
3.tr1::function对象的行为就像一般函数指针,这样的对象可接纳“与给定目标签名式兼容”的所有可调用实体。
条款36.绝不重新定义继承而来的non-virtual函数
考虑:
class B {
public:
void mf();
...
};
cladd D: public B { ... };
D x; //x是一个类型为D的对象
B* pB = &x;
pB->mf();
//异于以下
D *pD = &x;
pD->mf()
//这两种通过同一个对象x调用成员函数mf得到行为是不一致的。
造成这种行为的原因是,non-virtual函数如B::mf和D::mf都是静态绑定的。由于pB被声明为一个pointer-to-B,通过pB调用的non-virtual函数永远是B所定义的版本,即使pB指向一个类型为“B派生之class”的对象。但是另一方面virtual函数却是动态绑定的,所以它们不会受这个问题所限制。这是实务面上的讨论,下面我们看下理论层面的理由:
回忆public继承意味着is-a关系,且class内声明一个non-virtual函数会为该class建立起一个不变性凌驾于特异性,将这两个观点施行于classes B和D以及non-virtual成员函数B::mf身上,那么,
- 适用于B对象的某一件事,也适用于D对象,因为每个D对象都是一个B对象;
- B的derived classes一定会继承mf的接口和实现,因为mf是B的一个non-virtual函数
现在,如果D重新定义mf,那么设计便会出现矛盾,因为D若有必要有一个不同于B的mf实现且每一个B对象真的必须使用B所提供的mf实现码,这就导致“每一个D都是B不为真”,那就不能使用public继承;此外,这还意味着mf无法为B反映出“不变性凌驾于特异性”的性质,那么mf应该声明为virtual函数。
请记住
1.绝不重新定义继承而来的non-virtual函数
条款37.绝不重新定义继承而来的缺省参数值
本条款成立的理由是:virtual函数是动态绑定的,而缺省参数值却是静态绑定。回忆下静态绑定与动态绑定之间的差异:静态绑定又名前期绑定,动态绑定又名后期绑定。
1)
对象的所谓静态类型(static type),就是它在程序中被声明时所采用的类型。考虑以下的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 public Shape {
public:
virtual void draw(ShapeColor color) const;
//注意,上句写法则当客户以对象调用此函数时一定要指定参数值,因为静态帮顶下这个函数并不从base继承缺省参数值。
//但若以指针调用此函数,可以不指定参数值。因为动态绑定下这个函数会从其base继承缺省参数值。
...
};
//现在考虑这些指针:
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle; //都是静态类型Shape *
本例中ps,pc,pr都被声明为pointer-to-Shape类型,所以它们都以它为静态类型。
对象所谓的动态类型则是指“目前所指对象的类型”。也就是说,动态类型可以表现出一个对象将会有什么行为。以上例而言,pc的动态类型是Circle*,ps没有任何动态类型,因为它尚未指向任何对象。
动态类型一如其名称所示,可在程序执行过程中改变:
ps = pc;
ps = pr; //ps的动态类型现在是Rectangle*
Virtual函数系动态绑定而来,意思是调用一个virtual函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型。
注意到,由于virtual函数时动态绑定的,而缺省参数值却是静态绑定。你可能会在“调用一个定义于derived class内的virtual函数”的同时,却使用base class为它所指定的缺省参数值。
pr->draw(); //调用Rectangle::draw(Shape::Red)
此例中,pr的动态类型是Rectangle*,所以调用的是Rectangle的virtual函数,Rectangle::draw函数的缺省参数值应该是GREEN,但由于pr的静态类型是ps,所以此调用的缺省参数值来自Shape class并非Rectangle class。这个问题的重点不在于是否用指针或引用操作,而在于draw是一个virtual函数,它有个缺省参数值在derived class中被重新定义了。C++这么做的原因是为了保证运行期效率。如果缺省参数值是动态绑定,编译器就必须有某种办法在运行期为virtual函数决定适当的缺省参数值,这比目前实行的“在编译器决定”的机制更慢更复杂。
2)
接下来,当你试着遵守这条守则并且同时提供缺省参数给base和derived 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;
...
};
这造成了代码重复和代码相依性,如果Shape内的缺省参数值改变,所以derived classes也必须改变。此时需要考虑替代设计,其中之一就是NVI(non-virtual intereface)手法:令base class的一个punlic non-virtual函数调用private virtual函数,后者可悲derived class重新定义。这里我们可以让non-virtual函数指定缺省参数,而private virtual函数负责真正的工作:
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; //不须指定缺省参数值
};
由于non-virtual函数绝不应该被重写,这个设计很清楚的使得draw函数的color缺省参数值总是为Red;
请记住
1.绝对不要重新定义一个继承而来的缺省参数值,因为缺省参数值都是静态绑定的,而virtual函数——你应该唯一覆写的东西——却是动态绑定的。
条款38.通过复合塑膜出has-a或“根据某物实现出”
复合式类型之间的一种关系,当某种类型的对象内含它种类型的对象,便是这种关系:
class Address { ... };
class PhoneNumber { ... };
class Person {
public:
...
private:
std::string name;
Address address;
PhoneNumber voiceNumber;
PhoneNumber faxNumber;
};
复合有两个意义:复合意味has-a或is-implemented-terms-of(以某物实现出)。当复合发生于应用域内的对象之间,表现出has-a的关系,当它发生于实现域内则是表现is-implemented-terms-of的关系,比较麻烦的问题是区分这两种关系。
1)
根据一个list对象实现Set对象
template<class T> //list应用于Set,正确做法
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; //用来表述Set的数据
};
下面是Set成员函数的实现:
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>
stdLLsize_t Set<T>::size() const
{
return rep.size();
}
请记住
1.复合的意义和public继承完全不同;
2.在应用域,复合意味has-a;在实现域,复合意味着is-implemented-terms-of(根据某物实现出)
条款39.明智而审慎的使用private继承
现在重复条款32中所使用的例子,并以private继承替换public继承:
class Person { ... }
class Student: private Person { ... }; //使用private继承
void eat(const Person& p);
void study(const Student& s);
Person p; // p人
Student s; //s学生
eat(p); //正确
eat(s); //错误
显然private不意味着is-a关系,它意味着什么?
首先,如果classes之间的继承关系是private,编译器不会自动将一个derived class对象转换为一个base class对象。这和public继承的情况不同,也就是通过s调用eat失败的原因。第二条规则是,由private base class继承而来的所有成员,在derived class中都会编程private属性,纵使它们在base class中原本是protected或者public属性。
1)
private继承纯粹是一种实现技术,意味着只有实现部分被继承,而接口部分应略去。如果D以private形式继承B,意思是D对象根据B对象实现而得,没有其他意义。private继承意味着is-implemented-in-terms-of,注意到条款38杠指出复合的意义也是这样,如何在private和复合之间选择?答案是尽可能使用复合,必要时才使用private继承。什么时候是必要?主要是当protected成员和virtual函数牵扯进来的时候。
2)
假设我们的程序设计Widgets,而我们决定应该较好地了解如何使用Widgets。要知道,带有多个执行阶段的程序,可能在不同阶段拥有不同的行为轮廓。例如编译器在解析阶段所使用的函数,大大不同于在最优化和代码生成阶段所产生的函数。接下来我们准备修改Widget class,让它记录每个成员函数的被调用次数。
首先我们使用这个类:
class Timer {
public:
explicit Timer(int tickFrenquency);
virtual void onTick() const; //定时器每滴答一次,此函数就被自动调用一次.我们重新定义该函数,让它取出Widget的当时状态
...
};
为了让Widget重新定义Timer内的virtual函数,Widget必须继承自Timer。但由于Widget并不是Timer,所以不能使用public继承方式,所以我们必须以private形式继承Timer:
class Widget: private Timer {
private:
virtual void onTick() const; //查看Widgets的数据
...
};
但其实我们可以用复合取代private继承方式,在Widget内声明一个嵌套式private class,后者以public形式继承Timer并重新定义onTick,然后放一个这种类型的对象于Widget内:
class Widget {
private:
class WidgetTime: public Timer{
public:
virtual void onTick() const;
...
};
WidgetTimer timer;
...
};
这个做法的优点在于:第一,首先如果WidgetTime是Widget内部的一个private成员并继承Timer,Widget的derived class将无法取用WidgetTimer,因此无法继承它或重新定义它的virtual函数。第二,这种做法降低了编译依存性,不需要#include任何与Timer有关的东西,只需要带着一个简单的WidgetTimer声明式。private继承主要用于:“当一个意欲称为derived class者想访问一个意欲称为base class者的protected成分,或为了重新定义一个或多个virtual函数”,仅有一种激进情况使得你选择private继承而非继承加复合。
3)
这种情况只适用于你所处理的class不带任何数据时,这样的classes没有non-static成员变量,没有non-virtual函数,也没有virtual base classes。于是,这种所谓的empty classes对象不使用任何空间,因为没有任何隶属对象的数据需要存储。然而C++规定凡是独立对象都必须有非零大小,所以如果你这样做:
class Empty { }; //没有数据,所以其对象应该不适用内存
class HoldsAnInt { //应该只需要一个int空间
private:
int x;
Empty e; //应该不需要内存
};
你会发现size(HoldsAnInt) > size(int),然而请记住对象是“独立”对象,如果使用private继承使得对象称为derived class对象内的base class成分,即非独立:
class HoldsAnInt: private Empty {
private:
int x;
};
几乎可以确定sizeof(HoldsAnInt) == sizeof(Int),这是所谓的EBO(empty base optimization)。
请记住
1.在表征is-implemented-in-terms-of的时候,尽量使用复合,而非private继承;
2.当需要一个class访问另一个class的protected成员,或重新定义其一或多个virtual函数,private继承可完成这样的功能,此外与复合相比,private继承能够造成empty class最优化。
条款40.明智而审慎地使用多重继承
多重继承的意思是继承一个以上的base classes,但这些base classes并不常在继承体系中又有更高级的base classes,因为那会导致“钻石继承”:
class File { ... };
class InputFile: public File { ... };
class OutputFile: public File { ... };
class IOFile: public InputFile, public OutputFile { ... };
任何时候如果你有一个继承体系而其中某个base class和某个derived class之间有一条以上的相通路线,你就必须面对这样一个问题:是否打算让base class内的成员变量经由每一条路径被复制?假设File class有个成员变量,那么IOFile内该有多少笔这个名称的数据呢?从某一个角度老鼠,IOFile从其某个base class继承一份,所以其对象内应该有两份fileName成员变量。但从另一个角度说,简单的逻辑告诉我们,IOFile对象只该有一个文件名称,所以它继承自两个base classes而来的filename不该重复。C++两个方案都支持,缺省办法是支持第一种方案,如果这不是你想要的,那么必须令带有此数据的class成为一个virtual base class,为了这样做,你必须令所有直接继承它的classes采用virtual继承:
class File { ... };
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile, public OutputFile { ... };
使用virtual 继承是正确行为的观点,但是它往往会带来virtual继承的classes所产生的对象往往比使用non-virtual继承的兄弟们体积大,访问virtual base class的成员变量时也更慢,这是为使用virtual继承付出的代价。此外,virtual继承的成本还包括其他方面:支配“virtual base classes初始化”的规则并不直观且复杂。virtual base的初始化责任是由继承体系中的最低层负责,这暗示1)classes若派生自virtual bases而需要初始化,必须认知其virtual bases——不论那些bases距离多远,2)当一个新的derived class加入继承体系中,它必须承担起virtual bases的初始化责任。因此对virtual base classes的忠告是:第一,非必要不适用virtual bases,平常请使用non-virtual继承。第二,如果必须使用virtual base classes,尽可能在其中放置数据。
1)
请看下面这个class:
class IPerson {
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
IPerson的客户必须以IPerson的pointers和references来编写程序,因为抽象classes无法被实体化创建对象,为了创建一些可被当作IPerson来使用的对象,IPerson的客户使用工厂函数(条款31)将“派生自IPerson的具象classes”实体化:
//条款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;
...
但是makePerson如何创建对象并返回一个指针?无疑地一定有某些派生自Iperson的具象class,在其中makePerson可以创建对象。假设这个class名为CPerson,就像具象class一样,CPerson必须提供“继承自IPerson”的pure virtual函数的实现代码。例如,假设有个既有的数据库相关class,名为PersonInfo,提供CPerson所需要的实质:
class PersonInfo {
public:
explicit PersonInfo(DatabaseID pid);
virtual ~PersonInfo();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
...
private:
virtual const char* valueDelimOpen() const; //详下
virtual const char* valueDelimClose() const;
...
};
两个virtual函数valueDelimOpen()和valueDelimClose()允许derived classes设定头尾界限符号,PersonInfo成员函数将调用virtual函数,把适当的界限符号添加到它们的返回值。以PersonInfo::theName为例:
const char* PersonInfo::valueDelimOpen() const
{
return "[";
}
const char* PersonInfo::valueDelimClose() const
{
return "]"
}
const char* PersonInfo::theName() const
{
static char value[Max_Formatted_Field_Value_Length]; //限定固定大小的缓冲区,将带来线程问题,见条款21
std::strcpy(value, valueDelimOpen());
std::strcat(value, valueDelimClose());
return value;
}
上述代码theName调用valueDelimOpen产生字符串起始符号,然后产生name值,然后调用valueDelimClose。由于valueDelimOpen和valueDelimClose都是virtual函数,theName返回的结果不仅取决于PersonInfo也取决于从PersonInfo派生下去的classes.CPerson和PersonInfo的关系是,PersonInfo刚好有若干函数可帮助CPerson比较容易实现出来,它们的关系因此是is-implemented-in-terms-of,显然我们可以使用复合完成这项工作和private 继承。条款39指出复合是受欢迎的做法,但如果需要重新定义virtual函数,那么继承是必要的。此处使用private继承,但CPerson也必须实现IPerson接口,那需以public继承才能完成,这导致多重继承的一个通情达理的应用:将“public继承自某接口”和“private继承自某实现”结合在一起:
class IPerson { //指出需实现的接口
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 ~Person();
virtual const char* theName() const;
virtual const char* theBirthDate() const;
virtual const char* valueDelimOpen() const;
virtual const char* valueDelimClose() const;
...
};
class CPerson: public IPerson, private PersonInfo {
public:
explicit CPerson(DatabaseID pid): PersonInfo(pid) {}
virtual std::string name() const //实现必要的IPerson成员函数
{
return PersonInfo::theName(); //实现必要的IPerson成员函数
}
virtual std::string birthDate() const
{
return PersonInfo::theBirthDate();
}
private:
const char* valueDelimOpen() const { return ""; } //重新定义继承而来的virtual界限函数;
const char* valueDelimClose() const { return ""; }
}
这个例子告诉我们,多重继承也有它的合理用途。
请记住
1.多重继承比单一继承负责,它可能导致新的歧义性,以及对virtual继承的需要;
2.virtual继承会增加大小、速度、初始化复杂度等成本,如果virtual base classes不带任何数据,将会是最有价值的情况;
3.多重继承的确有正当用途。其中一个情节涉及“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合。

浙公网安备 33010602011771号