C++ 区分接口继承和实现继承

时间:2022-03-11 10:06:41

20180322 C++ 区分接口继承和实现继承


public继承概念由两部分组成,函数接口(function interfaces)继承和函数实现(function implementations)继承。


作为类的开发人员,我们主要研究类的三种继承情况:
1、派生类只继承成员函数的接口(也就是声明);
2、派生类同时继承函数的接口和实现,但又希望能够覆写(override)它们所继承的实现;
3、派生类同时继承函数的接口和实现,并且不允许覆盖任何东西。


为了更好地理解上述情况,考虑以下例子:
eg:
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是个抽象类,它的纯虚函数draw使它成为一个抽象类,所以客户不能够创建Shape class的实体,只能创建它的派生类的实体。
Shape类声明了三个函数,第一个是draw,在视屏中划出当前对象,第二个是error,准备让那些“需要报导某个错误”的成员函数调用,第三个是objectID,返回当前对象的独一无二的整数识别码,每个函数的声明方式都不相同,draw是个纯虚函数(pure virtual),error是个虚函数( 简朴的(非纯)impure virtual函数),objectID是个非虚函数(non-virtual)函数。


纯虚函数通常有两个特点:它们必须被任何“继承了他们”的具象类重新声明;并且它们在抽象类中通常没有定义。
所以结论是:
1、声明一个纯虚函数的目的是为了让派生类只继承函数的接口。
这里需要指出的是,可以为纯虚函数提供定义,即可以为Shape:draw供应一份实现代码,但调用它的唯一方法是“调用时明确指出其类名称”(这种写法在实际开发中,并没有声明卵用):
Shape* ps = new shape;//错误,Shape是抽象的
Shape* ps1 = new Rectangle;//没问题
ps1->draw();//调用Rectangle::draw
Shape* ps2 = new Ellipse;//没问题
ps2->draw;//调用Ellipse::draw
ps1->Shape::draw();//调用Shape::draw
ps2->Shape::draw();//调用Shape::draw


虚函数(简朴的impure virtual函数)背后的故事和纯虚函数(pure virtual函数)有点不同,一如往常,派生类继承其函数接口,但虚函数(简朴的impure virtual函数)会提供一份实现代码,派生类可能覆写(override)它,所以结论是:
1、声明虚函数(简朴的impure virtual函数)的目的是让派生类继承该函数的接口和缺省实现,考虑error函数,其接口表示,每个类都必须支持一个“当遇上错误是可调用”的函数,但每个类可*处理错误,若某个类不想针对错误做出任何特殊行为,它可以退回到Shape类提供的缺省错误处理行为。


但是允许虚函数(简朴的impure virtual函数)同时指定函数声明和函数缺省行为,却有可能造成危险,考虑下面的例子:


XYZ航空公司的飞机继承体系,该公司只有A型和B型两种飞机,两者都以相同方式飞行,因此XYZ设计的继承体系为:
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型飞机,C型和A型、B型的飞行方式不同,XYZ公司的程序员在继承体系中针对C型飞机加了一个类,但由于急于让飞机上线,竟然忘了定义其fly函数:


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




若代码中出现如下操作:
Airport PDX(...);//PDX是机场名字
Airplane* pa = new ModelC;
...
pa->fly(PDX);//调用Airplane::fly


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


解决该问题的要点在于切断“虚函数接口”和其“缺省实现”之间的连接,下面是一种做法:


class Airplane{
public:
  virtual void fly(const Airplane& destination) = 0;
  ...
protected:
  void defaultFly(const Airport& destination);
};
void Airplane::defaultFly(const Airport& destination)
{
  缺省行为,将飞机飞至指定的目的地
}


现在ModelA和ModelB调用的飞行的缺省实现为:
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中的纯虚函数迫使ModelC必须提供自己的fly版本:




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


void ModelC::fly(const Airport& destination)
{
  将C型飞机飞至指定目的地
}




下面是fly被分割为两部分的方法:其声明部分表现的是接口(那是派生类必须使用的),其定义部分则表现出缺省行为(那是派生类可能使用的,但只有在它们明确提出申请时才是),下面便是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 Airplane& destination)
{
 将C型飞机飞至指定的目的地
}










最后考虑Shape的非虚函数objectID()。若成员函数是个非虚函数,意味着它并不打算在派生类中有不同的行为,实际上非虚成员函数所表现的不变性远重要于特异性,因为它表示不论派生类变得多么特异化,就其自身而言,它的行为都不可以改变。






注意:
1、接口继承和实现继承不同。在public继承之下,派生类总是继承基类的接口;
2、纯虚函数只是具体指定接口继承;
3、虚函数( 简朴的(非纯)impure virtual函数)具体指定接口继承及缺省实现继承;
4、非虚函数(non-virtual函数)具体指定接口继承以及强制性实现继承。