条款34:区分接口继承和实现继承
Differentiate between inheritance ofinterface and inheritance of implementation
public inheritance是由两个相互独立的部分组成的:inheritance of function interfaces(函数接口继承)和 inheritanceof function implementations(函数实现继承)。这两种 inheritance之间的差异很像函数声明和函数定义之间的差异。
作为类的设计者,有的时候你想要派生类只继承成员函数的接口(即声明);有的时候你想要派生类既继承接口也继承实现,但允许它们替换继承到的实现;还有的时候你想要派生类继承一个函数的接口和实现,而不允许替换。考虑在图形应用程序中表示几何图形的类继承体系:
class Shape {
public:
virtual void draw() const = 0;
virtual void error(conststd::string& msg);
int objectID() const;
...
};
classRectangle: public Shape { ... };
class Ellipse: public Shape { ... };
Shape 是一个抽象类,它的 pure virtual function函数表明了这一点。客户不能创建 Shape class 的实例,只能创建从它继承的类的实例。
l 成员函数的接口总是会被继承。publicinheritance 意味着 is-a,所以对基类成立的任何东西,对于它的派生类也成立。因此,如果一个函数适用于某类,它也一定适用于它的派生类。
Shape class 中声明了三个函数。第一个draw,在一个明确的显示设备上画出当前对象(pure virtual)。第二个error,如果成员函数需要报告一个错误就会调用它(impure virtual)。第三个objectID,返回当前对象的唯一整型标识符(non-virtual)。
首先考虑pure virtual函数draw:
virtual void draw() const= 0;
pure virtual函数有两个最显著的特性:1.它们必须被任何继承它们的具象类重新声明 和2.抽象类中一般没有它们的定义。
l 声明一个pure virtual函数的目的是使派生类只继承函数接口。
这对Shape::draw函数再合理不过,因为它要求所有的Shape对象必须是可绘制的,但是 Shape类本身不能为这个函数提供一个合乎情理的缺省实现。例如,画椭圆和画矩形的算法是非常不同的,Shape::draw的声明告诉派生类的设计者:“你必须提供一个draw函数,但我不干涉你怎么实现它。”
顺便提一句,我们还是可以调用Shape::draw,而C++也不会抱怨什么,但是调用它的唯一方法是明确指出类名称:
Shape*ps = new Shape; // error! Shape is abstract
Shape *ps1 = new Rectangle; // fine
ps1->draw(); // calls Rectangle::draw
Shape *ps2 = new Ellipse; // fine
ps2->draw(); // calls Ellipse::draw
ps1->Shape::draw(); // calls Shape::draw
ps2->Shape::draw(); // calls Shape::draw
impure virtual函数和 pure virtual有一点不同。派生类照常还是继承函数的接口,但是 impurevirtual函数会提供一份实现代码,派生类可能override它。
l 声明impure virtual函数的目的是让派生类继承该函数的接口和缺省实现。
考虑 Shape::error 的情况:
virtual void error(conststd::string& msg);
其接口表示,每一个类必须支持一个在遭遇到错误时被调用的函数,但是每一个类可以自由处理错误。如果某个类不需要做什么特别的事情,它可以用Shape中错误处理的缺省版本。也就是说,Shape::error的声明告诉派生类的设计者:“你应该支持一个error函数,但如果你不想自己写,你可以用Shape中的缺省版本。”
结果是:允许impure virtual函数既指定函数接口又指定缺省实现是危险的。考虑XYZ航空公司的飞机继承体系。XYZ只有两种飞机,Model A和Model B,它们都按照同样的方法飞行:
classAirport { ... };
class Airplane {
public:
virtual void fly(const Airport&destination);
...
};
voidAirplane::fly(const Airport& destination)
{缺省代码,将飞机飞至指定目的地}
classModelA: public Airplane { ... };
class ModelB: public Airplane { ... };
为了表述不同机型理论上需要不同的对fly的实现,Airplane::fly被声明为virtual。然而,为了避免在ModelA和ModelB中重复的代码,缺省的飞行行为由 Airplane::fly的函数体提供,供ModelA 和 ModelB继承。
这是一个经典的面向对象设计。因为两个类共享一个通用特性(实现 fly的方法),所以这个通用特性被转移到基类之中,并由两个类来继承这个特性。现在假设XYZ公司决定引进一种新机型ModelC。ModelC的fly方式与 ModelA、ModelB 不同。特别是,它的飞行不同。
ModelC应该明确说出它要的fly方式,以避免不正确地继承基类中缺省的fly函数:
class Airplane {
public:
virtual void fly(const Airport&destination) = 0;
...
protected:
void defaultFly(const Airport&destination);
};
void Airplane::defaultFly(const Airport& destination)
{缺省行为,将飞机飞至指定目的地}
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); }
...
};
class ModelC: public Airplane {
public:
virtual void fly(const Airport&destination);
...
};
void ModelC::fly(const Airport& destination)
{将C型飞机飞至指定目的地}
Airplane::defaultFly现在成了protected,因为它完全是Airplane和它的派生类的实现细节。Airplane::defaultFly 是一个 non-virtual函数)这一点也很重要。因为派生类不应该重定义这个函数。
一些人反对以不同的函数分别提供接口和缺省实现,就像上面的fly和defaultFly那样。然而他们仍然同意接口和缺省实现应该被分开。这个矛盾可以通过利用以下事实解决:purevirtual函数必须在派生类中重新声明,但是它们也可以有自己的实现:
class Airplane {
public:
virtual void fly(const Airport&destination) = 0;
...
};
void Airplane::fly(constAirport& destination) // purevirtual函数实现
{ 缺省行为,将飞机飞至指定目的地 }
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)
{将C型飞机飞至指定目的地}
除了用purevirtua纯虚拟函数Airplane::fly的函数体代替了独立函数 Airplane::defaultFly之外,这是一个和前面几乎完全相同的设计。本质上fly可以被拆成两个部分:它的声明指定了它的接口(这是派生类必须使用的),而它的定义指定它的缺省行为(这是派生类可以使用的,但只在他们明确要求时)。
最后,我们看看Shape的non-virtual函数objectID:
int objectID() const;
当成员函数是non-virtual,不应该指望它在派生类中的行为会有所不同。实际上,一个non-virtual成员函数所表现的不变性凌驾于特异性,因为不论一个=派生类)=变得多么特殊,它都把它看作是不允许变化的行为。=
l 声明non-virtual函数的目的是为了令派生类继承函数的接口及一份强制性实现。
你可以这样考虑Shape::objectID的声明:“每一个Shape对象有一个用来产生对象识别码的函数,且这个识别码总是用同样的方法计算出来的,这个方法是由Shape::objectID的定义决定的,任何派生类不应该试图改变它的做法。”
总结:
purevirtual: 只继承接口
impurevirtual:继承接口和一份缺省实现
non-virtual: 继承接口和一份强制实现
两个常犯错误:
1. 声明所有的函数为 non-virtual(非虚拟)。这没有给派生类的特殊化留出空间;当然,完全有理由设计一个不作为基类使用的类。事实是,几乎任何作为基类使用的类都会有virtual函数。
2. 声明所有的成员函数为virtual。某些函数在派生类中不应该被重定义,果真如此你应该通过将那些函数声明为non-virtual而明确地表达这一点。如果你的不变性凌驾于特异性,请直说!
- 接口继承与实现继承不同。在 public继承下,派生类总是继承基类的接口。
- Pure virtual函数只具体指定接口继承。
- impure virtual函数具体指定接口继承加上缺省实现继承。