以C++进行面向对象编程,最重要的一个规则是:public继承意味着is-a关系。
如果你令class D(Derived)以public的形式继承class B(Base),你便是告诉编译器,每一个类型D也是一个类型B的对象,反之不成立。B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。
C++对于public继承严格奉行以上见解。考虑以下例子:
class Person {...};
class Student : public Person {...};
根据生活经验我们知道,每个学生都是人,但并非所有人都是学生。这便是这个继承体系的主张。人的概念比学生更一般化,学生是人的一种特殊形式。
于是,在C++中,任何函数如果期望获得一个类型为Person(或指针或引用)的实参,也都愿意接收一个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继承的意义与此完全不同,至于protected继承,那是一种意义至今令我困惑的东西。
public继承和is-a之间的等价关系听起来颇为简单,但有时候你的直觉可能会误导你。举个例子:企鹅是一种鸟,这是事实;鸟可以飞,这也是事实。如果我们天真地以C++描述这层关系,结果如下:
class Bird {
public:
virtual void fly(); // 鸟可以飞
...
};
class Penguin : public Bird { // 企鹅是一种鸟
...
};
然而这个体系说企鹅可以飞,而我们知道那不是真的。
我们说鸟会飞的时候,我们真正的意思并不是说所有的鸟都会飞,我们说的只是一般的鸟都有飞行能力。严谨一点,有数种鸟不会飞。我们使用以下继承关系,它表现出较佳的真实性:
class Bird {
...
};
class FlyingBird : public Bird {
public:
virtual void fly();
...
};
class Penguin : public Bird {
... // 没有声明fly函数
};
这样的继承体系更能反映我们真实的意思。
另一种思想处理这个问题,就是为企鹅重新定义fly函数,令他产生一个运行期错误:
```cpp
void error(const std::string& msg);
class Penguin : public Bird {
public:
virtual void fly() { error("Attempt to make a penguin fly!"); }
...
};
如何表现鸟的差异?从错误被侦测出的时间点看,“企鹅不会飞”这一限制可由编译期强制实施,但若违反“企鹅尝试飞行是一种错误”这一规则,只有运行期能被检测出来。
为了表现“企鹅不会飞”的限制,你不可以为Penguin定义fly函数:
class Bird {
... // 没有声明fly函数
};
class Penguin : public Bird {
... // 没有声明fly函数
};
现在,如果你试图让企鹅飞,编译期就会报错。
条款18说:好的接口可以防止无效的代码通过编译,因此你应该宁可采取在编译期拒绝企鹅飞行的设计,而不是只在运行期才能侦测它们的设计。
正方形是一种特殊的矩形,反之则不然。但是考虑以下代码:
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() * 2);
assert(r.height() == oldHeight);
}
显然,上述assert永远为真,因为makeBigger只改变r的宽度,r的高度从未被更改。
现在考虑这段代码,其中使用public继承,允许正方形被视为一种矩形:
class Square : public Rectangle {...};
Square sl
...
assert(s.width() == s.height()); // 对所有正方形一定为真
makeBigger(s);
assert(s.width() == s.height()); // 对所有正方形应该仍然为真
但现在我们遇到了一个问题。我们该如何调解下面各个断言:
- 在调用makeBigger之前,s的高度和宽度相同;
- 在makeBigger函数内,s的宽度改变,但高度不变;
- makeBigger返回后,s的高度再度和其宽度相同。
本例根本的困难是,某些可施行于矩形上的事(例如宽度可独立于高度被修改)却不可施行于正方形上(宽度和高度总是应该相等)。但是public继承主张,能够施行于基类身上的每件事,也可以施行于派生类对象上。在正方形和矩形的例子中,public继承建模它们之间的关系并不准确。
请记住
- public继承意味着is-a。适用于基类身上的每件事情也一定适用于派生类上,因为每个派生类对象也都是一个public对象。