33.public继承:is-a关系

前言

 
以c++进行面向对象编程时,最重要的一条规则是:public inheritance意味着is-a的关系


is-a关系

 
如果你令class d public derived from class b,你等于在同时告诉c++编译器以及代码读者,每一个类型为d的对象同时也是一个类型为b的对象,反之不成立。
这也可以表示为“每一个需要b对象的地方,d对象也同样适用”,因为每一个d对象也是一个b对象。
该定义只对public继承有效,private继承的意义与此完全不同(详见Effective C++ 40),至于protected继承,其意义十分暧昧。


问题实例

 
大多数人都认为is-a关系十分简单,但实际并非如此,因为直觉容易误导我们。

鸟与企鹅

企鹅是一种鸟,这是再自然不过的。鸟可以飞,这也是符合直觉的。于是…

1
2
3
4
5
6
7
8
class Bird{
public:
virtual void fly();//鸟会飞
...
};
class Penguin:public Bird{//企鹅是一种鸟
...
};

实际上企鹅不会飞,但企鹅却继承了fly接口,这明确表明当前继承体系存在问题。

重塑继承体系

你或许会意识到这是因为不严谨的语言表述导致我们吃了大亏。实际上是这样的:企鹅是鸟,但不是所有鸟都会飞,有的会有的不会。所以这才是正确的继承体系:

1
2
3
4
5
6
class bird {...};
class flyingbird:public bird{
public:
virtual void flying();
};
class penguin:public bird {};

这种写法针对fly这个接口无疑是成功的,但实际上对于某些软件系统而言,它根本不用去关心鸟会不会飞,此时建立一个flyingbird无疑是不明智的。这引出了一个事实:
世界上并不存在一个完美适用于所有软件的完美设计。所谓的最佳设计,取决于系统现在以及未来所关注的重点。

运行期报错处理

有人会为企鹅重新定义一个fly函数,令其产生一个运行期错误:

1
2
3
4
5
6
class penguinpublic bird
public
virtual void fly(){
error(“can not fly”);
}
};

这种写法是失败的,它等于在说:企鹅会飞,但是一飞就报错。这违背了我们的设计理念:让接口易于使用,不被误用。(Effective C++ 19)


矩形与正方形

squre应该public derived from rectangle吗?也许你会毫不犹豫地肯定这一观点,“正方形必然是矩形,但反之不成立”。请考虑如下程序:

1
2
3
4
5
6
class Rectangle{
public:
virtual void setHeight(int newHeight);
...//setWidth
};
class Square:public Rectangle {...};

Square继承了setHeight等接口,也就是说,我们可以通过调用成员函数,导致正方形长宽不等,那这种对象还能被称为正方形吗。(实际上,在正方形的接口中我们必须确保setHeight之后必须执行setWidth)


classes之间的关系

 
is-a并非是classes之间唯一的关系,另外两个常见的是has-a与is-implemented-in-terms-of。


总结

 
public继承意味着is-a,is-a意味着适用于base对象身上的每一件事情一定也适用于derived对象身上,因为每一个derived对象也一定是一个base对象。