确定你的public继承塑模出is-a关系
public继承意味着is-a。适用于base classes身上的每一件事情也一定适用于derived classes身上,因为每个derived class对象也都是一个base calss对象。
这章不知怎么写,感觉要自己体会吧。
避免遮掩继承来的名称
这个其实是跟作用域关系比较大,就是derived class内的名称会遮挡base classes内的名称。在public的继承下从来没有人希望如此。
class Base{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
//derived class里面的mf1与mf3会遮挡base class里面的同名函数
class Derived: pubcic Base{
public:
virtual void mf1();
void mf3();
void mf4();
};
//此时
Derived d;
int x;
d.mf1(); //调用Derived::mf1
d.mf1(x); //本意调用Base::mf1,但是被遮挡了,所以错误
但是可以使用using声明和转交函数来显示出base classes的名称。
class Base{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
virtual void mf2();
void mf3();
void mf3(double);
};
//如果不使用using声明derived class里面的mf1与mf3会遮挡base class里面的同名函数
class Derived: pubcic Base{
public:
using Base::mf1; //使用using声明让base class里面名为mf1与mf3的所有东西在derived class可见
using Base::mf3; //并且是public的
virtual void mf1();
void mf3();
void mf4();
};
//上面的using声明式也放在derived class的public的区域
//因为base class内的public名称也应该是derived class的public。
//这个符合is-a关系。
但是有时候你不想继承base class中所有的函数,这种情况一般是在private继承下发生。
比如Deried class以private继承Base class,而Derived唯一想要继承的是mf1的无参数版本,这样的话,使用using声明就不合适,因为这样用Derived class就能访问所有的mf1版本。此时,我们可以使用转交函数。
class Base{
public:
virtual void mf1() = 0;
virtual void mf1(int);
};
class Derived: private Base{
public:
virtual void mf1() //转交函数
{ Base::mf1(); }
};
区分接口继承与实现继承
对于class的设计者,有的时候我们只希望继承成员函数的接口,有时候我们希望同时继承函数接口和实现并覆写它们所继承的实现。有的时候我们又希望继承函数的接口与实现,并且不允许覆写任何东西。
在public的条件下,public继承意味着is-a,所以public继承的成员函数接口总是会被继承。
声明一个pure virtual函数就是为了让derived class只继承函数的接口。
声明一个virtual函数(非纯虚函数)的目的就是为了让derived class继承该函数的接口和缺省实现。
声明一个non-virtual 函数就是为了令derived继承接口和一个强制性的实现。
考虑virtual函数以外的其他选择
书上说这部分是为了让你跳脱面向对象设计的常轨,考虑一些其他的解法。
class GameCh{
public:
virtual int healthVal() const;
};
由Non-Virtual Interface 实现 Template Method模式
class GameCh{
public:
int heathVal() const
{
... //这里可以做一些事前工作
int retVal = doHealthVal();
... //这里可以做一些事后工作
return retVal;
}
private:
virtual int doHealthVal() const
{...}
};
实际上就是保留一个public的成员函数,并让该成员函数成为non-virtual,该成员函数调用一个private virtual函数进行实际工作。
代码的事前工作与事后工作来保证virtual函数在被调用之前作某些事情(锁定互斥锁,调用运转记录的日志等),在被调用之后又做了某些事情(解除互斥锁等)。如果让客户直接调用virtual函数的话就没有任何好办法可以做这件事。
NVI手法会在derived class中重新定义private virtual函数。
NVI手法中的virtual函数并不一定是private,也可以是protected,但是virtual一定是public的时候,就不能使用NVI手法了。
Function Pointers实现 Strategy 模式
NVI手法依然还是使用virtual来进行实现的。
还有一种设计思路,就是主张doHealthVal的计算是完全与不同的子类无关,我们可以让每一个子类的构造函数都接受一个指针,这个指针是指向一个计算函数。
class GameCh; //前置声明
int defaultHealthCalc(const GameCh& gc); //计算健康值的默认缺省算法
class GameCh{
public:
typedef int (HealthCalcFuc)(const GameCharacter&); //C++11后用using替代或std::function替代该语句,如下
//using HealthCalcFunc = int (*)(const GameCh&);
//using HealthCalcFunc = std::function<int(const GameCh&)>;
explicit GameCh(HealthCalcFuc hcf = defaultHealthCalc)
:healthFuc(hcf)
{}
int healthValue() const
{ return healthFuc(*this); }
...
private:
HealthCalcFuc healthFuc;
};
class EvilBadGuy: public GameCh{
public:
explicit EvilBadGuy(HealthCalcFuc hcf = defaultHealthCalc)
:GameCharacter(hcf)
{...}
...
};
int loseHeathQuickly(const GameCh&); //计算方式1
int loseHeathSlow(const GameCh&); //计算方式2
EvilBadGuy ebg1(loseHealthQuickly);
EvilBadGuy ebg2(loseHealthQuickly); //相同类型的人物搭配,不同的计算方式
上述typedef那段代码不好阅读,详见下面链接。
参考链接
使用这种方法的话,如果人物的计算函数在运行期间需要变化,完全可以在GameCh中提供一个成员函数SetHealthCalc来替换当前的健康指数。
这些健康指数计算函数不再是GameCh的成员函数,也并没有特别访问即将计算的那个对象里面的成分。
当然,这些计算函数需要的只是GameCh里面的public的信息,但如果,他们需要的信息是non-public的信息,他们不是成员函数,无权访问,这就麻烦了。
唯一解决的方法就是弱化class的封装,比如使用那个函数作为friend函数或是为其实现提供public的访问函数之类的。
由tr1::function 完成Strategy模式
如果熟悉template以及它们对于隐式接口的使用,基于函数指针的做法就有点死板。应该可以更灵活点。
我们可以使用tr1::function对象来去除约束。
class GameCh;
int defaultHealthCalc(const GameCh& gc);
class GameCh{
public:
//此时具有很大的弹性,接受一个reference指向const GameCh并返回int
using HealthCalcFunc = std::tr1::function<int(const GameCh&)>;
explicit GameCharacter(HealthCalcFunc hfc = defaultHealthCalc)
:healthFuc(hcf)
{}
int healthVal() const
{ return healthFunc(*this); }
private:
HealthCalcFunc healthFunc;
};
有非常大的弹性,配合std::tr1::bind甚至能让计算函数使用成员函数。(详细见书,不过书上没写细节)
古典Strategy模式
古典的Strategy模式会把计算函数做成一个分离的继承体系的virtual成员函数。
class GameCharacter;
class HealthCalcFuc{
public:
virtual int cacl(const GameCh& gc) const
{...}
};
HealthCalcFuc defaultHealthCalc;
class GameCh{
public:
explicit GameCh(HealthCalcFuc* phcf = &defaultHealthCalc)
:pHealthCalc(phcf)
{}
int healthVal() const
{ return pHealthCalc->calc(*this);}
private:
HealthCalcFuc* pHealthCalc;
};
这个解法的关键在于,很容易辨认(熟悉Strategy模式),并且只要为HealthCalcFuc添加一个derived class,就可以将一个既有的算法纳入使用。
本章好麻烦。
但是介绍了几个替代virtual的方案:
NVI手法
Function Pointer手法
tr1::function成员变量替代手法
古典Strategy手法
绝不重新定义继承来的non-virtual函数
因为non-virtual 就是为了在继承体系里面保持一个不变性。
绝不重新定义继承而来的缺省参数值
virtual 函数是动态绑定,而缺省参数值是静态绑定。
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(Shape color) const; //指定参数值
//此处一定要写上参数值,因为静态绑定下这个函数并不从其base继承缺省参数值,
//但是以pointer或reference调用此函数,可以不指定参数值,因为动态绑定下这个函数会从其base继承缺省实现
};
Shape* ps; //静态类型为Shape*
Shape* pc = new Circle; //静态类型为Shape*
Shape* pr = new Rectangle; //静态类型为Shape*
//因为都声明为Shape*,所以他们的静态类型为Shape*
//动态类型是指目前指向对象的类型(通常经由赋值动作)
ps = pc; //ps的动态类型为Circle*
ps = pr; //pr的动态类型为Rectangle*
pc->draw(Shape::Red); //调用Circle::draw(Shape::Red)
pr->draw(Shape::Red); //调用Rectangle::draw(Shape::Red)
//调用这个缺省值是来自与Shape的,并不是Rectangle的Green!!
pr->draw(); //调用Rectangle::draw(Shape::Red)!
//
反正绝对不要重新定义一个继承而来的缺省参数,因为缺省参数都是静态绑定,而我们唯一能覆写的是virtual函数–动态绑定。
根据复合塑模出has-a或是根据某物实现出
复合是类型之间的一种关系,当某种类型的对象内含它种类型的对象,就是复合关系。
class Address{...};
class PhoneNum{...};
class Person{
public:
private:
Address address;
PhoneNum phonenum;
};
上述表现出来的关系就是复合,代表的是has-a关系。
复合具有两种意义:一种是has-a,另外一种就是根据某物实现出。
has-a的表现在于应用域中(对象是你塑造的世界中的某种事物),而根据某物实现出就是表现在与实现域中(对象是纯粹的实现细节,如互斥锁、缓冲区)。
复合的概念与public的概念是完全不同的。
明智地使用private继承
如果class之间是一个private继承关系,那么,就不会在将derived class对象转换成一个base class对象。
Private在软件设计层面上没有太多的意义,其意义主要在实现的层面,表现出来的意义是is-implemented-in-terms-of(根据某物实现出)。与复合一样。
只有当必要的时候我们才使用private继承。比如涉及到protected成员与virtual函数的时候,或是当空间方面的利害关系足以踢翻private继承支柱的时候。
class Timer{
public:
explicit Timer(int tickFrequency);
virtual void onTick() const;
};
//为了让Widget不能隐式转换为Timer,可以有一下两种方法
//即让Widget获得Timer的所有实现但是不继承接口
//借由private继承,Timer的public onTick在Widget变成private
class Widget: private Timer{
private:
virtual void onTick() const;
};
//使用一个新的类和public继承来复合实现
class Widget{
private:
class WidgetTimer: public Timer{
public:
virtual void onTick() const;
..
};
WidgetTimer timer;
};
使用复合替换Private继承的理由:
1、你想使Widget拥有derived class,但是你却不想derived class重新定义onTick()
2、你想Widget的编译依存关系降到最低。
使用Private继承的一些理由:
1、当一个意欲成为derived class者想访问一个意欲成为base class者的protected成分。
2、为了重新定义一个或多个virtual函数。
3、激进的涉及空间最优化问题。
//激进的问题
class Empty{}; //这个类没有数据,现实中这个类可能没有non-static成员变量,却往往内含有typedef,enums,static成员便变量或非virtual函数
class HoldAnInt{
private:
int x;
Empty em; //这个是会要求内存的,即便真的没有数据
};
//可以使用private进行空白基类最优化(EBO),但只能在单一继承中可行,并非多重继承。而且EBO无法被施用与有多个base class的derived class上
class HoldAnInt: private Empty{
private:
int x;
};
明智地使用多重继承
多重继承相对来说比较复杂,所以我选择单一继承。
上图所示的是一个要命的钻石多重继承UML图,因为不知到怎么画会比较好看,所以就将就一下。
多重继承的话,如果是使用non-virtual继承的话,D里面有成员变量,那么继承下来的A会有两份成员变量。解决方法是把D变成一个virtual base class。
书上对于virtual base class的忠告是:如果是不必要的情况就不要使用virtual base class。如果必须使用的话,就必须避免在里面存放数据。
后面由举了一个多重继承的正当用途的例子
即让B成为纯接口类,让C保留存放的数据,并为private继承(C为协助实现的class)。