这一章探讨的主题是 C++ 的继承和面向对象设计.
C++ 的 OOP 有可能和你原本习惯的 OOP 稍有不同.
这一章只要回答了以下的一些问题:
- “继承”可以是单一继承或多重继承,每一个继承关系该用 public、private、protected?
- 成员函数是 virtual 、 non-virtual 还是 pure virtual?
- 缺省参数值 与 virtual 函数有什么交互影响?
- 继承如何影响 C++ 的名称查找规则?
- 设计选项有哪些?
- 如果class 的行为需要修改, vritual 函数是最佳选择吗?
32. 确定你的 public 继承是 “is-a” 关系
牢记,public 继承是 一种"is-a" 关系, 适用于 base classes 身上的每一件事情也一定适用于 派生类上,因为每一个派生类都是一个 base class 对象
你可能会根据直觉写出这样的代码:
1 | class Brid { |
企鹅并不会飞,但是你必须实现 fly 这个函数,因为Brid 这个基类中fly 被声明为 virtual。下面的继承关系,似乎更接近事实:
1 | class Brid { |
33. avoid hiding inherited names
1 | class Base { |
这里,编译器先查找 local 作用域,接着是 Derived class 作用域, Base 作用域
在上面的代码中, Base 类内所有名为 mf1 和mf3 函数都被遮掩掉了. 从名称查找观点来看, Base::mf1 和 Base:: mf3 不在被 Derived 继承.
1 | Derived d; |
这是因为 C++ 对"继承而来的名称"的缺省遮掩行为. 这是为了防止你在程序库或应用框架内建立新的 derived class 时附带从疏远的 base class 继承重载函数.要避免出现这样的问题也很简单,我们只需要借助 using 声明式就可以了.
1 | class Derived : public Base { |
不过,在 private 继承下,using 却在显现不出身手,我们需要另一种策略,即一个简单的转交函数(forworading function)
1 | class Base { |
34. 区分接口继承和实现继承
身为 classes 的设计者, 你不断地在”继承接口” 还是 “继承实现”之间做出选择.
这里我们需要按照这样的行为规范做事:
在 public 继承下, derived class 总是继承 base class的接口
声明一个 pure virtual 函数的目的是为了让 derived classes 只继承函数接口.
声明一个 impure virtual 函数的目的,是让 derived classes 继承该函数的接口和缺省实现.
声明 non-virtual 函数的目的是为了令 derived classes 继承函数的接口及一份强制性实现
35. 考虑 virtual 函数以外的其他选择
1 | class GameCharacter { |
有时考虑考虑 其他替代方案来 替换 virtual ,(更恰当的来说是跳出 面向对象设计) 能帮助我们写出更好的代码.
STL 就是一个很典型的例子
(1) 用 non-Virtual Interface ( NVI)手法实现 Template Method 模式
令客户通过 public non-virtual 成员函数间接调用 private virtual 函数. 这样 “调用virtual 函数之前和之后我们可以做一些准备工作”.
例外,NVI 手法允许派生类重新定义 virtual 函数,从而赋予它们 “如何实现”的能力,但让 base 类保留何时调用的权利.
1 | class GameCharacter { |
NVI 下 virtual 不一定是 private,也可能是 public,但不能是 public.
(2) 由 Function Pointers 实现 Strategy 模式
NVI 手法对 public virtual 函数而言是一个有趣的替代方案,不过看起来有点像”花瓶”华而不实? 毕竟我们还是要使用 virtual 函数来实现.
下面的例子是 常见的 Strategy 设计模式的简单使用,通过 函数指针的方式实现.
1 | class GameCharacter; //前置声明 |
(3) 借用 tr1:: function 完成 Stra
(4) 标准的 Strategy 模式
标准的Strategy 模式更容易被辨认出来,而且通过 继承 HealthCalcFunc 就能够加入一个新的算法.
1 | class GameCharacter; |
36. 不重新定义继承而来的 non-virtual 函数
non-virual 函数都是静态绑定,而 virtual 是动态绑定.如果在派生类中重新定义基类的 non-virtual, 那么除非你明确的指出,否则你将调用的总是派生类版本,而不是基类版本. public 继承指出了 派生类 is-a base 类, 覆盖 base 类的 non-virtual 毫无疑问违反了这一点.如果你想派生类能够改变函数的实现方式,那么你应该将基类中的这个函数定义为 virtual 使得派生类可以重写它.
37. 不重新定义继承而来的缺省数值
virtual 是动态绑定, 但省缺参数值却是静态绑定的.
1 | class Shape { |
但是当你调用一个定义于 派生类的 virtual 函数的同时,却使用的是 base class 中指定的省缺值.
1 | pr->draw(); //pr的类型是 Shape* ,调用 Rectangle::draw(Shape::Red) |
我们不该重新定义继承而来的缺省数值,所以你可能会妥协以下,写出下面糟糕的代码
1 | // 如果你想改变 Shape 内的省缺参数,你不得不也修改 Rectangle 中的代码 |
当virtual 函数实现出现问题时, 聪明的做法是考虑替代方案, 条款35指出 virtual 函数的替代设计.
1 | class Shape { |
38. 用复合关系表示has-a 关系或 根据某物实现出
条款 13 中指出, public 继承是一种 “is-a”关系,而复合关系表示一种 has-a 或 is-implemented-in-terms-of 关系.
在 STL 中, 很多容器是通常 复合(neiqian)另一种容器来实现的,现在你想通过继承关系来看看是否能够实现同样的功能.
1 | //你想复用 std::list 来实现 STL 中的 Set |
看起来不错是吗? 实际上有些东西完全错误了. 我们反复的提到了”public 继承是一种’is-a’关系”,那么对于 std::list 为真的事情,对于Set 也应该为真.即: “Set 是一种 std::list”.
额,你也发现不对劲了吧. Set 并不是一种 std::list .
复用的意思和 public 继承完全不同.
39. 明智而审慎的使用 private 继承
private 继承并不意味 is-a 关系,它意味着 is-implemented-in-terms-of(根据某物实现出).- 如果 classes 的关系是 private, 编译器不会自动将一个 derived class 对象转化成 一个 base 对象. - 由 private base 类继承而来的所有成员,不管是 protected 或 public 属性, 在derived class 中都会变成 private 属性.
1 | class Timer { |
上面的设计还可以使用 public 继承加复用的方式实现.
1 | class Widget { |
这样更复杂了,但是更加合理. 这表明出 private 比 复用的级别更低,我们要优先选择复用.但是对于一些特殊的情景, private 可能更适用.
1 | class A { |
使用复合可调用不了 func 函数,但是 private 继承却可以调用.另外 当面对 empty base 最优化时,你也应该z选择private 继承.