条款34 区分接口继承和实现继承
函数接口继承(类比声明)和函数实现继承(类比定义)。
先见如下代码举例,
class Shape { public: virtual void draw() const = 0;//纯虚函数作为函数接口被继承 virtual void error(const string& msg);//impure virtual,上让derived classes继承函数的接口和缺省实现 int objectID()const;//non-virtual函数,绝对不要在derived class类中重新定义 }; class Rectangle::public Shape{}; class Ellipse::public Shape{};
于是有以下结论,
i、成员函数的接口总是会被继承;
ii、声明一个pure virtual函数的目的是为了让derived class只继承函数接口;
(有必要说明下,在C++中是可以为pure virtual函数提供定义的,但调用它的唯一途径是“调用时明确指出其class名称”)
iii、声明impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现;(也就是说,让derived class对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函数,此时fly将按A、B型号的飞机飞往目的地。
class ModelC:public Airplane{}; //然后代码中有一些诸如此类的动作 Airport PDX();//作者家附近的机场 Airplane* pa = new ModelC; pa->fly(PDX);//于是将按A、B型号的飞机飞行避免上述问题的做法是,切断“virtual函数接口”和其“缺省实现”之前的连接。见如下代码改写,
class Airplane { public: virtual void fly(const Airport& destination) = 0; protected: void defaultFly(const Airport& destination); }; void Airplane::defaultFly(const Airport& destination) { /*缺省行为,将飞机飞到指定的目的地*/ } //于是在派生类中重新改写fly函数 class ModelA :public Airplane { public: virtual void fly(const Airport& destination) { defaultFly(destination); } }; class ModelB :public Airplane { virtual void fly(const Airport& destination) { defaultFly(destination); } }; class ModelC :public Airplane { virtual void fly(const Airport& destination); }; void ModelC::fly(const Airport& destination) { //将C开型飞机飞至指定的目的地 }
iv、声明non-virtual函数的目的是为了令derived classes继承函数的接口以及一份强制性实现。(详情见条款36)
条款35 考虑virtual函数以外的其他选择
i、借由Non-Virtual Interface(NVI)手法实现Template Method模式
令客户通过public non-virtual成员函数间接调用private virtual函数,此处non-virtual函数称为virtual函数的外覆器。外覆器的好处就是确保得以在一个virtual函数被调用之前设定好适当场景,并在结束之后清理现场。如下代码举例,
class GameCharacter { public: int healthValue()const { ...//做一些事前工作 int retVal = doHealthValue();//做真正的工作 ...//做一些事后工作 } private: virtual int doHealthValue()const { ...//缺省算法以,计算健康指数 } };
ii、借由Function Pointers实现Strategy模式
此设计手法的好处一,同一人物类型之不同实体可以有不同的健康计算函数,如下代码举例,
class GameCharacter { public: int healthValue()const { ...//做一些事前工作 int retVal = doHealthValue();//做真正的工作 ...//做一些事后工作 } private: virtual int doHealthValue()const { ...//缺省算法以,计算健康指数 } };
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,用来替换当前的健康指数计算函数。(说实话,这点我是没有看懂的!)
iii、借由function完成Strategy模式
第ii点基于函数指针的做法看起来有点苛刻死板,于是我们可以改用一个类型为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;//此处声明healthFunc函数 };上述代码中, function<int (const GameCharacter&)>中的签名式为int (const GameCharacter&)表示接受一个reference指向const GameCharacter,并返回int。此function类型(也就是所定义的HealthCalcFunc类型)产生的对象可以持有(保存)任何与此签名式兼容的可调用物。所谓兼容,即是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。具体用法如下举例,
short calcHealth(const GameCharacter&); struct HealthCalculator { int operator()(const GameCharacter&)const{} }; class GameLevel { public: float health(const GameCharacter&)const; }; class EvilBadGuy:public GameCharacter{}; class EyeCandyCharacter:public GameCharacter{}; EvilBadGuy ebg1(calcHealth);//人物1使用某个函数计算健康指数 EyeCandyCharacter ecc1(HealthCalculator());//人物2,使用某个函数对象计算健康指数 GameLevel currentLevel; EvilBadGuy ebg2(bind(&GameLevel::health, currentLevel, _1));//人物3,使用某个成员函数计算健康指数 //最后一条代码就是温故bind函数的用法,其中currentLevel类似隐匿传参,每次指出ebg2的健康计算函数应该总是以currentLevel作为Gamelevel对象, //_1为占位符,1表示占据了第1个参数。
iv、古典的Strategy模式
传统的Strategy做法将健康计算函数做成一个分离的继承体系中的virtual成员函数。
上图中,每一个GameCharacter对象都内含一个指针,指向一个来自HealthCalcFunc继承体系的对象。具体代码实现如下,
class GameCharacter;//前置声明 class HealthCalcFunc { virtual int calc(const GameCharacter& gc)const{} }; HealthCalcFunc defaultHealthCalc; class GameCharacter { public: typedef int(*HealthCalcFunc)(const GameCharacter&); explicit GameCharacter(HealthCalcFunc* phcf = &defaultHealthCalc) : pHealthFunc(phcf) {} int healthValue()const { return pHealthCalc->calc(*this); } private: HealthCalcFunc* pHealthFunc;//此处声明healthFunc函数 };
综上,
i、使用NVI手法,那是Template Method设计模式的一种特殊形式,它以public non-virtual成员函数包裹较低访问性(private或protected)的virtual函数。
ii、将virtual函数替换为“函数指针成员变量”,这是Strategy设计模式的一种分解表现形式。
iii、以function成员变量替换virtual函数,因而允许使用任何可调用物搭配一个兼容于需求的签名式。这也是Strategy设计模式的某种形式。
iv、将继承体系内的virtual函数替换为另一个继承体系内的virtual函数。这是Strategy设计模式的传统实现手法。
v、还有其它virtual函数的替换方案哈。
以上四点设计,只想说明,《设计模式》一书的内容还是非常重要的,否则很难看懂哈!
以上内容均来自Scott Meyers大师所著Effective C++ version3,如有错误地方,欢迎指正!相互学习,促进!!