15.5.2 虚函数功能图示

下面是Instrument4.cpp中的指针数组A[]的图,它可以帮助我们准确地理解当使用虚函数时编译器进行的内部活动。

15.5.2 虚函数功能图示 - 图1

这个Instrument指针数组没有特殊类型信息,它的每一个元素都指向一个类型为Instrument的对象。Wind、Percussion、Stringed和Brass都可以归入这个类别之中,因为它们都是从Instrument派生来的(并因而与Instrument有相同的接口和可以响应相同的消息),所以它们的地址自然被放进这个数组。然而,编译器并不知道它们是比Instrument对象具有更多内容的东西,所以,就将它们留给其自己的设备处理,而通常调用所有函数的基类版本。但在这里,所有这些函数都被用virtual声明,所以出现了不同的情况。

每当创建一个包含有虚函数的类或从包含有虚函数的类派生一个类时,编译器就为这个类创建一个惟一的VTABLE,如这个图的右面所示。在这个表中,编译器放置了在这个类中或在它的基类中所有已声明为virtual的函数的地址。如果在这个派生类中没有对在基类中声明为virtual的函数进行重新定义,编译器就使用基类的这个虚函数地址。(在Brass的VTABLE中,adjust的入口就是这种情况。)然后编译器在这个类中放置VPTR(可在Sizes.ccp中发现)。当使用简单继承时,对于每个对象只有一个VPTR。VPTR必须被初始化为指向相应的VTABLE的起始地址。(这在构造函数中发生,在稍后会看得更清楚。)

一旦VPTR被初始化为指向相应的VTABLE,对象就“知道”它自己是什么类型。但只有当虚函数被调用时这种自我认知才有用。

当通过基类地址调用一个虚函数时(此时编译器没有能完成早捆绑所需的所有信息),要特殊处理。它不是实现典型的函数调用,那样只是简单地用汇编语言CALL特定的地址,而是编译器为完成这个函数调用而产生不同的代码。下面看到的是通过Instrument指针对于Brass调用adjust()(Instrument引用产生同样的结果)。

15.5.2 虚函数功能图示 - 图2

编译器从这个Instrument指针开始,这个指针指向这个对象的起始地址。对于所有的Instrument对象或由Instrument派生的对象,它们的VPTR都在对象的相同位置(常常在对象的开头),所以编译器能够取出这个对象的VPTR。VPTR指向VTABLE的起始地址。所有的VTABLE都具有相同的顺序,不管何种类型的对象。play()是第一个,what()是第二个,adjust()是第三个。所以无论什么特殊的对象类型,编译器都知道adjust()函数必在VPTR+2处。这样,不是“以Instrument:adjust地址调用这个函数”(这是早捆绑,是错误动作),而是产生代码,即实际上“在VPTR+2处调用这个函数”。因为获取VPTR和确定实际函数地址发生在运行时,所以这样就得到了所希望的晚捆绑。我们向这个对象发送消息,随后这个对象能断定它应当做什么。