首页 > 学院 > 开发设计 > 正文

C++箴言:接口继承和实现继承

2019-11-17 05:18:26
字体:
来源:转载
供稿:网友
  (public) inheritance 这个表面上简单易懂的观念,一旦被近距离审阅,就会被证实是由两个相互独立的部分组成的:inheritance of function interfaces(函数接口的继续)和 inheritance of function implementations(函数实现的继续)。这两种 inheritance 之间的差异正好符合本书 IntrodUCtion 中论述的 function declarations(函数声明)和 function definitions(函数定义)之间的差异。  作为一个 class 的设计者,有的时候你想要 derived classes 只继续一个 member function 的 interface (declaration)。有的时候你想要 derived classes 既继续 interface(接口)也继续 implementation(实现),但你要答应它们替换他们继续到的 implementation。还有的时候你想要 derived classes 继续一个函数的 interface(接口)和 implementation(实现),而不答应它们替换任何东西。

  为了更好地感觉这些选择之间的不同之处,考虑一个在图形应用程序中表示几何图形的 class hierarchy(类继续体系):

class Shape {
public:
virtual void draw() const = 0;

virtual void error(const std::string& msg);

int objectID() const;

...
};

class Rectangle: public Shape { ... };

class Ellipse: public Shape { ... };
  Shape 是一个 abstract class(抽象类),它的 pure virtual function(纯虚拟函数)表明了这一点。作为结果,客户不能创建 Shape class 的实例,只能创建从它继续的 classes 的实例。但是,Shape 对所有从它(公有)继续的类施加了非常强大的影响,因为

  成员函数 interfaces are always inherited。就像 Item 32 解释的,public inheritance 意味着 is-a,所以对一个 base class 来说成立的任何东西,对于它的 derived classes 也必须成立。因此,假如一个函数适用于一个 class,它也一定适用于它的 derived classes。

  Shape class 中声明了三个函数。第一个,draw,在一个明确的显示设备上画出当前对象。第二个,error,假如 member functions 需要报告一个错误,就调用它。第三个,objectID,返回当前对象的唯一整型标识符。每一个函数都用不同的方式声明:draw 是一个 pure virtual function(纯虚拟函数);error 是一个 simple (impure?) virtual function(简单虚拟函数);而 objectID 是一个 non-virtual function(非虚拟函数)。这些不同的声明暗示了什么呢?

  考虑第一个 pure virtual function(纯虚拟函数)draw:

class Shape {
public:
virtual void draw() const = 0;
...
};
  pure virtual functions(纯虚拟函数)的两个最显著的特性是它们必须被任何继续它们的具体类重新声明,和抽象类中一般没有它们的定义。把这两个特性加在一起,你应该熟悉到。

  声明一个 pure virtual function(纯虚拟函数)的目的是使 derived classes 继续一个函数 interface only。

  这就使 Shape::draw function 具有了完整的意义,因为它要求所有的 Shape 对象必须能够画出来是合情合理的,但是 Shape class 本身不能为这个函数提供一个合乎情理的缺省的实现。例如,画一个椭圆的算法和画一个矩形的算法是非常不同的,Shape::draw 的声明告诉具体 derived classes 的设计者:“你必须提供一个 draw function,但是我对于你如何实现它不发表意见。”

  顺便提一句,为一个 pure virtual function(纯虚拟函数)提供一个定义是有可能的。也就是说,你可以为 Shape::draw 提供一个实现,而 C++ 也不会抱怨什么,但是调用它的唯一方法是用 class name 限定修饰这个调用:

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
  除了帮助你在鸡尾酒会上给同行程序员留下印象外,这个特性通常没什么用处,然而,就像下面你将看到的,它能用来作为一个“为 simple (impure) virtual functions 提供一个 safer-than-usual 的实现”的机制。

  simple virtual functions 背后的故事和 pure virtuals 有一点不同。derived classes 照常还是继续函数的 interface,但是 simple virtual functions 提供了一个可以被 derived classes 替换的实现。假如你为此考虑一阵儿,你就会熟悉到

  声明一个 simple virtual function 的目的是让 derived classes 继续一个函数 interface as well as a default implementation。

  考虑 Shape::error 的情况:

