区分接口继承和实现继承

本文系整理Effective C++中的条款34


首先当我们设计一个基类的时候,对于派生类的需求可能有下面几种

  1. 派生类只继承基类的接口(即函数的声明);
  2. 派生类同时继承接口和实现,并且希望能够重写(多态)自己的版本
  3. 派生类同时继承接口和实现,但是不允许重写任何东西

说这些晦涩难懂的文字,一时半会不好理解,下面分别举实例,用代码来解读这三种需求。

下面是一个展现绘图程序中各种几何图形的class继承体系:

class Shape
{
public:
    //纯虚函数
    virtual void draw()const = 0;
    //普通的虚函数
    virtual void error(const std::string& msg);
    普通函数
    int objectID()const;
    ...
};

class Rectangle:public Shape{...};
class Ellispse:public Shape{...};

这里首先说下,因为virtual void draw()const = 0; 是一个纯虚函数,从而使Shape类变为一个抽象类,即不能实例化出对象。

纯虚函数的声明:virtual + 函数返回值+函数名,在函数的后面紧跟=0。

这样就导致客户不能实例化出Shape的对象,只能实例化出其派生类的对象。但是基类还是影响了所有以public方式继承它的派生类,因为成员函数的接口总是被继承,public继承意味着is-a的关系。即保证任何一个对基类为真的事件,一定也对其派生类为真。

pure virtual(纯虚函数)的特性:

  1. 在抽象类中通常没有定义
  2. 必须被任何”继承了它们”的具体class重新声明

这就意味着声明一个pure virtual函数的目的是为了让派生类只继承函数的接口

但是令人意外的是,我们竟然可以为pure virtual函数提供定义,也就是说你可以为Shape::draw供应一份实现代码,c++并不会发出怨言,但是调用它的唯一途径是”调用时指明其class名称”

如下实例:

Shape* ps = new 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(普通虚函数)的特点

正如Shape::error这个例子:

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

其接口表示,每个 class都必须支持一个“当遇上错误时可调用的函数”,但每个class可自由处理错误,如果你不想针对错做出任何特殊行为,则可以使用Shape class提供的缺省版本.

但是允许普通虚函数同时指定函数声明和函数缺省行为,有可能造成危险,考虑如下一种场景。

航空公司设计的飞机继承体系,该公司只有A和B两种飞机,且以同样的方式飞行。因此有了一下的继承关系:

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

典型的面向对象设计,两个classes共享一份相同性质的代码,这个共同的性质被搬到了基类中,然后被两个类同时继承,这个设计突显出共同性质,避免代码重复,减缓长期维护所需的成本,是个不错的模型吧?非也。

假设现在航空公司增加一种新型飞机C,并且C型飞机,并且飞行方式和A,B有所不同。

自然而然class ModelC:public Airpane{...} ,但是由于疏忽,并未从新定义fly函数,这时就采用了基类提供的缺省行为。后果不言而喻!!!!

问题不是Airplane::fly有缺省行为,而在于ModelC并未明白说出”我要”的情况下就继承了该缺省行为,幸运的是我们可以切断”virtual函数接口”和其”缺省实现”之间的联系。

看下面的代码实例

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

void Airplane::defaultFly(const Airport& destination)
{
    //缺省行为,将飞机飞至指定的目的地
}

注意,上面的fly函数已经被声明为一个pure virtual函数,只提供飞行的接口,其缺省行为以一个保护的成员函数形式出现,子类若想使用缺省实现,可以在fly函数中对defaultFly做一个inline调用

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

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

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

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

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

这里需要注意,Airplane::defaultFly现在成了protect的,因为它是Airplane及其derived classes的实现细节,乘客应该只在意飞机能不能飞,而不在意怎么飞的问题。

有些人反对不同的函数分别提供接口和缺省实现,就像上面的fly和defaultFly那样,他们关心因过度雷同的函数名称而引起class命名空间污染问题。但是它们也同意,接口和缺省应该分开,这个看似矛盾的问题该如何解决呢?可以利用”pure virtual函数必须在派生类中重新声明,但它们可以拥有自己的实现”这一事实。看下面最终的版本。

class ModelA:public Airplane
{
public:
    virtual  void fly(const Airport& distination) = 0...
};

void Airplane::fly(const Airport& destination)//pure virtual函数实现
{
    //缺省代码,将飞机飞至指定地点
}

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型飞机飞至指定的目的地
}

这里的实现和前面几乎一模一样,只不过pure virtual函数Airplane::fly替换了独立函数Airplane::defaultFly。本质上现在的f’l’y被分割为两个基本要素:

  1. 声明部分表现的是接口(派生类继续使用的)
  2. 定义部分表现出缺省行为(派生类可能使用的,但需要它们明确提出申请)

如果合并fly和defaultFly,就丧失了”让两个函数享有不同保护级别”的机会,习惯上被定义为protected的函数(defaultFly)现在变成了public。

non-virtual(非虚函数)的特点

再来回到最开始的的继承体系Shape的non-virtual函数objectID

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

该函数是一个non-virtual函数,意味着并不打算在派生类中有不同的行为,实际上一个non-virtual成员函数表现出来的不变性;凌驾于其特异性,因为无论派生类变得多么特异化,它的行为都不可以改变。

上面说的这么说,总结起来就是下面几点:

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

猜你喜欢

转载自blog.csdn.net/qq_36528114/article/details/79553653