六、继承与面向对象设计
Item32. 确定你的public继承塑模出is-a关系
(即Liskov Substitution Principle)
下面是一个不严谨的public继承:
Penguin并不会飞。
所以一个较好的替代方案是:
好的设计应该让违反这种关系的继承在编译期间就被检测出来。
public继承主张:能够施行于base class对象身上的_每件事情_都可以施行于derived class对象身上。
Item33. 避免遮掩继承而来的名称
C++会遮掩外层的函数名称,而不论它们的参数类型相同与否,也不管这些函数是否是虚函数。(public继承
一定不要让其发生!)
使用using声明可以解决这个问题:
但是using声明式会使某一名称的函数在derived class中全部可见(如上例中,using Base::mf3会使其两个版本都可见)。这在public继承中是正确的(因为它是is-a关系),但在private继承中,可能会只想继承其中某一个版本(如那个无参数版本)。则需要下面的转交技术:
Item34. 区分接口继承和实现继承
在设计上可以让一个要被继承的纯虚函数(如fly)同时体现出两个基本要素:
其声明部分表现的是接口(那是derived classes必须使用的),其定义部分则表现出缺省行为(那是derived classes可能使用的,但只有在它们明确提出申请时才是):
小结:pure virtual函数只具体指定接口继承,impure virtual函数具体指定接口继承及缺省实现继承(这样的函数没有不变性,随derived classes而变),non-virtual函数具体指定接口继承以及强制性实现继承(这样的函数没有特化的空间,只能是base class中的那个)。
Item35. 考虑virtual函数以外的其他选择
1.籍由Non-Virtual Interface(NVI)实现Template Method模式
源自一个流派,他们主张virtual函数应该几乎总是private。
即-令客户通过public non-virtual成员函数间接调用private virtual。
NVI的一个优点是do "before" stuff和do "after" stuff。这确保得以在一个virtual函数被调用之前设计好适当场景(locking a mutex、log extry、verifying that class invariants and function preconditions are satisfied, etc.),并在调用结束之后清理场景(unlocking a mutex、verifying function postconditions、reverifying class invariants, etc.)。
2.籍由Function Pointers实现Strategy模式
这样便得同一人物类型之不同实体可以有不同的健康计算函数。例如:
而且可以在运行期变更计算函数。但是其缺点是当这个传入的non-member函数需要访问class的non-public成分时,我们只能弱化class的封装(如设置为friend、提供public访问函数)。
3.籍由tr1::function完成Strategy模式
签名式std::tr1::function<int (const GameCharacter&)>的含义是:接受一个reference指向const
GameCharacter,并返回int。tr1::function产生的对象可以持有任何与此签名式兼容的可调用物。
tr1::function比function pointer拥有更大的弹性,下面是三种情况:
①函数
short calcHealth(const GameCharacter&); // health calculation function;
//note non-int return type
②函数对象
struct HealthCalculator { // class for health
int operator()(const GameCharacter&) const // calculation function
{ ... } // objects
};
③成员函数
class GameLevel {
public:
float health(const GameCharacter&) const; // health calculation
... // mem function; note
}; // non-int return type
定义一些使用者
class EvilBadGuy: public GameCharacter { // as before
...
};
class EyeCandyCharacter: public GameCharacter { // another character
... // type; assume same
}; // constructor as EvilBadGuy
三种使用方法
EvilBadGuy ebg1(calcHealth); // character using a
// health calculation function
EyeCandyCharacter ecc1(HealthCalculator()); // character using a
// health calculation function object
GameLevel currentLevel;
...
EvilBadGuy ebg2( // character using a
std::tr1::bind(&GameLevel::health, // health calculation
currentLevel, // member function;
_1) // see below for details
);
(将currentLevel绑定为GameLevel对象,_1表示当为ebg2调用GameLevel::health时系以currentLevel作为GameLevel对象)
4.古典的Strategy模式
Item36. 绝不重新定义继承而来的non-virtual函数
如果A继承自B,又override了它的non-virtual函数,则此函数被调用时,可能表现出B或A的行为,这得看指
向该对象之指针的声明类型,因为non-virtual函数是statically bound。
Item37. 绝不重新定义继承而来的缺省参数值
因为缺省参数值都是静态绑定,而virtual函数却是动态绑定。违反了此条款,就导致调用一个定义于derived
class内的virtual函数的同时,却使用base class为它所指定的缺省参数值。
这时可以考虑替代设计,如NVI:
Item38. 通过composition塑模出has-a或is-implemented-in-terms-of
比如你想通过在底层使用list来构造一个set:
template<typename T> // the wrong way to use list for Set
class Set: public std::list<T> { ... };
但这种public继承违反了is-a的含义,list中可以有重复元素,但set不可以有,因此set不是一个list。
正确做法是set object can be implemented in terms of a list object:
Item39. 明智而审慎地使用private继承
由private继承而来的所有成员,在derived class中都会变成private属性。因此它们都成了实现的枝节。private继承意味着implemented-in-terms-of,这纯粹只是一种实现技术,在软件“设计”层面上没有意义,只在软件实现层面上有意义。
如果让class D以private继承class B,含义是为了采用B中已经备妥的某些特性,而不是因为B和D存在有任何观念上的关系。
Item38指出composition也用于实现implemented-in-terms-of,但要尽可能使用复合,必要时才使用private继承。
例如一个private继承:
也可以换成composition的设计:
composition的设计有两个优点:
1. 使得Widget可以拥有derived classes,但同时又可以阻止derived classes重新定义onTick。这一点private继承无法做到。(此法可用于模拟Java中的final或C#中的sealed)
2. 可以将Widget的编译依存性降至最低。即使WidgetTimer移出Widget体外,也只需要一个class
WidgetTimer声明式,而不用#include WidgetTimer,这是private继承所无法避免的。
Private继承有一个优点:可以实现EBO(empty base optimization)。
比较以下,先是composition:
将会发现sizeof(HoldsAnInt) > sizeof(int);
而在private中:
几乎可以肯定:sizeof(HoldsAnInt) == sizeof(int)
另外,当derived class需要访问protected base class的成员,或者需要重新定义继承而来的virtual函数时,使用private设计是合理的。
Item40. 明智而审慎地使用多重继承
为了避免钻石型继承,应该对直接继承自顶层的classes采用virtual继承:
class File { ... };
class InputFile: virtual public File { ... };
class OutputFile: virtual public File { ... };
class IOFile: public InputFile, public OutputFile { ... };
(thy:virtual继承专为多重继承引入,如果不是virtual继承的话那么如下:
则d.f(); 调用的是B::f(); 如果C,B不是virtual public A的话那d.f()就必须指出f()是类C的还是类D的,d.B::f()或d.C::f())
但是virtual继承会比non-virtual继承体积大,访问成员速度慢。另外virtual base的初始化责任是由继承体系中的最低层(most derived)class负责,这意味着若classes派生自virtual bases而需要初始化,它必须认知virtual bases——不论距离多远。
所以 1.非必要不使用virtual bases,2.如果一定要用,尽可能避免在其中放置数据。
此外,在设计时可以将public继承与private继承加以结合:
class CPerson: public IPerson, private PersonInfo {}
使用“public继承接口”和“private继承实现”。