C/C++编程:区分接口继承和实现继承

public继承有两部分组成:

  • 函数接口(function interface)
  • 函数实现(function implementation)

身为类的设计者,有时候你会希望派生类只继承成员函数的接口(也就是声明);有时候你会希望派生类同时继承函数的接口和实现,有时候你会希望能够覆写(override)它们所继承的实现;有时候你会希望生类同时继承函数的接口和实现,并且不允许覆写任何东西。

我们来看个例子:

class Shape{
public:
	virtual void draw() const = 0;
	virtual void error(const std::string& msg);
	int objectID() const;
	...
};

class Rectangel : public Shape {...}
class Ellipse : public Shape {...}

Shape是个抽象类:它的纯虚函数draw使它成为一个抽象类。所以客户不能够创建Shape类的实体,只能创建其派生类的实体。 尽管如此,Shape还是强烈影响了所有以public形式继承它的派生类,因为:成员函数的接口总是会被继承

  • public继承意味着is-a,所以对基类为真的任何事情一定对派生类也为真。因此如果某个函数可以施行于某个类身上,一定也可以作用域派生类身上
  • 对于纯虚函数virtual void draw() const = 0
    • 纯虚函数有两个最突出的特性:
      • 它们必须被任何继承了它们的具现类重新声明
      • 它们在抽象类中通常没有定义
    • 从这两个形式可以看出:声明一个纯虚函数的目的是为了让派生类只继承函数接口。Shaped::draw的声明相当于对具象派生类设计者说:”你必须提供一个draw函数,但我不干涉你怎么实现它
    • 纯虚函数在抽象类中通常没有定义,但是不是不能这样做:抽象类中可以实现纯虚函数,不过调用他们的唯一途径是明确支持其类的名称
Shape* ps = new Shape; // 错误,Shape是抽象的
Shape* ps1 = new Rectangle; //没问题
ps1->draw();   // 调用Rectangle::draw
ps1->Shape::draw(); // 调用Shape::draw

一般而言,这项性质用途有限,但是稍后你将看到,它可以实现一种机制,为简朴的impure virtual函数提供更平常更安全的缺省实现。

简朴的impure virtual函数和pure virtual函数有点不同。一如往常,派生类继承其函数接口,但非纯虚函数会提供一份实现代码,派生类可能覆写(overried)它,也就是说:

  • 声明impure virtual函数的目的,是让派生类继承该函数的接口和缺省实现

看个例子:

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

其error接口表示,每个类都必须支持一个”当遇上错误是可调用“的函数,但每个类可以自由处理错误,如果每个类不想针对错误做出任何特殊行为,它可以退回到Shape类提供的缺省错误处理行为。也就是说,Shape::error告诉派生类:”你可以一个error函数,用来处理错误,但如果你不想自己写一个,可以使用Shape class提供的缺省版本“。

但是,允许impure virtual函数同时指定函数声明和函数缺省行为,有可能造成危险。我们来看一个例子。

场景: 一家公司只有两种飞机,两者都以相同的方式飞行
实现:为了表现所有飞机都一定能飞,并阐明”不同型飞机原则上需要不同的fly实现“。Airplane::fly被声明为virtual。然而为了避免在ModelA和ModelB中编写相同代码,缺省行为由Airplane::fly提供,它同时被ModelA和ModelB继承。如下


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 {}

新需求:加入这家公司又买了一种C型飞机,这种飞机的不是采用默认的飞行方式,但是不小心忘记了重新定义fly函数:

class ModelC :  public Airplane {}

这将造成重大灾难。

问题不在Airplane::fly有缺省行为,而在于ModelC在没有明白说出"我要"的情况下就缺省继承了该缺省行为。幸运的是我们可以轻易做到"提供缺省实现给派生类,但除非它们要求否则免谈"。技巧在于切断”虚函数“和"缺省实现"之间的连接。下面是一种做法。

class Airplane{
public:
	virtual void fly(const Ariport& destination) = 0;
protected: 
	void defaultFly(const Ariport& destination);
}

void Airplane::defaultFly(const Airport& destination)// 缺省代码,将飞机飞往指定目的地
};

现在Airplane::fly已经被改为一个纯虚函数,只提供飞行接口。其缺省行为以独立的defaultFly函数提供。如果派生类想要使用缺省实现,可以在其fly函数中对defaultFly做一个inline调用:


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是protected的,因为它是Airplane和其派生类的实现细目。乘客只需要关心飞机能不能飞,不需要关心怎么飞。
  • Airplane::defaultFly是一个non-virtual函数。因为没有任何派生类应该重新定义此函数

有人反对以不同的函数分别提供接口和缺省实现(fly,default),会应为过渡雷同的函数名称而引起类命名空间污染问题。但是他们也同意,接口和缺省应该分开。那这个矛盾怎能解决呢?我们可以利用纯虚函数必须在派生类中重新声明,但是它们也可以有自己的实现这一事实来解决:

class Airplane{
public:
	virtual void fly(const Ariport& destination) = 0;
};
void Airplane::fly(const Ariport& 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);
	}
}

本质上,现在的fly被分成两个基本要素:其声明部分表现的是接口(派生类必须用的),其定义部分标注的是缺省行为(派生类可能要用的,但只有它们明确提出申请才是)。

最后,我们来看看Shape的non-virtual的objectID:

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

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

  • 声明non-virtual函数的目的是为了令派生类继承函数的接口以及一份强制性实现
  • 一个non-virtual成员函数所表现的不变性凌驾于其特异性,所以绝不该在派生类中被重新定义

Shape::objectID的声明相当于:”每个Shape对象都有一个用来产生对象识别码的函数,此识别码总是采用相同的计算方法,该方法由Shape::objectID的定义式决定,任何派生类都不应该尝试改变其行为“。

纯虚函数,impure virtual函数、non-virtual函数之间的差异,使得你得以精确指定你想要派生类继承的东西:只继承接口,或者是继承接口和一份缺省实现,或者是继承接口和一份强制实现。由于这些不同类型的声明意味着根本意义并不相同的事情,当你声明你的成员函数,必须谨慎选择。注意

  • 不要将所有的函数都声明为non-vritual,这使得派生类没有空间进行特化工作。non-vritual析构函数绝对会带来问题。事实上:

    • 如果你的类不会是基类,那么就可以将所有的成员函数都声明为non-virtual是正确的
    • 只要任何类打算被当作一个基类,都会拥有若干虚函数(只是析构函数是纯虚函数)
  • 不要将所有的成员函数都声明为virtual

    • 有些函数就是不该在派生类中被重新定义,这是它们应该是non-vritual
    • 只有interface class,才可以将所有的成员函数都声明为virtual

总结

  • 接口继承和实现继承不同。在public继承下,派生类总是继承基类的接口
  • pure virtual函数只具体指定接口继承
  • impure virtual函数具体指定接口接口和缺省实现继承
  • non-virtual函数具体指定接口继承以及强制性实现继承

你可能感兴趣的:(C++)