很多人认为,C++中是不存在接口继承的,只有Java、C#这类语言才提供了相应的语法支持。
但是,如同鲁迅说过的某句名言:世上本没有接口继承,用的人多了,才有了接口继承。C++中依然可以实现接口继承,只是形式上稍有不同罢了。
C++中的继承基于一个事实:父类定义的成员函数会一直被子类继承(包括被子类隐藏的部分)。
而父类中提供的函数可以有三种:1)普通成员函数 2)普通虚函数 3)纯虚函数。这三种函数类型代表了三种继承设计模式。
一个简单的实例代码如下:
04 |
virtual void Draw() = 0; |
05 |
virtual int GetError(); |
09 |
class Rectangular : public Shape |
14 |
class Circle : public Shape |
普通成员函数由父类声明且实现,子类应继承接口以及强制性的实现。
这几乎是最常见的一种函数类型,代表了典型的”is-a”继承设计模式。
ps:所谓的”is-a”设计模式,指的是”everything that applies to base classes must also apply to derived classes”
示例中,函数GetId严格遵守”is-a”模式。因为每个子类本质都是一个Shape对象,都有一个唯一的ID
普通虚函数可以在父类中有默认的实现,而这个默认实现可以由子类继承。
子类也可以选择重写虚函数以实现多态性。
所以,普通虚函数在继承设计中表示派生类必须支持此接口,但是否重写,由派生类自己决定。
如同每个子类对象都应该有一个报错函数。但是函数可以使用父类提供的默认实现(提示简单的出错信息,然后清理资源),也可以选择自己实现(每个子类有自己的错误语义)
纯虚函数会使得父类自动成为不可实例化的抽象类。而且每个继承的子类必须强制自行重写。
所以,纯虚函数表示子类继承父类的函数接口,并且必须自己具体实现该函数。
即从这个角度上看,纯虚函数代表的就是接口继承。
实例代码中,父类将Draw声明为纯虚函数。这表明每个具体的子类都应该有Draw函数,并且需要自己实现(每个具体子类的Draw实现应是不同的)。
对于纯虚函数,有一个有意思的特性:纯虚函数可以有实现代码
之所以说这个特性有意思,是因为拥有纯虚函数的类不能实例化并且纯虚函数指定的是接口继承,子类仍然需要自己实现函数。
这就引发了一个问题:如何调用这个纯虚函数的默认实现版本?解决的方法是显式调用
1 |
Shape* p = new Rectangular; |
那么这种让人觉得操蛋的trick有没有什么应用呢?
假设你有一个父类F,定义了一个普通虚函数,子类A,B都使用默认的虚函数实现。但是某天你需要增加一个新的子类C,但是这个子类不能使用默认的实现,必须重写。
不幸的是,你忘了重写这个函数,所以编译器为你调用了默认实现,于是意外的结果让你蛋碎了一地。
很明显,使用带有实现的纯虚函数就可以解决这个问题。纯虚函数会强制要求你重写虚函数,而你也可以在需要默认实现时通过显式调用完成相应工作。
不过需要这种trick的情况相当罕见,而且多半是设计出了问题或者完全可以人为避免。