从C++到.NET 揭开多态的面纱

出处:www.examlink.com 作者:小罗 日期:2007年01月24日 14时53分


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(即时编译)的需要——对每个函数的调用都是间接的,都会经过该表推导一次,获得函数代码的地址。注意,第一次调用的时候,函数代码还是中间代码(.NET的中间语言MISL的代码),所以将会跳至即时编译器,编译这些代码并放到内存中,然后将表中的对应表项指向编译后的native code,以后的每次调用都会直接跳到编译后的代码。

  以上只是想让你对.NET的“虚函数表”有个大体的认识。下面就来详细剖析。

  如果没有接口,.NET的虚函数调用机制将是很单纯的——几乎与C++一样。只不过,接口加入以后就不同了——可以将对象引用转化为接口引用,然后再调用接口中的虚函数。所以,势必要对“虚函数表”作某种改动,例如,对于下面的继承结构:

public interface IFirst
{
 void f1();
 void f2();
}

public interface ISecond
{
 void s1();
}

public class C:IFirst,Isecond
{
 public override void f1(){}
 public override void f2(){}
 public override void s1(){}
 public virtual void c1(){}
}  

  类型C的内存布局大体是这样的(由于.NET是单根的继承结构,每个类都隐式的继承自Object,所以,类型C的“虚函数表”中包含Object的所有成员函数)

  ObjRef指向一个对象,在对象顶部(除了用于同步的sync#块之外)是hType(可以看成对应于C++对象顶部的虚函数表指针),它所指的结构(CORINFO_CLASS_STRUCT,可以暂时将它看成虚函数表,尽管其中包含的信息不仅仅是虚函数指针)包含在C++中相当于虚函数表的部分,以及用于对象的运行时识别的信息。不同的是,在基于接口的.NET继承风格中,对接口的虚函数的分派是基于一个IOT(Interface Offset Table,即接口偏移表),图中的pIOT就是指向这样一个表,其中每一项都是一个偏移量,反指向该接口中的虚函数指针数组在CORINFO_CLASS_STRUCT中的位置。

  这样,当基于接口的引用调用虚函数时,其背后的机制是:先根据接口引用取得该类所对应的CORINFO_CLASS_STRUCT结构的地址,然后在pIOT所指的接口偏移表中索引相应的虚函数指针数组的偏移量,最后经过指针间接调用虚函数。 可以看出,基于接口引用调用虚函数时要经过两个间接层,第一,在IOT中索引对应接口的虚函数指针数组的偏移量,第二,在虚函数指针数组中索引相应的虚函数指针,最后才是调用。但是,当基于对象引用调用虚函数时,只要经过一个间接层——就像在C++中一样——直接在虚函数表中索引对应虚函数指针,接着调用。
最后更新时间:2008-03-28 12:10:30
文章评论
共有 0 位网友发表了评论
用户名: 新注册) 密码: 匿名评论 [查看所有评论]

评论内容:(不能超过250字,需审核后才会公布,请自觉遵守互联网相关政策法规。
您可以用以下几种方式找到此文章

考试全流程