深度探索c++对象模型之vptr初始化语意学

      接上文,还是这个图,还是这个继承关系:

   

其中它们的构造顺序是,是从内到外、从根源到末端。所以对一个PVertex对象来说,它的构造顺序是:1、Point   2、Point3d  3、Vertex  4、Vertex3d  5、PVertex,然后书中说到【大意】:在以上的每一个类中,都定义两个虚函数,一个是fun【我自己取的名】,一个是size,其中它俩的关系是,在size中调用fun;然后再在每一个类的构造器中,调用一下size。问:这样的话,在以上每一个类的构造器中调用的size里面,会调用PVertex中的fun虚函数【因为完整类对象就是PVertex,既语句“PVertex pv;”】,还是调用当前构造类中的fun虚函数?如果读者朋友看到这里不明白我问的是什么,那么我换一种方式提问:比如一句【PVertex pv;】语句,最先执行的是Point类的构造器,那么在执行Point构造器时,会调用size函数,在这个size函数里面,究竟是调用的是PVertex::fun虚函数,还是Point::fun虚函数?

      书中首先给出了答案:在执行Point构造器时,里面调用的fun函数应该是Point中的fun函数,而不应该是PVertex::fun。因为“经由构造中的对象来调用一个virtual base function,其函数实体应该是在此class中有作用的那个”,“当base class constructor执行时,derived实体还没有被构造出来。”在PVertex constructor执行完毕之前,pv并不是一个完整的对象,在执行Point constructor完之后,只有Point类对象被构造出来。

      然后,作者提供的解决之道是什么呢,就是在执行类的constructor时,限制一组virtual function候选名单。文中这样说道:“想一想,什么是决定一个class的virtual function名单的关键?答案是virtual table。virtual table如何被处理?答案是通过vptr。所以,为了控制一个class中有所作用的函数,编译系统只要简单的控制住vptr的初始化和设定操作即可。当然,设定vptr是编译器的责任,任何程序员都不必操心此事。”。

      但vptr在什么时候被设定呢,答案是它当前隶属的class的constructor调用完成之后,但是要在任何用户提供的代码和“member initialization list中所列的members初始化操作”之前。这样每一个类的构造器都一直等到它的基类构造器执行完毕之后,再去设定自己的vptr,那么每次它都能够调用正确的virtual function实体。

      让每一个base class constructor设定好对象的vptr,使它指向相关联的virtual table地址,构造中的对象就可以正确的变成“构造过程中所幻化出来的每一个class”对象。比如,一个PVertex会先形成一个Point对象,接着是Point3d、Vertex、Vertex3d,最后才变成一个PVertex完整对象,在此过程中它会调用每一个base class constructor。“个体”的连续概括了“系统”,仅此而已。

     在此过程中,关于vptr的设定,总结为3点:

1):在派生类对象构造器中,所有的虚基类及上一层基类的构造器都会被调用;

2):在上述工作完成之后,该类的vptr才会被初始化,指向相关的virtual table;

3):如果有“members initialization list”或者用户自己定义的代码,那么它们都将放置在vptr设定工作完成以后【如果两者都存在,那么“members initialization list”先于用户自定义代码】,以免发生错误的 virtual function调用。

      比如,如果我们定义的PVertex constructor如下:

PVertex::Pvertex(int x, int y, int z)
{
	cout << "PVertex class size is" << size() << endl;
}

那么它会在编译器内部被扩展成如下形式:

//注意,以下是C++伪码
PVertex::Pvertex(PVertex *this,bool _most_deriver,int x, int y, int z)
{
	//条件选择式决定要不要调用base class constructor
	if (_most_derived != false) this->Point::Point(x, y);

	//调用上一层的基类
	this->Vertex3d::Vertex3d(x, y, z);

	//就在这里!设定vptr!!
	this->_vptr_PVertex = _vtbl_PVertex;
	this->_vptr_Point_PVertex = _vtbl_Point_PVertex;

	//下面才是“members initialization list”和用户自定义代码
	cout << "PVertex class size is" 
		//以下是经由虚拟机制调用size虚函数
		<< (*this->_vptr_PVertex[3].faddr)(this)
		<< endl;
}

      在两种情况下,vptr必须被设定好。首先是一个完整类的对象构造完成之前,其次是一个subobject class constructor中要调用一个虚函数。现在的编译器,有的将constructor分裂为两个版本,一个是类完整版,一个是subobject版本,在subobject版本中,vptr的设定可以省略。

      但文中最后也说道:尽管vptr的设定是在用户代码或members initialization list之前,但在语意上来说,在constructor中使用虚函数依然有不安全的可能性,因为也许函数还依赖未被正确初始化的members,所以作者并不推荐文中做法。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章