class Shape {
public:
virtual void error(const std::string& msg);
...
};
  interface 要求每一个 class 必须支持一个在遭碰到错误时被调用的函数,但是每一个 class 可以自由地用它觉得合适的任何方法处理错误。假如一个 class 不需要做什么非凡的事情,它可以仅仅求助于 Shape class 中提供的错误处理的缺省版本。也就是说,Shape::error 的声明告诉 derived classes 的设计者:“你应该支持一个 error function,但假如你不想自己写,你可以求助 Shape class 中的缺省版本。”

  结果是:答应 simple virtual functions 既指定一个函数接口又指定一个缺省实现是危险的。来看一下为什么,考虑一个 XYZ 航空公司的飞机的 hierarchy(继续体系)。XYZ 只有两种飞机,Model A 和 Model B,它们都严格地按照同样的方法飞行。于是,XYZ 设计如下 hierarchy(继续体系):

class Airport { ... }; // rePResents airports

class Airplane {
public:
virtual void fly(const Airport& destination);

...

};

void Airplane::fly(const Airport& destination)
{
default code for flying an airplane to the given destination
}

class ModelA: public Airplane { ... };

class ModelB: public Airplane { ... };

  为了表述所有的飞机必须支持一个 fly 函数,并为了“不同机型可能(在理论上)需要不同的对 fly 的实现”的事实,Airplane::fly 被声明为 virtual。然而,为了避免在 ModelA 和 ModelB classes 中些重复的代码,缺省的飞行行为由 Airplane::fly 的函数体提供,供 ModelA 和 ModelB 继续。

  这是一个经典的 object-oriented 设计。因为两个 classes 共享一个通用特性(它们实现 fly 的方法),所以这个通用特性就被转移到一个 base class 之中,并由两个 classes 来继续这个特性。这个设计使得通用特性变得清楚明白,避免了代码重复,提升了未来的可扩展性,简化了长期的维护——因为 object-oriented 技术,所有这些东西都受到很高的追捧。XYZ 航空公司应该引以为荣。  现在,假设 XYZ 公司的财富增长了,决定引进一种新机型,Model C。Model C 在某些方面与 Model A 和 Model B 不同。非凡是,它的飞行不同。

  XYZ 公司的程序员在 hierarchy(继续体系)中增加了 Model C 的 class,但是由于他们匆匆忙忙地让新的机型投入服务,他们忘记了重定义 fly function:

class ModelC: public Airplane {

... // no fly function is declared
};
  于是,在他们的代码中,就出现了类似这样的东西:

Airport PDX(...); // PDX is the airport near my home

Airplane *pa = new ModelC;

...

pa->fly(PDX); // calls Airplane::fly!
  这是一个灾难:企图让一个 ModelC object 像一个 ModelA 或 ModelB 一样飞行。这在旅行人群中可不是一种鼓舞人心的行为。

  这里的问题并不在于 Airplane::fly 有缺省的行为,而是在于 ModelC 被答应不必明确说出它要做什么就可以继续这一行为。幸运的是,“为 derived classes(派生类)提供缺省的行为,但是除非它们提出明确的要求,否则就不交给他们”是很轻易做到的。这个诀窍就是切断 virtual function(虚拟函数)的 interface(接口)和它的 default implementation(缺省实现)之间的联系。以下用的就是这个方法:

class Airplane {
public:
virtual void fly(const Airport& destination) = 0;

...

protected:
void defaultFly(const Airport& destination);
};

