第6章 继承与面向对象设计
Inheritance and Objected-Oriented Design
这一章节关于C++的面向对象编程OOP,讨论以下问题:继承可以是单一继承或多重继承,每一个继承连接可以是 public,protected,private,也可以是 virtual 或 non-virtual .然后是成员函数的选项:virtual?non-virtual?pure virtual?Y以及成员函数和其他语言特性的交互影响:缺省参数值与 virtual 函数有什么交互影响?继承如何影响C++的名称查找规则?设计选项有哪些?如果 class 的行为需要修改,virtual 函数是最佳选择吗?
public 继承意味着"is-a",如果尝试让它带有其他意义,则是错误的.同样的道理,virtual 函数意味"接口必须被继承",non-virtual 函数意味着"接口和实现都必须被继承".
条款32: 确定你的 public 继承塑模出is-a关系
Make sure public inheritance models "is-a"
以C++进行面向对象编程,最重要的一个规则是:public inheritance意味着"is-a"的关系.
如果令 class D("Derived")以 public 形式继承 class B("Base"),便是告诉C++编译器说,每一个类型为D的对象同时也是一个类型为B的对象.意思是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念.
C++对于"public继承"严格奉行上述见解,考虑以下例子:
class Person { ... };
class Student : public Person { ... };
根据生活经验知道,每个学生都是人,但并非每个人都是学生.这便是这个继承体系的主张.
如上所述,在C++领域中,任何函数如果期望获得一个类型为Person(或pointer-to-Person或reference-to-Person)的实参,都也愿意接受一个Student对象(或pointer-to-Student或
reference-to-Student):
void eat(const Person& p);
void study(const Student& s);
Person p;
Student s;
eat(p); // ok
eat(s); // ok
study(s); // ok
study(p); // error!
这个论点只对 public 继承才成立.只有当Student以 public 形式继承Person,C++的行为才如上所述. private 继承的意义与此完全不同(详见条款39).
public 继承和"is-a"之间的等价关系看起来很简单,但有时候直觉会误导人.例如企鹅是一种鸟,鸟可以飞,这也是事实.如果天真地以C++描述这层关系,如下:
class Bird {
public:
virtual void fly(); // bird can fly
};
class Penguin : public Bird {
...
};
突然间混乱,因为这个继承体系说企鹅可以飞!!!怎么回事?
这是由于语言的不严谨造成的,鸟可以飞,其实是说大多数鸟可以飞.因此也有一小部分鸟不会飞,因此以下继承关系才是准确的:
class Bird {
... // 没有声明fly函数
};
class FlyingBird : public Bird {
public:
virtual void fly();
};
class Penguin : public Bird {
... // 没有声明fly函数
};
这反映出一个事实,并不存在一个"适用于所有软件"的完美设计.所谓
最佳设计,取决于系统希望做什么事,包括现在与未来.
另外一种思想派别处理所谓"所有的鸟都会飞,企鹅是鸟,但是企鹅不会飞"的问题,就是为企鹅重新定义fly函数,令它产生一个运行期间错误:
void error(const std::string msg); // 定义于另外某处
class Penguin : public Bird {
public:
virtual void fly() { error("Attempt to make a penguin fly!"); }
...
};
很重要的是,必须认知这里所说的某些东西可能和所想的不同,这里并不是说"企鹅不会飞",而是说"企鹅会飞,但尝试那么做是一种错误".
如何描述期间的差异?
从错误被侦测出来的时间点来看,"企鹅不会飞"这一限制可由编译器强制实施,
但若违反"企鹅尝试飞行,是一种错误"这一条规则,只有运行期间才能检测出来.
为了表现"企鹅不会飞"的限制,就不可以为Penguin定义fly函数.
这和采取"令程序于运行期发生错误"的方法极为不同,若以那种做法,编译器不会对p.fly调用形式发出任何警告. 条款18所述:好的接口可以防止无效的代码通过编译,
因此应该宁可采取"在编译期拒绝企鹅飞行"的设计,而不是"只在运行期才能侦测它们"的设计.
接下来考虑一个几何学的问题,正方形和矩形之间的关系. class Square是否应该以 public 形式继承 class Rectangle?
"当然如此,每一个正方形都是矩形,矩形不一定是正方形",这是真理.但在实际继承体系中...,考虑以下代码:
class Rectangle {
public:
virtual void setHeight(int newHeight);
virtual void setWidth(int newWidth);
virtual int height() const;
virtual int width() const;
...
};
void makeBigger(Rectangle& r) {
int oldHeight = r.height;
r.setWidth(r.width() + 10);
assert(r.height() == oldHeight); // 判断r的高度是否未曾改变
}
显然,上述的assert结果永远为真,因为makeBigger只改变r的宽带,r的高度未被改变.
现在考虑这段代码,其中使用 public 继承,允许正方形被视为一种矩形:
class Square : public Rectangle { ... };
Square s;
...
assert(s.width() == s.height());
makeBigger(s);
assert(s.width() == s.height());
对于正方形而言,这两个assert都是真的,因为它的高度和宽度一定相等.但这两个assert对于矩形而已,不可能都为真.但是正方形是继承矩形的,两者结果应该一致.这竟然发生了矛盾.
因此,public 继承和平时的认知是有不同的.对于一些继承而言是可能接近事实真相的,但也可能不.
is-a并非是唯一存在于 class 之间的关系.另两个常见的关系是has-a(有一个)和is-implemented-in-terms-of(根据某物实现出),详见条款38和条款39.
注意:
"public继承"意味is-a.适用于base class 身上的每一件事情一定也适用于derived class 身上,因为每一个derived class 对象也都是一个base class 对象.