Item 34:区分接口继承和实现继承
继承时的函数接口传递
当你 public 继承一个类时,接口是一定会被继承的,你可以选择子类是否应当继承实现:
- 不继承实现,只继承方法接口:纯虚函数。
- 继承方法接口,以及默认的实现:虚函数。
- 继承方法接口,以及强制的实现:普通函数。
Rect 和 Ellipse 都继承自 Shape。
class Shape{
public:
// 纯虚函数
virtual void draw() const = 0;
// 非纯虚函数
virtual void error(const string& msg);
// 普通函数
int id() const;
};
class Rect: public Shape{...};
class Ellipse: public Shape{...};
public 继承,基类的成员函数接口总是会传递到子类。
- draw() 是一个纯虚函数,子类必须重新声明 draw 方法,同时父类不给任何实现。
- id() 是一个普通函数,子类继承了这个接口,以及强制的实现方式。
- error() 是一个普通的虚函数,子类可以提供一个 error 方法,也可以使用默认的实现。
因为像 ID 这种属性子类没必要去更改它,直接在父类中要求强制实现!
危险的默认实现
默认实现通常是子类中共同逻辑的抽象,显式地规约了子类的共同特性,避免了代码重复,方便了以后的增强,也便于长期的代码维护。
然而有时候提供默认实现是危险的,因为你不可预知会有怎样的子类添加进来。例如一个 Airplane 类以及它的几个 Model 子类:
class Airplane{
public:
virtual void fly(){
// default fly code
}
};
class ModelA: public Airplane{...};
class ModelB: public Airplane{...};
不难想象,我们写父类 Airplane 时,其中的 fly 是针对 ModelA 和 ModelB 实现了通用的逻辑。如果有一天我们加入了 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() 是一个普通函数。如果你把它定义成了虚函数,那么它要不要给默认实现?子类是否允许重写?这是一个循环的问题。
优雅的默认实现
上面我们给出了一种方法来提供安全的默认实现。有人认为这些名字难以区分的函数污染了命名空间。
我们可以为纯虚函数提供实现,编译会通过。但只能通过 Airplane::fly 的方式调用它。
class Airplane{
public:
virtual void fly() = 0;
};
void Airplane::fly(){
// default fly code
}
class ModelA: public Airplane{
public:
virtual void fly(){
Airplane::fly();
}
};
上述的实现和普通成员函数 defaultFly 并无太大区别,只是把 defaultFly 和 fly 合并了。 合并之后其实是有一定的副作用的:原来的默认实现是 protected,现在变成 public 了。在外部可以访问它:
Airplane* p = new ModelA;
p->Airplane::fly();
总结
- 接口继承与实现继承不同。在公开继承下,派生类总是继承基类接口:
- 纯虚拟函数指定仅有接口被继承。
- 普通虚函数接口继承加上缺省实现继承。
- 普通函数指定接口继承加上强制实现继承。
- 可以为纯虚函数提供实现,调用函数时需要使用类名作用域符。