一. 内容
-
以 C++ 进行面向对象编程,最重要的一个规则是:
public inheritance (公开继承)意味着 is-a 的关
系,请把这个规则牢牢记在心中。 -
也就是说,如果你令 class D 以 public 的方式继承 class B,你便是告诉 C++ 编译器和你的客户:
每一个类型为 D 的对象,同时也是一个类型为 B 的对象,反之则不成立
。类型 B 比 类型 D 表现出更一般的概念,而类型 D 比类型 B 表现出更特殊化的概念。B 对象可使用到的地方,D 对象都可以派上用场
。反之,如果你需要对象 D ,B 对象并不能效劳,因为 B 对象并不具备 D 对象所含有的特殊化信息
。 -
以上论述只对 public 继承才成立,private 继承的意义与此完全不同,见条款39,至于 protected 继承,那是一种意义至今令我疑惑的东西。
-
虽然 public 继承意味着 is-a 的关系看似简单,但有时候
你的直觉会误导你
。举个例子:企鹅是一种鸟,这是事实。鸟可以飞,这也是事实。如果我们写下以下代码:class Bird { public: virtual ~Bird(); virtual void Fly(); //鸟可以飞 }; class Penguin : public Bird { //企鹅是一种鸟 public: //... };
在这个例子中,我们的实现和所说的事实存在着误差,事实上我们说鸟可以飞,不是说所有的鸟可以飞,如果严谨一些,有一些鸟是不会飞的。所以修改如下:
class Bird { //没有声明 fly 函数 public: virtual ~Bird(); }; class FlyingBird : public Bird { //会飞的鸟 public: virtual void Fly(); }; class Penguin : public Bird { public: //... };
即便如此,我们可能仍未处理好这些鸟事,因为对于某些系统而言,可能根本不需要区分会飞的鸟和不会飞的鸟。这反映出一个的问题:
世界上并不存在一个适用于所有软件的完美设计。所谓最佳设计,取决于系统希望去做什么事,包括现在和未来
。 -
处理鸟事的另一种流派是:为企鹅重新定义 fly 函数,令它产生一个运行期错误:
class Bird { //没有声明 fly 函数 public: virtual ~Bird(); virtual void Fly(); }; class Penguin : public Bird { public: virtual void Fly() override; //抛出错误 }; inline void Penguin::Fly() { std::cout << "企鹅不会飞,你在想什么?" << "\n"; }
然而这种做法其实和原本你的想法不同:这里不是说 企鹅不会飞,而是说 企鹅会飞,但尝试那样做 是一种错误。如何描述两者的差异?可以从发现错误的时机来看,企鹅不会飞
可以在编译期检测出来
,但若违反企鹅尝试飞行,是一种错误,只有在运行期才能侦测出来
。为了在编译器抛出错误,你不可以为 Penguin 定义 fly 函数:
class Bird { //没有声明 fly 函数 public: virtual ~Bird(); }; class Penguin : public Bird { //没有声明 fly 函数 public: //... }; inline void Try(){ Penguin P; P.fly(); //编译器报错! }
这种做法和令程序在运行期发生错误极为不同,以后者做法,编译器不会对 P.fly() 的调用发出任何抱怨。条款18说过,
好的接口可以防止无效的代码通过编译
,因此你应该宁可在编译期拒绝企鹅飞行,而不是在运行期侦测企鹅飞行的错误
。 -
另一个有趣的例子是:矩形和正方形的例子。数学知识告诉我们:正方形是一种矩形,你会很容易的想到让 正方形 public 继承 矩形,毕竟这是 is-a 的关系。但事实并非如此,例如 矩形的长宽可不一致,可以独立修改,
按照 public 继承的观点,作用于 base class 的事情, derived class 应同样适用
。但正方形的长宽应始终保持一致。所以它们之间的关系用 public 继承并不合适。虽然代码上编译器不会有任何报错,但它们的行为是错误的。这也让我们明白一个道理:代码通过编译并不意味着可以正确运作。
二. 总结
- public 继承意味 is-a。适用于 base classes 身上的每一件事情一定也适用于 derived classes 身上,因为每一个 derived class 对象也都是一个 base class 对象。