void Airplane::defaultFly(const Airport& destination)
{
default code for flying an airplane to the given destination
}
  注重 Airplane::fly 是被如何变成一个 pure virtual function(纯虚拟函数)的。它为飞行提供了 interface(接口)。那个缺省的实现也会出现在 Airplane class 中,但是现在它是一个独立的函数,defaultFly。像 ModelA 和 ModelB 这样需要使用缺省行为的 Classes 只是需要在他们的 fly 的函数体中做一下对 defaultFly 的 inline 调用(但是请参见 Item 30 提供的关于 inline 化和 virtual functions(虚拟函数)的交互作用的信息):

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 class,不可能在无意中继续到不正确的 fly 的实现,因为 Airplane 中的 pure virtual(纯虚拟)强制要求 ModelC 提供的它自己的 fly 版本。

class ModelC: public Airplane {
public:
virtual void fly(const Airport& destination);

...
};

void ModelC::fly(const Airport& destination)
{
code for flying a ModelC airplane to the given destination
}
  这一方案并非十分安全(程序员还是能通过 copy-and-paste 使他们自己陷入麻烦),但是它比最初的设计更加可靠。至于 Airplane::defaultFly,它是 protected(保护的)是因为它完全是 Airplane 和它的 derived classes(派生类)的实现细节。使用飞机的客户应该只在意它能飞,而不必管飞行是如何实现的。

  Airplane::defaultFly 是一个 non-virtual function(非虚拟函数)这一点也很重要。这是因为 derived class(派生类)不应该重定义这个函数,这是一个在 Item 36 中专门介绍的原则。假如 defaultFly 是 virtual(虚拟的),你就会碰到一个循环的问题:假如某些 derived class(派生类)应该重定义 defaultFly 却忘记了的时候会如何呢?

  一些人反对为 interface(接口)和 default implementation(缺省实现)分别提供函数,就像上面的 fly 和 defaultFly 那样。首先,他们注重到,这样做会导致类似的相关函数名污染 class namespace(类名字空间)的问题。然而他们仍然同意 interface(接口)和 default implementation(缺省实现)应该被分开。他们是怎样解决这个表面上的矛盾呢?通过利用以下事实:pure virtual functions(纯虚拟函数)必须在 concrete derived classes(具体派生类)中被 redeclared(重声明),但是它们也可以有它们自己的实现。以下就是 Airplane hierarchy(继续体系)如何利用这一能力定义一个 pure virtual function(纯虚拟函数):


class Airplane {
public:
virtual void fly(const Airport& destination) = 0;

...
};

void Airplane::fly(const Airport& destination) // an implementation of
{ // a pure virtual function
default code for flying an airplane to
the given 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)
{
code for flying a ModelC airplane to the given destination
}
  除了用 pure virtual function(纯虚拟函数)Airplane::fly 的函数体代替了独立函数 Airplane::defaultFly 之外,这是一个和前面的几乎完全相同的设计。本质上,fly 可以被拆成两个基本组件。它的 declaration(声明)指定了它的 interface(接口)(这是 derived classes(派生类)必须使用的),而它的 definition(定义)指定它的缺省行为(这是 derived classes(派生类)可以使用的,但只是在他们明确要求这一点时)。将 fly 和 defaultFly 合并,无论如何,你失去了给予这两个函数不同的保护层次的能力:原来是 protected 的代码(通过位于 defaultFly 中实现)现在成为 public(因为它位于 fly 中)。

  最后,我们看看 Shape 的 non-virtual function(非虚拟函数),objectID:

