第6章:继承与面向对象设计
条款32:确定你的public继承塑模出is-a关系
- “
public继承”意味is-a。适用于base class身上的每一件事情一定也适用于derived class身上,因为每一个derived class对象也都是一个base class对象
条款33:避免遮掩继承而来的名称
derived class的作用域被嵌套在base class作用域内1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
void mf2();
void mf2(double);
};
class Derived : public Base
{
public:
virtual void mf1();
void mf2();
};
Derived d;
int x;
...
d.mf1(); // 没问题, 调用 Derived::mf1
d.mf1(x); // 错误! 因为 Derived::mf1 遮掩了 Base::mf1
d.mf2(); // 没问题, 调用 Derived::mf2
d.mf2(x); // 错误! 因为 Derived::mf2 遮掩了 Base::mf2这段代码中
base class内所有名为mf1和mf2的函数都被derived class内的mf1和mf2函数遮掩掉了。从名称查找观点来看,Base::mf1和Base::mf2不再被Derived继承!实际上如果你正在使用
public继承而又不继承那些重载函数,就是违反base和derived class之间的is-a关系,而条款32说过is-a是public继承的基石。因此你几乎总会想要推翻C++对“继承而来的名称”的缺省遮掩行为。使用
using声明式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29class Base
{
private:
int x;
public:
virtual void mf1() = 0;
virtual void mf1(int);
void mf2();
void mf2(double);
...
};
class Derived : public Base
{
public:
using Base::mf1; // 让 Base class 内名为 mf1 和 mf2 的所有东西
using Base::mf2; // 在 Derived 作用域内都可见, 并且 public
virtual void mf1();
void mf2();
};
Derived d;
int x;
...
d.mf1(); // 没问题, 调用 Derived::mf1
d.mf1(x); // 现在没问题了, 调用 Based::mf1
d.mf2(); // 没问题, 调用 Derived::mf2
d.mf2(x); // 现在没问题了, 调用 Based::mf2使用
inline转交函数(forwarding function)然而在
private继承之下(见条款39),有时候你并不想继承base class的所有函数。假设Derived以private形式继承Base,而Derived唯一想继承的mf1是那个无参数版本。using声明式在这里派不上用场,因为using声明式会令继承而来的某给定名称之所有同名函数在derived class中都可见。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23class Base
{
public:
virtual void mf1() = 0;
virtual void mf1(int);
... // 与前同
};
class Derived : private Base
{
public:
virtual void mf1() // 转交函数
{
Base::mf1(); // 暗自成为 inline (见条款 30)
}
...
};
Derived d;
int x;
d.mf1(); // 很好, 调用的是 Derived::mf1
d.mf1(x); // 错误! Base::mf1 被遮掩
条款34:区分接口继承和实现继承
身为class设计者,有时候你会希望derived class只继承成员函数的接口,也就是声明;有时候你又会希望derived class同时继承函数的接口和实现,但又希望能够覆写(override)它们所继承的实现;又有时候你希望derived class同时继承函数的接口和实现,并且不允许覆写任何东西。
base类强烈影响以public形式继承它的derived class,因为:
成员函数的接口总是会被继承
声明一个
pure virtual函数的目的是为了让derived类只继承函数接口含有
pure virtual函数的类属于抽象基类,不能被实例化。继承了它们的类必须重新声明此函数接口并给予实现。抽象基类可以给pure virtual函数提供定义,但调用它的唯一途径是“调用时指定其class的名称”。声明
impure函数的目的是让derived class继承该函数的接口和缺省实现但是,允许
impure virtual函数同时指定函数声明和函数缺省行为,却有可能造成危险。欲探讨原因,让我们考虑XYZ航空公司设计的飞机继承体系。该公司只有A型和B型两种飞机,两者都以相同方式飞行。因此XYZ设计出这样的继承体系:1
2
3
4
5
6
7
8
9
10
11
12
13
14class 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实现”,Airplane::fly被声明为virtual。然而为了避免在ModelA和ModelB中撰写相同代码,缺省飞行行为由Airplane::fly提供,它同时被ModelA和ModelB继承。现在,
XYZ航空公司决定购买一种新式C型飞机。C型和A型以及B型的飞行方式不同。XYZ公司的程序员在继承体系中针对C型飞机添加了一个class,但由于他们急着让新飞机上线服务,竟忘了重新定义其fly函数:1
2
3
4
5
6
7
8
9
10class ModelC : public Airplane
{
// 未声明fly函数
};
Airport PDX(... ); // PDX 是我家附近的机场
Airplane* pa= new ModelC;
...
// 这将酿成大灾难
pa->fly(PDX); // 调用 Airplane::fly一种解救办法为切断“
virtual函数接口”和其“缺省实现”之间的连接。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44class Airplane
{
public:
// 现在是 pure virtual 函数
virtual void fly(const Airport& destination) = 0;
protected:
void defaultFly (const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
... // 缺省行为, 将飞机飞至指定的目的地。
}
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);
}
};
// 现在 ModelC class 不可能意外继承不正确的 fly 实现代码了
// 因为 Airplane 中的 pure virtual 函数追使 ModelC 必须提供自己的 f1y 版本
class ModelC: public Airplane
{
public:
virtual void fly(const Airport& destination);
};
void ModelC::fly(const Airport& destination)
{
... // 将 C 型飞机飞至指定的目的地
}另一种解救方法是,利用“
pure virtual函数必须在derived class中重新声明,但它们也可以拥有自己的实现”这一事实。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43class Airplane
{
public:
// 现在是 pure virtual 函数
virtual void fly(const Airport& destination) = 0;
};
// 给予 pure virtual 函数实现
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:
virtuai 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 型飞机飞至指定的目的地
}声明
non-virtual函数的目的是为了derived class继承函数的接口及其强制性实现non-virtual函数为其所属class建立的意义是不变性凌驾其特异性,所以它不该在derived class中被重新定义。
条款35:考虑virtual函数以外的其他选择
假设你正在写一个视频游戏软件,你的游戏属于暴力砍杀类型,剧中人物存在被伤害而降低健康状态的情况。因此你决定提供一个成员函数 healthvalue,它会返回一个整数,表示人物的健康程度。由于不同的人物可能以不同的方式计算他们的健康指数,将healthvalue声明为virtual似乎是再明白不过的做法:
1 | class GameCharacter |
现在考虑一些其它解法:
借助
non-virtual interface(NVI)手法实现Template Method模式保留
healthvalue为public成员函数,但让它成为non-virtual,并调用一个private virtual函数进行实际工作:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class GameCharacter
{
public:
int healthvalue() const // derived class 不重新定义它
{
... // 做一些事前工作
int retval = doHealthvalue(); // 做真正的工作
... // 做一些事后工作
return retval;
}
// NVI 手法下没必要让 virtual 函数一定是 private
private:
virtual int doHealthValue() const // derived class 可重新定义它
{
... // 缺省算法, 计算健康指数。
}
};这一基本设计,“令客户通过
public non-virtual成员函数间接调用private virtual函数”,称为non-virtual interface(NVI)手法。它是所谓Template Method设计模式的一个独特表现形式。我把这个non-virtual函数(healthvalue)称为virtual函数的外覆器(wrapper) 。NVI手法的一个优点隐藏在上述代码注释“做一些事前工作”和“做一些事后工作”之中。“事前工作”可以包括锁定互斥器、制造运转日志记录项、验证class约束条件、验证函数先决条件等。“事后工作”可以包括互斥器解除锁定、验证函数的事后条件、再次验证class约束条件等。如果你让客户直接调用virtual函数,就没有任何好办法可以做这些事。借助
Function Pointer实现Strategy模式另一个设计主张“人物健康指数的计算与人物类型无关”,这样的计算完全不需要“人物”这个成分。例如我们可能会要求每个人物的构造函数接受一个函数指针,指向一个健康计算函数,而我们可以调用该函数进行实际计算:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class GameCharacter; // 前向声明
// 以下函数是计算健康指数的缺省算法
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
typedef int (*HealthCalcFunc)(const GameCharacter&);
explicit GameCharacter (HealthCalcFunc hcf = defaultFealthCalc)
: healthFunc(hcf) {}
int healthvalue() const
{
return healthFunc (*this);
}
...
private:
HealthCalcFunc healthFunc;
};这种设计策略和前面介绍的方法相比提供了一些有趣的弹性:
同一人物类型的不同实体可以有不同的健康计算函数
1
2
3
4
5
6
7
8
9
10
11
12class 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,用来替换当前的健康指数计算函数。然而这种策略意味着,计算函数并不能访问“被计算健康指数的”那个对象的内部(
non-public)成分。如果需要访问non-public成分进行精确计算,这就有问题了。唯一能够解决的办法就是:弱化
class的封装。例如,将健康计算函数声明为friend。或是为其实现的某一部分提供public访问函数。这需要你来权衡利弊进行抉择。
借助
std::function实现Strategy模式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class GameCharacter;
int defaultHealthCalc(const GameCharacter& gc);
class GameCharacter
{
public:
// 不同之处
typedef std::function<int (const GameCharacter&)> HealthCalcFunc;
explicit GameCharacter (HealthCalcFunc hcf = defaultFealthCalc)
: healthFunc(hcf) {}
int healthvalue() const
{
return healthFunc (*this);
}
...
private:
HealthCalcFunc healthFunc;
};那个签名代表的函数是“接受一个
reference指向const GameCharacter,并返回int”。这个std::function类型(也就是我们所定义的HealthCalcFunc类型)产生的对象可以持有(保存)任何与此签名式兼容的可调用物。所谓兼容,意思是这个可调用物的参数可被隐式转换为const GameCharacter&,而其返回类型可被隐式转换为int。这就提供了很大的灵活性:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38short calcHealth(const GameCharacter&); // 健康计算函数
// 注意其返回类型为 non-int
struct HealthCalculator
{
// 为计算健康而设计的函数对象
int operator()(const GameCharacter&) const
{...}
};
class GameLevel
{
public:
float health(const GameCharacter&) const; // 成员函数, 用以计算健康
// 注意其 non-int 返回类型
};
// 人物类型 1
class EvilBadGuy: public GameCharacter
{
... // 同前
};
// 人物类型 2
class EyeCandyCharacter: public GameCharacter
{
// 假设其构造函数与 EvilBadGuy 同
};
// 人物 1, 使用某个函数计算健康指数
EvilBadGuy ebg1(calcHealth);
// 人物 2, 使用某个函数对象计算健康指数
EyeCandyCharacter ecc1(HealthCalculator());
GameLevel currentLevel;
...
// 人物 3, 使用某个成员函数计算健康指数
EvilBadGuy ebg2(std::bind(&GameLevel::health, ¤tLevel, _1));传统的
Strategy模式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42class GameCharacter;
class HealthCalcFunc
{
public:
virtual ~HealthCalcFunc() {}
virtual int calc(const GameCharacter& gc) const = 0;
};
class SlowHealthLoser : HealthCalcFunc
{
public:
virtual int calc(const GameCharacter& gc) const
{
...
}
...
};
class FastHealthLoser : HealthCalcFunc
{
public:
virtual int calc(const GameCharacter& gc) const
{
...
}
...
};
class GameCharacter
{
public:
explicit GameCharacter(HealthCalcFunc* phcf) : pHealthCalc(phcf) {}
int healthValue() const
{
return pHealthCalc->calc(*this);
}
private:
HealthCalcFunc* pHealthCalc;
};
条款36:绝不重新定义继承而来的non-virtual函数
- 如题
条款37:绝不重新定义继承而来的缺省参数值
本条款的讨论局限于“继承一个带有缺省参数值的virtual函数”:virtual函数是动态绑定(后期绑定,延迟绑定),而缺省参数值却是静态绑定(前期绑定,早绑定)。
1 | // 一个用以描述几何形状的class |
对象的静态类型就是它在程序中所声明的类型,对象的动态类型则是指“目前所指对象的类型”
1
2
3shape* ps; // 静态类型为 shape*, 无动态类型
Shape* pc = new Circle; // 静态类型为 Shape*, 动态类型是 Circle*
Shape* pr = new Rectangle; // 静态类型为 shape*, 动态类型是 Rectangle*动态类型可在程序执行过程中改变(通常是经由赋值动作):
1
2ps = pc; // ps 的动态类型如今是 circle*
ps = pr; // ps 的动态类型如今是 Rectangle*
virtual函数系动态绑定而来,意思是调用一个virtual函数时,究竟调用哪一份函数实现代码,取决于发出调用的那个对象的动态类型。
1 | pr->draw(); // 调用的是 Rectangle::draw(shape::Red)! |
此例之中,pr的动态类型是Rectangle*,所以调用的是Rectangle的virtual函数。Rectangle::draw函数的缺省参数值应该是Green,但由于pr的静态类型是shape*,所以此一调用的缺省参数值来自Shape class而非Rectangle class!这不符合预期。
解救方法是利用条款35中介绍的NVI手法替代virtual函数:
1 | class Shape |
这个设计很清楚地使得draw函数的color缺省参数值总是Red。
条款38:通过复合塑模出has-a或is-implemented-in-terms-of
当某种类型的对象内含其它对象时便形成了复合关系。
应用域
程序中的对象其实相当于你所塑造出来的世界中的某些事物。比如人,汽车,高楼大厦等。这样的对象属于应用域部分。
实现域
其它对象如缓冲区,互斥器,搜索树等纯粹是实现细节上的工具。这些对象相当于软件中的实现域。
当复合发生于应用域对象之间表现出has-a关系,当它发生于实现域内则表现出is-implemented-in-terms-of(根据某物实现出)的关系。
复合的意义和
public继承完全不同注意区分
is-a和is-implemented-in-terms-of这两种对象关系。
条款39:明智而谨慎地使用private继承
一个derived类继承base类有public、protected或private三种继承方式。
公有继承(
public)(普遍使用)base类的public成员也是derived类的public成员,base类的protected成员也是derived类的protected成员保护继承(
protected)(基本不使用)base类的public和protected成员将成为derived类的protected成员。私有继承(
private)(少的情况使用)base类的public和protected成员将成为derived类的private成员。
无论哪一种继承方式,base类的private成员都不能直接被derived类访问,但是可以通过调用base类的public和protected成员间接来访问(如果base类提供了访问接口的话)。
private继承意味着is-implemented-in-terms-ofprivate继承只限于软件实现层面,这就是为什么private继承后,base class的所有成分(除了private)在你的class内都是private,因为它们都只是实现细节而已。尽可能使用复合,必要时才使用
private继承当
derived类想要访问base class的protected成员时当
derived类为了重新定义base class的virtual函数时现在考虑一个
Widget类,它需要用到另一个计时器Timer类来实现一些业务。1
2
3
4
5
6class Timer
{
public:
explicit Timer(int tickFrequecy);
virtual void onTick() const; // 定时器每滴答一次, 此函数就调用一次
};为了让
Widget重新定义Timer内的virtual函数,Widget必须继承自Timer。因为Widget不是个Timer,因此public继承不合适。必须以private方式继承:1
2
3
4
5
6class Widget
{
...
private:
virtual void onTick() const; //
};通过
private继承,Timer的public onTick函数在Widget内变成private,而我们重新声明(定义)时仍然把它留在那儿。如果把onTick放进public内会误导客户以为他们可以调用它,那就违反了条款18。另一种设计方案是采用复合 +
public继承:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15class Widget
{
...
private:
// 在 Widget 内声明一个嵌套式 private class
class WidgetTimer : public Timer
{
public:
// 以 public 方式继承并重新定义 virtual 函数
virtual void onTick() const;
...
};
WidgetTimer timer;
...
};采用第二种较为复杂的方式的两点理由:
- 你或许会想设计
Widget使它得以拥有derived class,但同时你可能会想阻止derived class重新定义onTick。如果Widget继承自Timer,上面的想法就不可能实现,即使是private继承也不可能。但如果WidgetTimer是Widget内部的一个private成员并继承自Timer,Widget的derived class将无法取用WidgetTimer,因此无法继承它或重新定义它的virtual函数。 - 你或许会想要将
Widget的编译依存性降至最低(条款31)。如果Widget继承Timer,当Widget被编译时Timer的定义必须可见,所以定义Widget的那个文件恐怕必须#include Timer.h。但如果WidgetTimer移出Widget所在文件之外而Widget内含指针指向一个WidgetTimer,Widget可以只带着一个简单的WidgetTimer前向声明式,不再需要#include任何与Timer有关的东西。很容易就实现了解耦。
- 你或许会想设计
EBO(empty base optimization)empty class指的是没有non-static成员变量,没有virtual函数,也没有virtual base class。于是这种class不占用理论上不占用任何内存空间。然而在实现技术上,C++要求独立(非附属)对象都必须有非零大小。1
2
3
4
5
6
7
8
9
10class Empty { }; // 没有任何需要存储的数据
// 理论上 sizeof(HoldAnInt) = 4 字节(就一个 int)
// 实际上 sizeof(HoldAnInt) = 8 字节
class HoldAnInt
{
private:
int x; // 4 字节
Empty e; // 理论上应该不占据内存
};C++编译器默认为empty class独立对象安插一个char(1字节)由于内存对齐
HoldAnInt的实际大小为4 + 1(内存对齐至 4) = 8但这个约束不适用于
derived class对象的base class成分(非独立):1
2
3
4
5
6// 理论和实际上 sizeof(HoldAnInt) = 4 字节(就一个 int)
class HoldAnInt : public Empty
{
private:
int x; // 4 字节
};现实中的
empty class并非真是empty。往往内含typedef,enum,static成员变量,或non-virtual函数。STL就有许多技术用途的empty class,其中内含有用的成员(通常是typedef),包括base class unary_function和binary_function,这些是“用户自定义之函数对象”通常会继承的class。由于EBO,这样的继承很少增加derived class的大小。
条款40:明智而谨慎地使用多重继承
多重继承较单一继承复杂,可能导致歧义性
当继承的多个
base具有同名成员函数时,derived类调用时会出现歧义。有时多重继承不得不需要
virtual继承,而这会带来额外的体积、运行速度、初始化复杂度等成本比如这样的钻石继承体系:

