15.5.3 撩开面纱

如果能看到由虚函数调用而产生的汇编语言代码,这将是很有帮助的,这样我们可以看到晚捆绑实际上是如何发生的。下面是在函数f(Instrument&i)内部调用:

15.5.3 撩开面纱 - 图1

某个编译器所产生的输出:

15.5.3 撩开面纱 - 图2

C++函数调用的参数与C函数调用一样,是从右向左进栈的(这个顺序是为了支持C的变量参数表),所以参数1首先压栈。对于这个函数,寄存器si(Intel x86处理器的一部分)存放i的地址。因为它是被选中的对象的首地址,它也被压进栈。记住,这个首地址对应于this的值,正因为调用每个成员函数时this都必须作为参数压进栈,所以成员函数知道它工作在哪个特殊对象上。这样,我们总能看到,在成员函数调用之前压栈的次数等于参数个数加1(除了static成员函数,它没有this)。

现在,必须实现实际的虚函数调用。首先,必须产生VPTR,使得能找到VTABLE。对于这个编译器,VPTR在对象的开头,所以this的内容对应于VPTR。下面这一行:

15.5.3 撩开面纱 - 图3

取出si(即this)所指的字,它就是VPTR。将这个VPTR放入寄存器bx中。

放在bx中的这个VPTR指向这个VTABLE的首地址,但被调用的函数不是在VTABLE中第0个位置,而是在第2个位置(因为它是这个表中的第3个函数)。对于这种内存模式,每个函数指针是两个字节长,所以编译器对VPTR加4,计算相应的函数地址所在的地方。注意,这是编译时建立的常值,所以惟一要做的事情就是保证在第2个位置上的指针恰好指向adjust()。幸好编译器仔细处理,并保证在VTABLE中的所有函数指针都以相同的次序出现,而不论我们在派生类中是以什么次序覆盖它们。

一旦在VTABLE中相应函数指针的地址被计算出来,就调用这个函数。所以取出这个地址并马上在这个句子中调用:

15.5.3 撩开面纱 - 图4

最后,栈指针移回去,以清除在调用之前压入栈的参数。在C和C++汇编代码中,将经常看到调用者清除这些参数,但这可能依据处理器和编译器的实现而有所不同。