15.7 抽象基类和纯虚函数
在设计时,常常希望基类仅仅作为其派生类的一个接口。这就是说,仅想对基类进行向上类型转换,使用它的接口,而不希望用户实际地创建一个基类的对象。要做到这点,可以在基类中加入至少一个纯虚函数(pure virtual function),来使基类成为抽象(abstract)类。纯虚函数使用关键字virtual,并且在其后面加上=0。如果某人试着生成一个抽象类的对象,编译器会制止他。这个工具允许生成特定的设计。
当继承一个抽象类时,必须实现所有的纯虚函数,否则继承出的类也将是一个抽象类。创建一个纯虚函数允许在接口中放置成员函数,而不一定要提供一段可能对这个函数毫无意义的代码。同时,纯虚函数要求继承出的类对它提供一个定义。
在所有的instrument的例子中,基类Instrument中的函数总是“哑”函数。如果调用这些函数,就会出错。这是因为,Instrument的目的是对所有从它派生出来的类创建公共接口。
建立公共接口的惟一原因是它能对于每个不同的子类有不同的表示。它建立一个基本的格式,用来确定什么是对于所有派生类是公共的—除此之外,别无用途。所以,把Instrument设计为抽象类就比较合适。当仅希望通过一个公共接口来操纵一组类,且这个公共接口不需要实现(或者不需要完全实现)时,可以创建一个抽象类。
如果有一个作用类似于抽象类的类(就像Instrument),则这个类的对象几乎总是没有意义的。也就是说,Instrument的含义只表示接口,不表示特例实现,所以创建一个Instrument对象没有意义。我们也许想防止用户这样做,这可以通过让Instrument的所有虚函数打印出错信息而完成,但这种方法到运行时才能获得出错信息,并且要求用户进行可靠而详尽的测试。所以最好是在编译时就能发现这个问题。
下面是用于纯虚函数声明的语法:
这样做,等于告诉编译器在VTABLE中为函数保留一个位置,但在这个特定位置中不放地址。只要有一个函数在类中被声明为纯虚函数,则VTABLE就是不完全的。
如果一个类的VTABLE是不完全的,当某人试图创建这个类的对象时,编译器做什么呢?它不能安全地创建一个纯抽象类的对象,所以如果试图创建一个纯抽象类的对象,编译器就发出一个出错信息。这样,编译器就保证了抽象类的纯洁性,它就不会被误用了。
下面是修改后的Instrument4.cpp,它使用了纯虚函数。因为这个类中全是纯虚函数,所以我们称之为纯抽象类(pure abstract class):
纯虚函数是非常有用的,因为它们使得类有明显的抽象性,并告诉用户和编译器打算如何使用。
注意,纯虚函数禁止对抽象类的函数以传值方式调用。这也是防止对象切片(object slicing)(这将会被简单地介绍)的一种方法。通过抽象类,可以保证在向上类型转换期间总是使用指针或引用。
纯虚函数防止产生完全的VTABLE,但这并不意味着我们不希望对其他一些函数产生函数体。我们常常希望调用一个函数的基类版本,即便它是虚拟的。把公共代码放在尽可能靠近我们的类层次根的地方,这是很好的想法。这不仅节省了代码空间,而且使得改变的传播更加容易。
15.7.1 纯虚定义
在基类中,对纯虚函数提供定义是可能的。我们仍然告诉编译器不允许产生抽象基类的对象,而且如果要创建对象,则纯虚函数必须在派生类中定义。然而,我们可能希望一段公共代码,使一些或所有派生类定义都能调用,而不必在每个函数中重复这段代码。
正如下面的纯虚定义:
Pet的VTABLE表仍然空着,但在这个派生类中刚好有一个函数,可以通过名字调用它。
这个特点的另一个好处是,它允许我们实现从常规虚函数到纯虚函数的改变,而无须打乱已存在的代码。(这是一个处理不用重新定义虚函数的类的方法。)