Jiahonzheng's Blog

C++ 正确认识公有继承

字数统计: 632阅读时长: 2 min
2018/04/08 Share

Item 32: Make sure public inheritance models “is-a”.

在 C++ 面向对象程序设计中,最重要的规则便是:公有继承应当是 “is-a” 的关系。

Derived 公有继承自 Base 时,我们得清晰地认识: BaseDerived 的抽象,任何时候 Derived 都可以代替 Base 适用。

例如,Student 继承自 Person ,那么接受 Person 类型参数的函数,也应当能够接受 Student 类型参数。

1
2
3
4
5
6
void eat(const Person& p);
void study(const Student& s);

Person p; Student s;
eat(p); eat(s);
study(p); study(s);

语言的二义性

上述 PersonStudent 的例子很好理解,也很符合直觉。但有时情况却会不同,比如 Penguin 继承自 Bird ,但我们知道鸟会飞,企鹅不会飞。

1
2
3
4
5
6
7
class Bird{
public:
virtual void fly();
};
class Penguin: public Bird{
// fly??
};

这时我们可能困惑 Penguin 到底是否应该有 fly 方法。其实,这个问题来源于自然语言的二义性:严格地考虑,“鸟会飞”并不代表“所有的鸟都会飞”,所以我们应该对会飞的鸟单独进行建模。

1
2
3
4
5
6
class Bird{};
class FlyingBird: public Bird{
public:
virtual void fly();
};
class Penguin: public Bird{};

这样,当我们调用 penguin.fly() 时,便会编译错误。当然,有另一种反模式的解决办法,就是让 Penguin 继承自拥有 fly() 方法的 Bird ,但在 Penguin::fly() 中抛出异常。这两种方式,在概念上是有区别的:前者是说企鹅不能飞,后者是企鹅可以飞,但飞了会出错。

由于“接口应当设计得不容易被误用”,所以最好将错误从运行时提前到编译时,所以前者更好!

错误的继承

生活的经验给了我们关于对象继承的直觉,然而并不一定正确,比如我们来实现一个正方形继承自矩形。

1
2
3
4
5
6
7
8
9
10
11
12
class Rect{};
class Square: public Rect{};
void makeBigger(Rect& r) {
int oldHeight = r.height();
r.setWidth(r.width() + 10);
assert(r.height() == oldHeight);
}

Square s;
assert(s.width() == s.height()); // true
makeBigger(s);
assert(s.width() == s.height()); // false

根据正方形的定义,宽高相等是任何时候都需要成立的。然而 makeBigger 却破坏了正方形的属性,所以正方形并不是一个矩形(因为矩形需要有这样一个性质:增加宽度时,高度不会变)。

故,Square 继承自 Rect 是错误的做法,C++ 类的继承比现实世界中的继承关系更加严格:任何使用于父类的性质都要适用于子类

CATALOG
  1. 1. 语言的二义性
  2. 2. 错误的继承