正 文

掀起你的盖头来:谈VC++对象模型


www.7dspace.com  更新日期:2006-3-1 5:06:27  七度空间


  3、地址点与“逻辑this调整”

  考虑下一个虚函数S::rvf(),该函数覆盖了R::rvf()。我们都知道S::rvf()必须有一个隐藏的S*类型的this参数。但是,因为也可以用R*来调用rvf(),也就是说,R的rvf虚函数槽可能以如下方式被用到:

((R*)ps)->rvf(); // (*((R*)ps)->R::vfptr[1])((R*)ps)

  所以,大多数实现用另一个调整块将传递给rvf的R*转换为S*。还有一些实现在S的虚函数表末尾添加一个特别的虚函数项,该虚函数项提供方法,从而可以直接调用ps->rvf(),而不用先转换R*。MSC++的实现不是这样,MSC++有意将S::rvf编译为接受一个指向S中嵌套的R实例,而非指向S实例的指针(我们称这种行为是“给派生类的指针类型与该虚函数第一次被引入时接受的指针类型相同”)。所有这些在后台透明发生,对成员变量的存取,成员函数的this指针,都进行“逻辑this调整”。

  当然,在debugger中,必须对这种this调整进行补偿。

ps->rvf(); // ((R*)ps)->rvf(); // S::rvf((R*)ps)

  译者注:调用rvf虚函数时,直接给入R*作为this指针。

  所以,当覆盖非最左边的基类的虚函数时,MSC++一般不创建调整块,也不增加额外的虚函数项。

  4、调整块

  正如已经描述的,有时需要调整块来调整this指针的值(this指针通常位于栈上返回地址之下,或者在寄存器中),在this指针上加或减去一个常量偏移,再调用虚函数。某些实现(尤其是基于cfront的)并不使用调整块机制。它们在每个虚函数表项中增加额外的偏移数据。每当虚函数被调用时,该偏移数据(通常为0),被加到对象的地址上,然后对象的地址再作为this指针传入。

ps->rvf();
// struct { void (*pfn)(void*); size_t disp; };
// (*ps->vfptr[i].pfn)(ps + ps->vfptr[i].disp);

  译者注:当调用rvf虚函数时,前一句表示虚函数表每一项是一个结构,结构中包含偏移量;后一句表示调用第i个虚函数时,this指针使用保存在虚函数表中第i项的偏移量来进行调整。

  这种方法的缺点是虚函数表增大了,虚函数的调用也更加复杂。

  现代基于PC的实现一般采用“调整—跳转”技术:

S::pvf-adjust: // MSC++
this -= SdPR;
goto S::pvf()

  当然,下面的代码序列更好(然而,当前没有任何实现采用该方法):

S::pvf-adjust:
this -= SdPR; // fall into S::pvf()
S::pvf() { ... }

  译者注:IBM的C++编译器使用该方法。

  5、虚继承下的虚函数

  T虚继承P,覆盖P的虚成员函数,声明了新的虚函数。如果采用在基类虚函数表末尾添加新项的方式,则访问虚函数总要求访问虚基类。在VC++中,为了避免获取虚函数表时,转换到虚基类P的高昂代价,T中的新虚函数通过一个新的虚函数表获取,从而带来了一个新的虚函数表指针。该指针放在T实例的顶端。

struct T : virtual P {
   int t1;
   void pvf();         // overrides P::pvf
   virtual void tvf(); // new
};

void T::pvf() {
   ++p1; // ((P*)this)->p1++; // vbtable lookup!
   ++t1; // this->t1++;
}

  如上所示,即使是在虚函数中,访问虚基类的成员变量也要通过获取虚基类表的偏移,实行计算来进行。这样做之所以必要,是因为虚函数可能被进一步继承的类所覆盖,而进一步继承的类的布局中,虚基类的位置变化了。下面就是这样的一个类:

struct U : T {
   int u1;
};

  在此U增加了一个成员变量,从而改变了P的偏移。因为VC++实现中,T::pvf()接受的是嵌套在T中的P的指针,所以,需要提供一个调整块,把this指针调整到T::t1之后(该处即是P在T中的位置)。

  6、特殊成员函数

  本节讨论编译器合成到特殊成员函数中的隐藏代码。

  6.1 构造函数和析构函数

  正如我们所见,在构造和析构过程中,有时需要初始化一些隐藏的成员变量。最坏的情况下,一个构造函数要执行如下操作:

  * 如果是“最终派生类”,初始化vbptr成员变量,调用虚基类的构造函数;

  * 调用非虚基类的构造函数

  * 调用成员变量的构造函数

  * 初始化虚函数表成员变量

  * 执行构造函数体中,程序所定义的其他初始化代码

  (注意:一个“最终派生类”的实例,一定不是嵌套在其他派生类实例中的基类实例)

  所以,如果你有一个包含虚函数的很深的继承层次,即使该继承层次由单继承构成,对象的构造可能也需要很多针对虚函数表的初始化。

  反之,析构函数必须按照与构造时严格相反的顺序来“肢解”一个对象。

  * 合成并初始化虚函数表成员变量

  * 执行析构函数体中,程序定义的其他析构代码
 
  * 调用成员变量的析构函数(按照相反的顺序)

  * 调用直接非虚基类的析构函数(按照相反的顺序)

  * 如果是“最终派生类”,调用虚基类的析构函数(按照相反顺序)

  在VC++中,有虚基类的类的构造函数接受一个隐藏的“最终派生类标志”,标示虚基类是否需要初始化。对于析构函数,VC++采用“分层析构模型”,代码中加入一个隐藏的析构函数,该函数被用于析构包含虚基类的类(对于“最终派生类”实例而言);代码中再加入另一个析构函数,析构不包含虚基类的类。前一个析构函数调用后一个。

  6.2 虚析构函数与delete操作符

  考虑结构V和W。

struct V {
   virtual ~V();
};

struct W : V {
   operator delete();
};

  析构函数可以为虚。一个类如果有虚析构函数的话,将会象有其他虚函数一样,拥有一个虚函数表指针,虚函数表中包含一项,其内容为指向对该类适用的虚析构函数的地址。这些机制和普通虚函数相同。虚析构函数的特别之处在于:当类实例被销毁时,虚析构函数被隐含地调用。调用地(delete发生的地方)虽然不知道销毁的动态类型,然而,要保证调用对该类型合适的delete操作符。例如,当pv指向W的实例时,当W::~W被调用之后,W实例将由W类的 delete操作符来销毁。

V* pv = new V;
delete pv;   // pv->~V::V(); // use ::operator delete()
pv = new W;
delete pv;   // pv->~W::W(); // use W::operator delete()
pv = new W;
::delete pv; // pv->~W::W(); // use ::operator delete()

  译者注:

  ·V没有定义delete操作符,delete时使用函数库的delete操作符;

  ·W定义了delete操作符,delete时使用自己的delete操作符;

  ·可以用全局范围标示符显示地调用函数库的delete操作符。

  为了实现上述语意,VC++扩展了其“分层析构模型”,从而自动创建另一个隐藏的析构帮助函数——“deleting析构函数”,然后,用该函数的地址来替换虚函数表中“实际”虚析构函数的地址。析构帮助函数调用对该类合适的析构函数,然后为该类有选择性地调用合适的delete操作符。

6页,页码:[1] [2] [3] [4] [5] [6] 

上一篇:Linux下查找漏洞的N种兵器
下一篇:固步自封 Borland焉能不败
掀起你的盖头来:谈VC++对象模型 作者:程化编译 来源:blog
收藏此页】【打印】【关闭
站 内 搜 索
 

热 点 导 读
特 别 推 荐