Skip to content

Latest commit

 

History

History
202 lines (161 loc) · 7.82 KB

File metadata and controls

202 lines (161 loc) · 7.82 KB

条款34:区分接口继承和实现继承

身为class设计者,有时候你会希望派生类只继承成员函数的接口;有时候你又会希望派生类同时继承函数的接口和实现,但又希望能够重写它们所继承的实现;又有时候你希望派生类同时继承函数的接口和实现,并且不允许重写任何东西。

为了感受上述差异,让我们考虑一个展现绘图程序中各种几何形状的class继承体系:

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强烈影响了所有以public继承它的派生类,因为:

  • 成员函数的结果口总是被继承。如果某个函数可施行于某class身上,一定也可施行于其派生类上。

首先考虑纯虚函数draw:

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

纯虚函数有两个最突出的特性:它们必须被任何继承了它们的具象class重新声明,而且它们在抽象类中通常没有定义。把这两个性质摆在一起,你就会明白:

  • 声明一个纯虚函数的目的是为了让派生类只继承函数接口。

Shape::draw 是声明是对具象派生类设计者说,“你必须提供一个draw函数,但我不干涉你怎么实现它。”我们可以为春促函数提供定义,C++并不会报错,但调用它的唯一途径是调用时指明class名称:

Shape* ps = new Shape;      // error: Shape是抽象类
Shape* ps1 = new Rectangle; // ok
ps1->draw();                // Rectangle::draw
Shape* ps2 = new Ellipse;   // ok
ps2->draw();                // Ellipse::draw
ps1->Shape::draw();         // Shape::draw
ps2->Shape::draw();         // Shape::draw

这项性质的用途十分有限。

非纯虚函数和纯虚函数有点不同,派生类继承其接口,但非纯虚函数会提供一份实现代码,派生类可能重写它。稍加思索,你就会明白:

  • 声明非纯虚函数的目的,是让派生类继承该函数的接口和默认实现

考虑 Shape::error 这个例子:

class Shape {
public:
  virtual void error(const std::string& msg);
  ...
};

其接口表示,每个class都必须支持一个“当遇上错误时可调用”的函数,但每个class可自由处错误。Shape::error 的声明告诉派生类的设计者,“你必须支持一个error函数,但如果不想自己实现,可使用Shape类提供的默认版本。”

但是,允许非纯虚函数同时指定函数声明和函数默认行为,却有可能造成危险。让我们考虑XYZ航空公司设计的飞机继承体系。该公司只有A型和B型两种飞机,两者都以相同方式飞行。因此XYZ设计出这样的继承体系:

class Airport {...};
class Airplane {
public:
  virtual void fly(const Airport& destination);
  ...
};
void Airplane::fly(const ƒ) {...}   // 缺省代码,将飞机飞至指定目的地
class ModelA : public Airplane {...};
class ModelB : public Airplane {...};

为了表示所有飞机都一定能飞,并阐明“不同型飞机原则上需要不同的fly实现, Airplane::fly 被声明为virtual。然而为了避免在ModelA和ModelB中撰写相同代码,默认飞行行为由 Airplane::fly 提供,它同时被ModelA和ModelB继承。

现在,假设XYZ公司决定购买一种新式C型飞机。C型和A型以及B型有某些不同,更明确地说,它的飞行方式不同。

XYZ公司的程序员在继承体系中针对C型飞机添加了一个class,但由于它们着急让新飞机上线服务,竟忘了重写其fly函数:

class ModelC : public Airplane {
  ... // 未声明fly函数
};

然后代码中有一些诸如此类的操作:

Airport PDX(...);
Airplane* pa = new ModelC;
...
pa->fly(PDX);   // Airplane::fly

这个程序试图让ModelC以ModelA或ModelB的飞行方式来飞行,这将酿成大灾难。

问题不在 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的形式表现。若想使用默认实现,可在其fly函数中对defaultFly做一个inline调用:

class ModelA : public Airplane {
public:
  virtual void fly(const Airport& destination) 
  { defaultFly(destination); }
  ...
};
class ModelB : public Airplane {
  virtual void fly(const Airport& destination) 
  { defaultFly(destination); }
  ...
};

现在ModelC类不可能意外继承不正确的fly实现代码了,因为Airplane的纯虚函数迫使ModelC必须提供自己的fly版本:

class ModelC : public Airplane {
public:
  virtual void fly(const Airport& destination);
  ...
};
void ModelC::fly(const Airport& destination) {
  // 将C型飞机飞至指定目的地
}

有些人反对以不同的函数分别提供接口和默认实现,像上述的fly和defaultFly那样。它们关心因过度雷同的函数名称而因此class命名空间污染问题。如何结局这个矛盾?我们可以利用纯虚函数必须在派生类中重新声明,但它们也可以拥有自己的实现这一事实。下面便是Airplane继承体系如何给纯虚函数一份定义:

class Airplane {
public:
  virtual void fly(const Airplane& 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) {
  // 将C型飞机飞至指定目的地
}

这个设计几乎和前面一模一样,只不过纯虚函数 Airplane::fly 替换了独立函数 Airplane::defaultFly。如果合并fly和defaultFly,就丧失了让两个函数享有不同保护级别的机会:习惯上被设定为protected的函数(defaultFly)如今成了public(fly)。

最后,让我们看看Shape的non-virtual函数objectID:

class Shape {
public: 
  int objectID() const;
  ...
};

如果成员函数是个non-virtual函数,意味着它并不打算在派生类中有不同的行为。无论派生类表现得多么特异化,它的行为都不可以改变。就其自身而言:

  • 声明non-virtual函数的目的是为了令派生类继承函数的接口以及一份强制性实现。

你可以把 Shape::objectID() 的声明想做是:每个Shape对象都有一个用来生成对象识别码的函数;此识别码总是采用相同的计算方法,该方法由 Shape::objectID() 的定义决定,任何派生类都不应该尝试改变其行为。非虚函数代表的意义是不变性凌驾于特异性,因此它绝不该在派生类中被重定义。

请记住

  • 接口继承和实现继承不同。在public继承下,派生类总是继承基类的接口。
  • 纯虚函数只具体指定接口继承。
  • 非纯虚函数具体指定接口继承以及默认实现继承。
  • 非虚函数具体指定接口继承以及强制性实现继续。