隐式向上强制转换使基类指针或引用可以指向基类对象或派生类对象,因此需要动态联编。C++使用
虚成员函数来满足这种需求。
13.4.4 虚拟成员函数和动态联编
我们来回顾一下使用引用或指针调用方法的过程。请看下面的代码:
BrassPlus ophelia;
Brass * bp:
bp = Gophelia:
bp->ViewAcct {):
正如前面介绍的,如果在基类中没有将ViewAcct()声明为虚拟的,则bp->ViewAcct()将根据指针类型
(Brass*),而调用Brass :: ViewAcct()。指针类型在进行编译时是已知的,因此编译器在进行编译时,可以
将ViewAcct()关联到Brass :: ViewAcct()。简而言之,编译器对非虚方法使用静态联编。
但是,如果在基类中将ViewAcct()声明为虚拟的,则bp->ViewAcct()根据对象类型(BrassPlus)调用
BrassPlus :: ViewAcct()。在这个例子中,对象类型为BrassPlus,但通常(如程序清单13.10所示)只有在运
行程序时才能确定对象的类型。所以编译器生成的代码将在程序执行时,根据对象类型将ViewAcct()关联
到Brass :: ViewAcct()或BrassPlus :: ViewAcct()。简而言之,编译器对虚方法使用动态联编。
在大多数情况下,动态联编很好,因为它让程序能够选择为特定类型设计的方法。因此,您可能会问:
● 为什么有两种类型的联编?
● 既然动态联编如此之好,为什么不将它设置成默认的?
● 动态联编是如何工作的?
下面来看一看这些问题的答案。
1. 为什么有两种类型的联编以及为什么默认为静态联编
如果动态联编让您能够重新定义类方法,而静态联编在这方面很差;为何不摒弃静态联编呢?原因有
两个 -- 效率和概念模型。
首先来看效率。为使程序能够在运行阶段进行决策,必须采取一些方法来跟踪基类指针或引用指向的
对象类型,这增加了额外的处理开销(稍后将介绍一种动态联编方法)。例如,如果类不会用作基类,则不
需要动态联编。同样,如果派生类(如RatedPlayer)不重新定义基类的任何方法,也不需要使用动态联编。
在这些情况下,使用静态联编更合理,效率也更高。由于静态联编的效率更高,因此被设置为C++的默认
选择。Strousstrup说,C++的指导原则之一是,不要为不使用的特性付出代价(内存或者处理时间)。仅当
程序设计确实需要虚函数时,才使用它们。
接下来看概念模型。在设计类时,可能包含一些不在派生类重新定义的成员函数。例如,Brass :: Balance()
函数返回账户结余,不应该重新定义。不将该函数设置为虚函数,有两方面的好处:首先效率更高;其次,
指出不要重新定义该函数。这表明,仅将那些预期将被重新定义的方法声明为虚拟的。
提示:如果要在派生类中重新定义基类的方法,则将它设置为虚方法;否则,设置为非虚方法。
当然,设计类时,方法属于哪种情况有时并不那么明显。与现实世界中的许多方面一样,类设计并不
是个线性过程。
2. 虚函数的工作原理
C++规定了虚函数的行为,但将实现方法留给了编译器作者。不需要知道实现方法就可以使用虚函数,
但了解虚函数的工作原理有助于更好地理解概念,因此,这里对其进行介绍。
通常,编译器处理虚函数的方法是:给每个对象添加一个隐藏成员。隐藏成员中保存了一个指向函数
地址数组的指针。这种数组称为虚函数表(virtual function table,vtbl)。虚函数表中存储了为类对象进行声
明的虚函数的地址。例如,基类对象包含一个指针,该指针指向基类中所有虚函数的地址表。派生类对象
将包含一个指向独立地址表的指针。如果派生类提供了虚函数的新定义,该虚函数表将保存新函数的地址;
如果派生类没有重新定义虚函数,该vtbl将保存函数原始版本的地址。如果派生类定义了新的虚函数,则
// derived-class object
// base-class pointer
// Brass pointer to BrassPlus object
// which version?