以c++进行面向对象编程时,最重要的一个规则是:public inheritance(公开继承)意味着“is-a”的关系。
如果你令class D以public形式继承class B,便就是告诉c++编译器说,每一个类型为D的对象同时也是一个类型为B的对象,反之不成立。意思是B比D表现出更一般化的概念,而D比B表现出更特殊化的概念。主张B对象可派上用场的任何地方,D对象一样可以派上用场,因为每一个D对象都是一种B对象,反之如果你需要一个D对象,B对象无法效劳,虽然每个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;//p是人
Student s;//s是学生
eat(p);//没问题,p是人
eat(s);//没问题,s是学生,而学生也是人
study(s);//没问题,s是学生
study(p);//错误,p不是学生
这个论点只对public继承才成立。只有当Student以public形式继承Person,c++的行为才会像这样描述,private继承的意义与此完全不同,至于protected继承,在这里不详细解释。
public继承和is-a之间的等价关系听起来颇为简单,但有时候你的直觉可能会误导你。举个例子,企鹅是一种鸟,这是事实。鸟可以飞,这也是事实,如果以c++描述这层关系,结果如下:
class Bird{
public:
virtual void fly();//鸟可以飞
...
};
class Penguin:public Bird{//企鹅是一种鸟
...
};
突然间我们遇上了乱流,因为该继承体系说企鹅可以飞,而我们知道这不是真的。
在这个例子中,我们成了不严谨下的牺牲品。当我们说鸟会飞的时候,我们真正的意思并不是说所有的鸟都会飞,我们要说的是一般的鸟都有飞行能力。再谨慎一点那就是我们应该承认一个事实:有数种鸟不会飞。我们来到以下继承关系:
class Bird{
...//没有声明fly函数
};
class FlyingBird:public Bird{
public:
virtual void fly();
...
};
class Penguin:public Bird{
...//没有声明fly函数
};
这样的继承体系比原先的设计更能忠实反映我们真正的意思。
即便如此,此刻我们仍然未能完全处理好这件事,因为对某些系统而言,可能不需要区别会飞的鸟和不会飞的鸟。如果程序的作用是在处理鸟喙和鸟翅,完全不在乎飞行,原先的“双class继承体系”或许就令人比较满意。这体现出了一个事实,并不存在一个“适用于所有软件”的完美设计。所谓最佳设计,取决于系统希望做什么事。
另外一种思想派别处理所谓的“所有的鸟都会飞,企鹅是鸟,但是企鹅不会飞”的问题,就是为企鹅重新定义fly函数,令它产生一个运行期错误:
void error(const std::string& msg);//定义于另外某处
class Penguin:public Bird{
public:
virtual void fly(){
error("Attempt to make a penguin fly");
...
}
};
重要的是必须认为ie这里所说的某些东西可能和你所想的不同,这里并不是说“企鹅不会飞”,而是说“企鹅会飞,但尝试那么做是一种错误”。
如何描述其间的差异?从错误被侦察出来的时间点分析,“企鹅不会飞”这一限制可由编译期强制实施,但若违反“企鹅尝试飞行,是一种错误”这一条规则,只有运行期才能检测出来。
为了表现“企鹅不会飞”的限制,不可以为Penguin定义fly函数:
class Bird{
...//没有声明fly函数
};
class Penguin:public Bird{
...//没有声明fly函数
};
现在,假设 你试图让企鹅飞,编译器不会通过:
Penguin p;
p.fly();//报错
这和采取“令程序于运行期间发生错误”的解法极为不同。若以那种做法,编译器不会对p.fly调用式发出任何抱怨。条款18解释过:好的接口可以防止无效的 代码通过编译。因此你应该宁可采取“在编译器拒绝企鹅飞行”的设计,而不是“只在运行期才能侦测它们”的设计。