这个继承体系中某个
base class和某个derived class之间有一条以上的相通路线,假设File class有个成员变量fileName,那么IOFile从每一个base class各继承一份,所以其对象内会有两份fileName成员变量。然而IOFile对象只该有一个文件名称,所以名称fileName不该重复。解决办法就是采用
virtual继承:
virtual继承带来的后果是:使用
virtual继承的那些class所产生的对象往往比使用non-virtual继承的class体积大访问
virtual base class的成员变量时,也比访问non-virtual base class的成员变量速度慢另外,支配“
virtual base class初始化”的规则比起non-virtual base class的情况远为复杂且不直观。virtual base class的初始化责任是由继承体系中的最低层(most derived)class负责的,这意味着:class若派生自virtual base而需要初始化,必须知道其virtual base class——不论那些base class距离多远。- 当一个新的
derived class加入继承体系中,它必须承担其virtual base class的初始化责任。
关于
virtual继承的忠告:
- 非必要不使用
virtual base。平常请使用non-virtual继承。 - 如果必须使用
virtual base class,尽可能避免在其中放置数据。这么一来你就不需担心这些class身上的初始化(和赋值)所带来的诡异事情了。
最后看一个多重继承的应用例子——public继承某个Interface class和private继承某个协助实现的class。

(参考书籍P195-P198)