第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-of
private
继承只限于软件实现层面,这就是为什么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
)