文章目录
继承于面向对象设计
条款32.确定你的public继承塑模出is-a关系
- “public继承”意味is-a。适用于base classes身上的每一件事情一定也适用于derived classes身上,因为每一个derived class对象也都是一个base class对象。
条款33.避免遮掩继承而来的名称
该题材主要与作用域有关,一般来说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();
...
};
void Derived::mf4()
{
...
mf2();
...
}
这里面的mf2()首先会在class Derived覆盖的作用域找,然后是在Base中找,都没有的话在含Base的那个namespace(s)的作用域(如果有的话),最后在global的作用域找。
对上诉例子进行部分改动,添加mf3重载,重新充分认识继承体系内的“名称可视性”
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 mf4();
void mf4();
...
};
当如下调用时会出现base class中的mf1与mf3被derived class内的mf1和mf3函数遮掩掉了。
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
然而上述已经违反了base和Derived classes之间的is-a关系,在派生类中无法使用基类函数。不过可以使用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作用域可见(并且都为public)
using Base::mf3;
virtual void mf1();
void mf4();
void mf4();
...
};
当并不希望继承base classes的所有函数时,使用private继承,对于需要继承的函数使用转交函数(forwarding function):
class Base{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
... //与前同
};
class Derived:private Base{
public:
virtual void mf1() //转交函数,暗自成为inline
{Base::mf1();}
...
};
条款34.区分接口继承和实现继承
* 声明一个pure virtual函数的目的就是为了让derived classes只继承函数接口
* (非纯)impure virtual函数的目的,是让derived classes继承该函数的接口和缺省实现。
impure virtual函数同时指定函数声明和缺省行为,可能造成危险。
在基类中的impure virtual函数会在derived class中不定义时会默认缺省。可能在未知的方式被调用。
需要切断“virtual 函数接口”和其“缺省实现”之间的连接。最好的方法是实现纯虚函数的自定义,举一个例子:
定义不同飞机的飞行方式。A,B一直使用默认的方式,C有特有的方式
class Airplane{
public:
virtual void fly(const Airport& destinaton) = 0;
...
};
void Airplane::fly(const Airport& destination) //纯虚函数的实现
{
缺省行为
}
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型飞机特有的飞行方式
}
此时,通过对纯虚函数的定义,A,B可以使默认的实现接口,C需要手动去实现。实现了接口与实现分离。此时声明部分表现的是接口(用于derived class使用),定义部分实现缺省行为,(derived class可以使用缺省行为,但是只有明确提出申请时才行)。
- 接口继承和实现继承不同。在public继承之下,derived classes总是继承base class接口
- pure virtual函数值具体指定接口继承
- impure virtual函数具体指定接口继承及缺省实现继承
- non-virtual 函数具体指定接口继承以及强制性实现继承
条款35:考虑virtual函数以外的其它选择
以游戏角色健康值为例,不同的角色健康值不同,可能第一想到的是用virtual函数去实现。看下以下几种替代方法。
在解决问题而寻找某个设计方法时,不妨考虑virtual函数的替代方法。有以下几种替代方法
- 使用non-virtual interface(NVI)手法,那是Template Method设计模式中的一种特殊形式。用public non-virtual 成员函数包裹较低访问性的virtual函数。
class GameCharacter{
public:
int healthValue() const //devired classes不重新定义
{
... //做一些事前工作
int retval = doHealthValue();
... //做一些事后工作
return retVal;
}
private:
virtual int doHealthValue()const //devired classes重新定义
{
...
}
};
优点是可以在事前事后的操作中省略重写代码,比如加解锁,日志记录,验证class约束条件
- 将virtual函数替换为“函数指针成员变量”,是Strategy设计模式的一种分解表现形式。
class GameCharacter; //前置声明
int defaultHeathCalc(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:
HeallthCalcGunc healthFunc;
};
相比于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,用来替换当前的健康指数计算函数。
- trl::function成员变量替换virtual函数。允许使用任何的可调用物搭配一个兼容于需求的签名式。
与使用函数指针不同,而是改用一个类型为trl::function的对象,相比于函数指针,该对象可以是某个“像函数的东西”,且返回类型是任何可悲转换为int的类型。
class GameCharacter; //如前
int defaultHeathCalc(const GameCharacter& gc); //如前
class GameCharacter{
public:
//HealthCalcFunc可以是任何“可调用物”(callable entity),可被调用并接受任何兼容于GameCharacter之物,返回任何兼容于int的东西
typedef std::trl::function<int (const GameCharacter&)>HealthCalcFunc
explicit GameCharacter(HealthCalcFunc hcf = defaultHealthCalc)
:healthFunc(hcf)
{}
int healthValue()const
{return healthFunc(*this);}
..
private:
HeallthCalcGunc healthFunc;
};
具体观察HealthCalcFunc。
std::trl::function<int (const GameCharacter&)>
对比于函数指针,这个签名代表的函数是“接受一个reference指向const GameCharacter,并返回int”,但与函数指针不同的是,返回类型兼容能所有隐式转换为int类型,入参也可以是非const类型,这种范式行为带来了极大的方便。
- 将继承体系内的virtual函数替换为另外一个继承体系中的virtual函数。这个是Strategy设计模式的传统实现手法。对应的代码如下:
class GameCharacter; //前置声明
class HealthCalcFunc{
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是某个继承体系的根类,其可以有derived classes 分别为EvilBadGuy与EyeCandyCharacter,HealthCalcFunc是另一个继承体系的根类,体系中有derived classes分别为SlowHealthLoser和FastHealthLoser,每一个GameCharacter对象中都内含一个指针,指向来自HealthCalcFunc继承体系的对象。
条款36:绝不重新定义继承而来的non-virtual函数
该条款很好理解,即重新定义的non-virtual函数会破坏父类的定义,违背了条款32与34
条款37: 绝不重新定义继承而来的缺省参数值
该条例重点讨论“继承一个带有缺省参数值的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(ShapeColor color) const;
//以上这么写当客户以对象调用此函数是,一定要指定参数值。因为静态绑定下这个函数并不从其base继承缺省参数值。
//但以指针(或reference)调用此函数,可以不指定参数值,因为动态绑定下这个函数会从其base继承缺省参数值。
};
静态绑定为前期绑定
//静态类型都为Shape*
Shape* ps;
Shape* pc = new Circle;
Shape* pr = new Rectangle;
动态类型为延迟绑定
ps = pc; //ps的动态类型如今是Circle*
ps = pr; //ps的动态类型如今为Rectangle*
简单来说,就是当以对象调用的时候derived classes对象会无法继承base类的缺省参数。而通过指针调用时。调用相应的draw函数时缺省参数因为是静态绑定所以会使用base类的缺省参数而不是自身的。
这种现象导致base缺省参数的改变会导致其derived classes类的改变。
解决的方法如条款35所示的几种方法:
以NVL为例
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;
};
条款38:通过复合塑模出“has-a”或“根据某物实现”
复合塑模即在一个class中包含其他的class,如人至少有一个名字,地址和身份号码,这个就是has-a,而不会说成人是一个名字,地址和身份号码的is-a形式。
而对于“根据某物实现”,举一个例子:
用一组class来表现由不重复对象组成的sets;由于set template中“每个元素耗用三个指针”,会消耗大量空间,节省时间。但对空间约束比较严格时,就无法使用set template。而list template与set类似,但由于list能容纳重复的事物,因此它们的关系并非是is-a的关系,无法使用public继承来塑模它们。set需要“根据list来”实现。
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; //用来表述Set的数据
};
//Set成员函数可大量依赖list及标准程序库其他部分提供的机能来实现
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();
}
条款39:明智而谨慎地使用private继承
不同于public继承的is-a形式,private继承意味着条款38的“根据某物实现”。因此继承自private base class的每样东西在你的class中都是private:因为它们都是实现枝节而已。
因此尽量使用条款38的复合必要时才使用private。
- 通常比复合(composition)的级别低。但是当derived class 需要访问protected base class的成员,或需要重新定义继承而来的virtual函数时,这么设计是合理的
- 和复合(composition)不同,private继承可以照成empty base最优化。即所谓的EBO(empty base optimization;空白基类最优化)
条款40:明智而谨慎地使用多重继承
多重继承会导致很多隐藏性的问题,
- 例如多重继承的类中都有相同名称的成员变量或者函数时。调用会产生歧义。在调用时需要明确指出调用的是哪一个base class的函数。
- 当多重继承的类又同一个基类时,会产生“钻石型多重继承”,而不想继承复制多份时,需要直接继承最高base的derivedclasses采用“virtual继承”
class File{...};
class InputFile:virtual public File{...};
class OutputFile:virtual public File{...};
class IOFile:public InputFile,public OutputFile
{...};
virtual继承所产生的体积大且初始化的规则远比non-virtual bases的情况复杂且不直观,若果virtual base 不带任何数据,将具有实用价值的情况。
关于多重继承的正当用途,涉及到“public继承某个Interface class”和“private继承某个协助实现的class”的两相组合的情况
具体实例可以看书中IPerson,PersonInfo,CPerson的例子。