Item 34:区分接口继承和实现继承 Effective C++笔记

C++ 继承 接口 虚函数 继承 编译 虚函数 成员函数

Item 34: Dirrerentiate between inheritance of interface and inheritance of implementation.

不同于Objective C或者Java,C++中的继承接口和实现继承是同一个语法过程。 当你public继承一个类时,接口是一定会被继承的(见Item32),你可以选择子类是否应当继承实现:

  • 不继承实现,只继承方法接口:纯虚函数。
  • 继承方法接口,以及默认的实现:虚函数。
  • 继承方法接口,以及强制的实现:普通函数。

一个例子

为了更加直观地讨论接口继承和实现继承的关系,我们还是来看一个例子:RectEllipse都继承自Shape

class Shape{
public:
    // 纯虚函数
    virtual void draw() const = 0;
    // 不纯的虚函数,impure...
    virtual void error(const string& msg);
    // 普通函数
    int id() const;
};
class Rect: public Shape{...};
class Ellipse: public Shape{...};

纯虚函数draw()使得Shape成为一个抽象类,只能被继承而不能创建实例。一旦被public继承,它的成员函数接口总是会传递到子类。

  • draw()是一个纯虚函数,子类必须重新声明draw方法,同时父类不给任何实现。
  • id()是一个普通函数,子类继承了这个接口,以及强制的实现方式(子类为什么不要重写父类方法?参见Item 33)。
  • error()是一个普通的虚函数,子类可以提供一个error方法,也可以使用默认的实现。

因为像ID这种属性子类没必要去更改它,直接在父类中要求强制实现!

危险的默认实现

默认实现通常是子类中共同逻辑的抽象,显式地规约了子类的共同特性,避免了代码重复,方便了以后的增强,也便于长期的代码维护。 然而有时候提供默认实现是危险的,因为你不可预知会有怎样的子类添加进来。例如一个Airplane类以及它的几个Model子类:

class Airplane{
public:
    virtual void fly(){
        // default fly code
    }
};
class ModelA: public Airplane{...};
class ModelB: public Airplane{...};

不难想象,我们写父类Airplane时,其中的fly是针对ModelAModelB实现了通用的逻辑。如果有一天我们加入了ModelC却忘记了重写fly方法:

class ModelC: public Airplane{...};
Airplane* p = new ModelC;
p->fly();

虽然ModelC忘记了重写fly方法,但代码仍然成功编译了!这可能会引发灾难。。这个设计问题的本质是普通虚函数提供了默认实现,而不管子类是否显式地声明它需要默认实现。

安全的默认实现

我们可以用另一个方法来给出默认实现,而把fly声明为纯虚函数,这样既能要求子类显式地重新声明一个fly,当子类要求时又能提供默认的实现。

class Airplane{
public:
    virtual void fly() = 0;
protected:
    void defaultFly(){...}
}
class ModelA: public Airplane{
public:
    virtual void fly(){defaultFly();}
}
class ModelB: public Airplane{
public:
    virtual void fly(){defaultFly();}
}

这样当我们再写一个ModelC时,如果自己忘记了声明fly()会编译错,因为父类中的fly()是纯虚函数。 如果希望使用默认实现时可以直接调用defaultFly()

注意defaultFly是一个普通函数!如果你把它定义成了虚函数,那么它要不要给默认实现?子类是否允许重写?这是一个循环的问题。。

优雅的默认实现

上面我们给出了一种方法来提供安全的默认实现。代价便是为这种接口都提供一对函数:fly, defaultFly, land, defaultLand, … 有人认为这些名字难以区分的函数污染了命名空间。他们有更好的办法:为纯虚函数提供函数定义。

确实是可以为纯虚函数提供实现的,编译会通过。但只能通过Shape::draw的方式调用它。

class Airplane{
public:
    virtual void fly() = 0;
};
void Airplane::fly(){
    // default fly code
}

class ModelA: public Airplane{
public:
    virtual void fly(){
        Airplane::fly();
    }
};

上述的实现和普通成员函数defaultFly并无太大区别,只是把defaultFlyfly合并了。 合并之后其实是有一定的副作用的:原来的默认实现是protected,现在变成public了。在外部可以访问它:

Airplane* p = new ModelA;
p->Airplane::fly();

在一定程度上破坏了封装,但Item 22我们提到,protected并不比public更加封装。 所以也无大碍,毕竟不管defaultFly还是fly都是暴露给类外的对象使用的,本来就不能够封装。

Harttle

致力于简单的、一致的、高效的前端开发

看看这个?