class Shape {
public:
int objectID() const;
...
};
  当一个 member function(成员函数)是 non-virtual(非虚拟的)时,不应该指望它在 derived classes(派生类)中的行为会有所不同。实际上,一个 non-virtual member function(非虚拟成员函数)指定了一个 invariant over specialization(超越非凡化的不变量),因为不论一个 derived class(派生类)变得多么非凡,它都把它看作是不答应变化的行为。如下所指除的,

  声明一个 non-virtual function(非虚拟函数)的目的是 to have derived classes inherit a function interface as well as a mandatory implementation(使派生类既继续一个函数的接口,又继续一个强制的实现)。

  你可以这样考虑 Shape::objectID 的声明,“每一个 Shape object 有一个产生 object identifier(对象标识码),而且这个 object identifier(对象标识码)总是用同样的方法计算出来的,这个方法是由 Shape::objectID 的定义决定的,而且 derived class(派生类)不应该试图改变它的做法。”因为一个 non-virtual function(非虚拟函数)被看作一个 invariant over specialization(超越非凡化的不变量),在 derived class(派生类)中他绝不应该被重定义,细节的讨论参见 Item 36。

  对 pure virtual,simple virtual,和 non-virtual functions 的声明的不同答应你精确指定你需要 derived classes(派生类)继续什么东西。分别是 interface only(仅有接口),interface and a default implementation(接口和一个缺省的实现),和 interface and a mandatory implementation(接口和一个强制的实现)。因为这些不同的声明类型意味着根本不同的意义,当你声明你的 member functions(成员函数)时你必须在它们之间仔细地选择。假如你这样做了,你应该可以避免由缺乏经验的类设计者造成的两个最常见的错误。

  第一个错误是声明所有的函数为 non-virtual(非虚拟)。这没有给 derived classes(派生类)的非凡化留出空间;non-virtual destructors(非虚拟析构函数)尤其有问题(参见 Item 7)。当然,完全有理由设计一个不作为 base class(基类)使用的类。在这种情况下,一套独享的 non-virtual member functions(非虚拟成员函数)是完全合理的。然而,更通常的情况下,这样的类既可能出于对 virtual(虚拟)和 non-virtual functions(非虚拟函数)之间区别的无知,也可能是对 virtual functions(虚拟函数)的性能成本毫无根据的担心的结果。事实是,几乎任何作为 base class(基类)使用的类都会有 virtual functions(虚拟函数)(还是参见 Item 7)。

  假如你关心 virtual functions(虚拟函数)的成本,请答应我提起基于经验的 80-20 规则(参见 Item 30),在一个典型的程序中的情况是,80% 的运行时间花费在执行其中的 20% 的代码上。这个规则是很重要的,因为它意味着,平均下来,你的函数调用中的 80% 可以被虚拟化而不会对你的程序的整体性能产生哪怕是最稍微的可察觉的影响。在你走进对“你是否能负担得起一个 virtual function(虚拟函数)的成本”忧虑的阴影之前,应该使用一些简单的预防措施,以确保你关注的是你的程序中能产生决定性不同的那 20%。

  另一个常见的错误声明所有的 member functions(成员函数)为 virtual(虚拟)。有时候这样做是正确的—— Item 31 的 Interface classes(接口类)可以作为证据。然而,它也可能是缺乏表明态度的决心的类设计者的标志。某些函数在 derived classes(派生类)中不应该被重定义,而且只要在这种情况下,你都应该通过将那些函数声明为 non-virtual(非虚拟)而明确地表达这一点。它不是为那些人服务的,他们假设假如他们只需花一些时间重定义你的所有函数,你的类就会被所有的人用来做所有的事情,假如你有一个 invariant over specialization(超越非凡化的不变量),请直说,不必害怕!

  Things to Remember

  ·Inheritance of interface(接口继续)与 inheritance of implementation(实现继续)不同。在 public inheritance(公开继续)下,derived classes(派生类)总是继续 base class interfaces(基类接口)。

  ·Pure virtual functions(纯虚拟函数)指定 inheritance of interface only(仅有接口被继续)。

  ·Simple (impure) virtual functions(简单虚拟函数)指定 inheritance of interface(接口继续)加上 inheritance of a default implementation(缺省实现继续)。

  ·Non-virtual functions(非虚拟函数)指定 inheritance of interface(接口继续)加上 inheritance of a mandatory implementation(强制实现继续)。


发表评论 共有条评论
用户名: 密码:
验证码: 匿名发表