35.区分接口继承与实现继承

前言

 
public继承概念其实由两部分组成:函数接口(function interface)继承与函数实现(function implementation)继承。这两种继承的差异类似于函数声明与函数定义的差异。


接口继承与实现继承

具体含义

public继承体系下derived class无非存在以下三种可能:

  1. 只继承base class成员函数的接口(也就是声明)
  2. 同时继承接口与实现,并且能够覆写(override)它们所继承的实现
  3. 同时继承接口与实现,但不允许覆写任何东西

问题实例

以下有一个绘图程序:

1
2
3
4
5
6
7
8
class shape{
public:
virtual void draw() const=0;
virtual void error(const string &msg)
int objectID() const;
}
class rectangle:public shape {}
class ellipse:public shape {}

可以看到抽象类shape内部共有三个声明方式各不相同的函数,draw是一个pure virtual函数,error是一个impure virtual函数,objectID是个non-virtual函数。


函数声明对继承的影响

pure vitual

pure virtual函数有两个最突出的特性:

  • 必须被任何继承了它们的具象class重新声明
  • 通常在abstract class中没有定义

综上,有结论如下:声明一个pure virtual的目的是为了让derived classes只继承函数接口。它等价于在对一个具象derived class要求:“你必须提供该函数,但我并不干涉你如何实现。”

实际上,pure virtual函数也可以有定义,但你只能通过“调用时明确指出其class名称”来调用它,例如:

1
2
shape *ps = new shape;
ps—>shape::draw();

这种写法一般仅有一个用途:为impure virtual函数提供更平常更安全的缺省实现。


impure virtual

impure virtual函数的目的在于:让derived class继承该函数的接口与缺省实现。它等价于在告诉derived class的设计者,“你必须支持该函数,如果你不想自定义的话,那当前存在一个缺省版本。”

隐含风险与解决方案

允许impure virtual函数同时指定函数声明和函数缺省行为带有一定的风险。

问题实例

假定航空公司共有AB两种飞机,二者以相同方式飞行,因此继承体系如下:

1
2
3
4
5
6
7
8
9
10
11
class Airport{...};//机场
class Airplane{
public:
virtual void fly(const Airport& derstination);
...
};
void Airplane::fly(const Airport& destination){
...//缺省,将飞机飞往目的地
}
class ModelA:public Airplane {...};
class ModelB:public Airplane {...};

截至目前为止,这个继承体系是完全正确的。但假设该航空公司又购买了一款新式飞机C,其飞行方式与AB不同。但由于急着令飞机上线,忘记了重定义其fly函数:
1
2
3
class ModelC:public Airplane {...};
Airplane* pa = new ModelC;
pa->fly(airport);

上述程序试图用AB飞机的飞行方式来驱动C,无疑会造成雪崩。

问题剖析

此问题的发生并不在于Airplane::fly()具备缺省行为,而在于ModelC在未表明自己需要的情况下就继承了该缺省行为。因此我们的程序应该完成这个功能:“提供缺省实现给derived class,但只有他们明确要求继承该缺省实现时才会继承”

解决方案
protected defaultFly

此方法的原理在于切断“virtual函数接口”与“缺省实现”之间的连接。

1
2
3
4
5
6
7
8
9
10
class Airplane {
public:
virtual void fly(const Airport& destination)=0;
...
protected:
void defaultFly(const Airport& destination);//函数以protected姿态封装
};
void Airplane::defaultFly(const Airport& destination){
...//缺省,将飞机飞往目的地
}

此时,fly已经被改为一个pure virtual函数,仅仅提供飞行接口。其缺省行为以独立的defaultFly登场,如果想要调用缺省行为,则在继承而来的fly接口中使用inline调用即可:
1
2
3
4
5
6
7
8
9
10
class ModelA:public Airplane{
public:
virtual void fly(const Airport& destination){
defaultFly(destination);
}
...
};
void ModelC::fly(const Airport& destination){
...//自定义飞行模式
}

定义pure virtual函数

有人认为不应该定义defaultFly以污染命名空间,但接口和缺省实现也确实应该分开,于是他们巧妙地通过定义pure virtual函数来避免了这一尴尬:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Airplane {
public:
virtual void fly(const Airport& destination)=0;
...
}
void Airplane::fly(const Airport& destination) {
//缺省行为
}
class ModelA:public Airplane{
public:
virtual void fly(const Airport& destination){
Airplane::fly(destination);
}
...
};
void ModelC::fly(const Airport& destination){
...//自定义飞行模式
}


non-virtual

该函数的不变性(invariant)凌驾于特异性(specialization),因此它表示无论derived class表现地多么奇特,该函数行为都不可改变。声明non-virtual是为了令derived class继承函数的接口及一份强制实现。


继承时易犯的错误

  • 将所有函数声明为non-virtual
    除非你的class不作为base class,否则别这样作死,该用virtual就用,别过多担心其带来的效率问题。(除非在profiler发现真的是它们影响了效率)
  • 将所有函数声明为virtual
    除非你在写interface class,否则该有不变性就应该大胆地说出来。

总结

  1. 接口继承不同于实现继承。在public继承下,dc总是继承bc的接口。
  2. pure virtual只继承接口。
  3. impure virtual为继承指定接口及default实现。
  4. non-virtual继承指定接口,并强制性继承实现。