(公有)继承的概念看起来很简单,进一步分析,会发现它由两个可分的部分组成:函数接口的继承和函数实现的继承.
<span style="font-size:18px;">class Shape {
public:
virtual void draw() const = 0;
virtual void error(const string& msg);
int objectID() const;
};</span>
class Rectangle: public Shape { ... };
class Ellipse: public Shape { ... };
纯虚函数 draw 使得 Shape 成为一个抽象类。所以,用户不能创建 Shape类的实例,只能创建它的派生类的实例.
首先看纯虚函数 draw。纯虚函数最显著的特征是:它们必须在继承了它们的任何具体类中重新声明,而且它们在抽象类中往往没有定义.
定义纯虚函数的目的在于,使派生类仅仅只是继承函数的接口
有时,声明一个除纯虚函数外什么也不包含的类很有用。这样的类叫协议类(Protocol class) ,它为派生类仅提供函数接口,完全没有实现。协议类在条款 34 中介绍过,并将在条款 43 再次提及.
简单虚函数的情况和纯虚函数有点不一样。照例,派生类继承了函数的接口,但简单虚函数一般还提供了实现,派生类可以选择改写它们或不改写它们.
声明简单虚函数的目的在于,使派生类继承函数的接口和缺省实现
具体到 Shape::error,这个接口是在说,每个类必须提供一个出错时可以被调用的函数,但每个类可以按它们认为合适的任何方式处理错误。如果某个类不想做什么特别的事,可以借助于 Shape 类中提供的缺省出错处理函数。也就是说,Shape::error 的声明是在告诉子类的设计者,"你必须支持 error 函数,但如果你不想写自己的版本,可以借助 Shape 类中的缺省版本。"
但是,为简单虚函数同时提供函数声明和缺省实现是很危险的。
class Airport { ... }; // 表示飞机
class Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void Airplane::fly(const Airport& destination)
{
飞机飞往某一目的地的缺省代码
}
class ModelA: public Airplane { ... };
class ModelB: public Airplane { ... };
为了表明所有飞机都必须支持 fly 函数,而且因为不同型号的飞机原则上都需要对 fly 有不同的实现, 所以Airplane::fly 被声明为 virtual 。但是,为了避免在 ModelA 类和 ModelB 类中写重复的代码, 缺省的飞行行为是由 Airplane::fly函数提供的,ModelA 和 ModelB 继承了这一函数。
这是典型的面向对象设计。两个类享有共同的特征(实现 fly 的方式),所以这一共同特征被转移到基类,并让这两个类来继承这一特征。
现在假设 XYZ 公司发了大财,决定引进一种新型飞机, C 型。 C 型和 A 型、B 型有区别,特别是,飞行方式不一样。
XYZ 的程序员在上面的层次结构中为 C 型增加了一个类,但因为急于使新型飞机投入使用,他们忘了重新定义 fly 函数:
class ModelC: public Airplane {
... // 没有声明 fly 函数
};
然后,在程序中,他们做了类似下面的事:
Airport JFK(...); // JFK 是纽约市的一个机场
Airplane *pa = new ModelC;
pa->fly(JFK); // 调用 Airplane::fly
这里的问题不在于 Airplane::fly 具有缺省行为,而在于 ModelC 可以不用明确地声明就可以继承这一行为。
幸运的是,可以很容易做到为子类提供缺省行为同时只是在子类想要的时候才给它们。窍门在于切断虚函数的接口和它的缺省实现之间的联系。
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
protected:
void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
飞机飞往某一目的地的缺省代码
}
注意 Airplane::fly 已经变成了纯虚函数,它提供了飞行的接口。缺省实现还是存在于 Airplane 类中,但现在它是以一个独立函数(defaultFly)的形式存在的。ModelA 和 ModelB 这些类想执行缺省行为的话,只用简单地在它们的 fly函数体中对 defaultFly 进行一个内联调用。
class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
};
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination)
{ defaultFly(destination); }
}
对于 ModelC 类来说,它不可能无意间继承不正确的 fly 实现。 因为 Airplane中的纯虚函数强迫 ModelC 提供它自己版本的 fly 。
Airplane::defaultFly 是一个非虚函数也很重要。因为没有子类会重新定义这个函数。
一些人反对将接口和缺省实现作为单独函数分开,例如上面的 fly 和defaultFly 。他们认为,起码这会污染类的名字空间,因为有这么多相近的函数名称在扩散。然而他们还是赞同接口和缺省实现应该分离。怎么解决这种表面上存在的矛盾呢?可以借助于这一事实:纯虚函数必须在子类中重新声明,但它还是可以在基类中有自己的实现。下面的 Airplane 正是利用这一点重新定义了一个纯虚函数:
class Airplane {
public:
virtual void fly(const Airport& destination) = 0;
...
};
void Airplane::fly(const Airport& destination)
{
飞机飞往某一目的地的缺省代码
}
class ModelA: public Airplane {
public:
virtual void fly(const Airport& destination)
{Airplane::fly(destination); }
};
class ModelB: public Airplane {
public:
virtual void fly(const Airport& destination)
{Airplane::fly(destination); }
...
};
class ModelC: public Airplane {
public:
virtual void fly(const Airport& destination);
...
};
void ModelC::fly(const Airport& destination)
{
ModelC 飞往某一目的地的代码
}
从本质上说,fly 已经被分成两个基本部分了。它的声明说明了它的接口(派生类必须使用) ,而它的定义说明了它的缺省行为(派生类可能会使用,但要明确地请求)。
最后,来谈谈 Shape 的非虚函数,objectID 。当一个成员函数为非虚函数时,它在派生类中的行为就不应该不同。实际上,非虚成员函数表明了一种特殊性上的不变性,因为它表示的是不会改变的行为---- 不管一个派生类有多特殊。所以,声明非虚函数的目的在于,使派生类继承函数的接口和强制性实现。
理解了纯虚函数、简单虚函数和非虚函数在声明上的区别,就可以精确地指定你想让派生类继承什么:仅仅是接口,还是接口和一个缺省实现?或者,接口和一个强制实现?因为这些不同类型的声明指的是根本不同的事,所以在声明成员函数时一定要从中慎重选择。只有这样做,才可以避免没经验的程序员常犯的两个错误。
第一个错误是把所有的函数都声明为非虚函数。这就使得派生类没有特殊化的余地;非虚析构函数尤其会出问题(参见条款 14) 。当然,设计出来的类不准备作为基类使用也是完全合理的(条款 M34 就给出了一个你会这样做的例子) 。这种情况下,专门声明一组非虚成员函数是适当的。但是,把所有的函数都声明为非虚函数,大多数情况下是因为对虚函数和非虚函数之间区别的无知,或者是过分担心虚函数对程序性能的影响(参见条款 M24) 。而事实上是:几乎任何一个作为基类使用的类都有虚函数(再次参见条款 14)。
另一个常见的问题是将所有的函数都声明为虚函数。有时这没错---- 比如,协议类(Protocol class)就是证据(参见条款 34) 。但是,这样做往往表现了类的设计者缺乏表明坚定立场的勇气。一些函数不能在派生类中重定义,只要是这种情况,就要旗帜鲜明地将它声明为非虚函数。不能让你的函数好象可以为任何人做任何事---- 只要他们花点时间重新定义所有的函数。