15.4 虚函数
对于特定的函数,为了引起晚捆绑,C++要求在基类中声明这个函数时使用virtual关键字。晚捆绑只对virtual函数起作用,而且只在使用含有virtual函数的基类的地址时发生,尽管它们也可以在更早的基类中定义。
为了创建一个像virtual这样的成员函数,可以简单地在这个函数声明的前面加上关键字virtual。仅仅在声明的时候需要使用关键字virtual,定义时并不需要。如果一个函数在基类中被声明为virtual,那么在所有的派生类中它都是virtual的。在派生类中virtual函数的重定义通常称为重写(overriding)。
注意,仅需要在基类中声明一个函数为virtual。调用所有匹配基类声明行为的派生类函数都将使用虚机制。虽然可以在派生类声明前使用关键字virtual(这也是无害的),但这样会使程序段显得冗余和混乱。
为了从Instrument2.cpp中得到所希望的结果,只需简单地在基类中的play()之前增加virtual关键字:
这个文件除了增加了virtual关键字之外,一切与Instrument2.cpp相同,但结果明显不一样。现在输出调用的是Wind:play。
15.4.1 扩展性
通过将play()在基类中定义为virtual,不用改变tune()函数就可以在系统中随意增加新函数。在一个设计风格良好的OOP程序中,大多数甚至所有的函数都沿用tune()模型,只与基类接口通信。这样的程序是可扩展的(extensible),因为可以通过从公共基类继承新数据类型而增加新功能。操作基类接口的函数完全不需要改变就可以适合于这些新类。
这里有一个instrument例子,它有更多的虚函数和一些新类,它们都能与老的版本一起正确工作,而不用改变tune()函数:
可以看到,这个例子已在Wind之下增加了另外的继承层,但不管这里有多少层,virtual机制仍会正确工作。针对Brass和Woodwind, adjust()函数没有重写(重新定义)。当出现这种情况时,将会自动地调用继承层次中“最近”的定义—编译器保证对于虚函数总是有某种定义,所以决不会出现最终调用不与函数体捆绑的情况(这种情况将导致灾难)。
数组A[]存放指向基类Instrument的指针,所以在数组初始化过程中发生向上类型转换。这个数组和函数f()将在稍后的讨论中用到。
在对tune()的调用中,向上类型转换在对象的每一个不同的类型上完成。总能得到期望的结果。这可以被描述为“发送一条消息给一个对象,让这个对象考虑用它来做什么”。virtual函数使我们在分析项目时可以初步确定:基类应当出现在哪里?应当如何扩展这个程序?在程序最初创建时,即便我们没有发现合适的基类接口和虚函数,但在稍后或者更晚,当决定扩展或维护这个程序时,也常常会发现它们。这不是分析或设计错误,它只意味着一开始我们还没有所有的信息。由于C++中严格的模块化,因此这并不是大问题。因为当我们对系统的一部分进行修改时,往往不会像C那样波及系统的其他部分。