差,透明性差等等。然而,它的思想已经接近现代编译器对多态机制的实现手法了。
通过将上面的例子中的函数指针扩展为一个隐含的指针数组——虚函数表(vtbl)——C++拥有了我们现在所看到的多态能力。在虚函数表中,每一个虚函数指针占有一个表项,如果派生类覆盖(override)了相应的虚函数,则对应表项就改成指向派生类的那个虚函数的——这些工作由编译器完成——从而,如上例所示,用户不必知晓对象的确切类型,就能够触发其特定的行为(也就是说,调用“取决于对象具体类型”的成员函数),虚函数表对用户是完全透明的,用户只需要使用一个virtual关键字就能够轻松拥有强大的多态能力。
如果一个C++类中有虚函数,则该类将会拥有一个虚函数表(vtbl),并且,该类的对象中(一般在头部)有一个隐含的指向虚函数表的指针(vptr)。
现在假设有如下代码:
void f(B* pb)
{
pb->f1();
}
则编译器为该函数生成的代码如下(以伪代码表示,以示明了):
void f(B* pb)
{
DWORD* __vptr=((DWORD*)pb)[0]; //获得虚函数表指针
void (B::*midd_pf)()=__vptr[offsetof_virtual_pf1];
//从表中获得相应虚函数指针
(pb->*midd_pf)(); //调用虚函数
}
这样一来,如果pb指向的是D对象,则获得的是指向D::f1的函数指针(参考上面的第二幅图),如果pb确实指向B对象,根据B对象内的vptr所指的虚函数表,获得的是指向B::f1的函数指针。
现在,关于C++的多态机制基本已经明了。剩下的就是多重继承下的虚函数表格局,大同小异,就不多说了。只不过,其中还是有一些微妙的细节的,可以参见《Inside C++ Object Model》(Lippman著)(中文名《深入C++对象模型》——侯捷译)。
关于C++虚函数调用机制还有一个细节——在构造函数中调用虚函数要千万小心,因为“在构造函数中”意味着“对象还没有构造完毕”,这时候虚函数调用机制很可能还没有启动,例如:
class B
{
B(){this->vf();} //调用B::vf
virtual void vf(){cout<<”in B::vf()\n”;
};
现在,不管B身为哪个类的基类,B的构造函数中调用的都是B::vf。细心的读者会发现:这是由于对象构造顺序的关系——C++明确规定,对象的“大厦”是“自底向上”构建的,也就是说,从最底层的基类开始构造,所以,在B中调用this->vf时,虽然this所指的对象确实(即将)是派生类对象,但是派生类对象的构建行为还没有开始,所以这次调用不可能跑到派生类的vf函数去,就好像第二层楼还没有建好,一层楼的人是无法跑到二楼去的一样。
说得更深一些,虚函数的调用是要经过虚函数指针和虚函数表来间接推导的,在B的构造函数中,编译器会插入一些代码,将对象头部的vptr设置为指向B的虚函数表的指针,于是this->vf的推导使用的是B的虚函数表,当然只能跑到B的vf那儿去。而后来,当B构建完毕,轮到派生类对象部分构造时,派生类的构造函数会将对象头部的vptr改成指向派生类的虚函数表的指针,这时候虚函数调用机制才算是Enable了,以后的this->vf将使用派生类虚函数表来推导,从而到达正确的函数。
.NET 对象模型
C++对象模型与.NET(或Java)有个主要的区别——C++支持多重继承,不支持接口,而.NET(或Java)支持接口,不支持多重继承。
而.NET的虚函数调用机制与C++也比较相似,只不过由于接口和JIT(即时编译)的介入而有一些不同。
在.NET中,每一个类都有一个对应的函数指针表(事实上,这个“表”是个数据结构,里面还有其它信息),与C++不同的是,该类的每个函数(不管是不是虚函数)都在其中对应一个表项。这是由于JIT(即时编译)的需要——对每个函数的调用都是间接的,都会经过该表推导一次,获得函数代码的地址。注意,第一