Effective C++ 读书笔记 Item32 确保public继承是"is a"的关系

朱雀 2020-07-05 02:32 783阅读 1赞

C++面向对象程序设计中,最重要的规则便是:public继承应当是”is-a”的关系。当Derived public继承自Base时, 相当于你告诉编译器和所有看到你代码的人:Base是Derived的抽象,Derived就是一个Base,任何时候Derived都可以代替Base使用。

比如一个Student继承自Person,那么Person有什么属性Student也应该有,接受Person类型参数的函数也应当接受一个Student

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

语言的二义性

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

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

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

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

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

哪种实现方式好呢?[Item 18]中提到,接口应当设计得不容易被误用,最好将错误从运行时提前到编译时。所以前者更好!

错误的继承

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

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

根据正方形的定义,宽高相等是任何时候都需要成立的。然而makeBigger却破坏了正方形的属性, 所以正方形并不是一个矩形(因为矩形需要有这样一个性质:增加宽度时高度不会变)。即Square继承自Rect是错误的做法。 C++类的继承比现实世界中的继承关系更加严格:任何适用于父类的性质都要适用于子类!

本节我们谈到的是”is-a”关系,类与类之间还有着其他类型的关系比如”has-a”, “is-implemented-in-terms-of”等。这些在Item-38和Item-39中分别介绍。

发表评论

表情:
评论列表 (有 0 条评论,783人围观)

还没有评论,来说两句吧...

相关阅读