正 文

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


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


  数组

  堆上分配空间的数组使虚析构函数进一步复杂化。问题变复杂的原因有两个:

  1、堆上分配空间的数组,由于数组可大可小,所以,数组大小值应该和数组一起保存。因此,堆上分配空间的数组会分配额外的空间来存储数组元素的个数;

  2、当数组被删除时,数组中每个元素都要被正确地释放,即使当数组大小不确定时也必须成功完成该操作。然而,派生类可能比基类占用更多的内存空间,从而使正确释放比较困难。


struct WW : W { int w1; };
pv = new W[m];
delete [] pv; // delete m W's  (sizeof(W)  == sizeof(V))
pv = new WW[n];
delete [] pv; // delete n WW's (sizeof(WW) >  sizeof(V))

  译者注:WW从W继承,增加了一个成员变量,因此,WW占用的内存空间比W大。然而,不管指针pv指向W的数组还是WW的数组,delete[]都必须正确地释放WW或W对象占用的内存空间。

  虽然从严格意义上来说,数组delete的多态行为C++标准并未定义,然而,微软有一些客户要求实现该行为。因此,在MSC++中,该行为是用另一个编译器生成的虚析构帮助函数来完成。该函数被称为“向量delete析构函数”(因其针对特定的类定制,比如WW,所以,它能够遍历数组的每个元素,调用对每个元素适用的析构函数)。

  异常处理

  简单说来,异常处理是C++标准委员会工作文件提供的一种机制,通过该机制,一个函数可以通知其调用者“异常”情况的发生,调用者则能据此选择合适的代码来处理异常。该机制在传统的“函数调用返回,检查错误状态代码”方法之外,给程序提供了另一种处理错误的手段。

  因为C++是面向对象的语言,很自然地,C++中用对象来表达异常状态。并且,使用何种异常处理也是基于“抛出的”异常对象的静态或动态类型来决定的。不光如此,既然C++总是保证超出范围的对象能够被正确地销毁,异常实现也必须保证当控制从异常抛出点转换到异常“捕获”点时(栈展开),超出范围的对象能够被自动、正确地销毁。

  考虑如下例子:

struct X { X(); }; // exception object class
struct Z { Z(); ~Z(); }; // class with a destructor
extern void recover(const X&);
void f(int), g(int);

int main() {
    try {
        f(0);
    } catch (const X& rx) {
        recover(rx);
    }
    return 0;
}

void f(int i) {
    Z z1;
    g(i);
    Z z2;
    g(i-1);
}

void g(int j) {
    if (j < 0)
        throw X();
}

  译者注:X是异常类,Z是带析构函数的工作类,recover是错误处理函数,f和g一起产生异常条件,g实际抛出异常。

  这段程序会抛出异常。在main中,加入了处理异常的try & catch框架,当调用f(0)时,f构造z1,调用g(0)后,再构造z2,再调用g(-1),此时g发现参数为负,抛出X异常对象。我们希望在某个调用层次上,该异常能够得到处理。既然g和f都没有建立处理异常的框架,我们就只能希望main函数建立的异常处理框架能够处理X异常对象。实际上,确实如此。当控制被转移到main中异常捕获点时,从g中的异常抛出点到main中的异常捕获点之间,该范围内的对象都必须被销毁。在本例中,z2和z1应该被销毁。

  谈到异常处理的具体实现方式,一般情况下,在抛出点和捕获点都使用“表”来表述能够捕获异常对象的类型;并且,实现要保证能够在特定的捕获点真正捕获特定的异常对象;一般地,还要运用抛出的对象来初始化捕获语句的“实参”。通过合理地选择编码方案,可以保证这些表格不会占用过多的内存空间。

  异常处理的开销到底如何?让我们再考虑一下函数f。看起来f没有做异常处理。f确实没有包含try,catch,或者是 throw关键字,因此,我们会猜异常处理应该对f没有什么影响。错!编译器必须保证一旦z1被构造,而后续调用的任何函数向f抛回了异常,异常又出了f 的范围时,z1对象能被正确地销毁。同样,一旦z2被构造,编译器也必须保证后续抛出异常时,能够正确地销毁z2和z1。

  要实现这些 “展开”语意,编译器必须在后台提供一种机制,该机制在调用者函数中,针对调用的函数抛出的异常动态决定异常环境(处理点)。这可能包括在每个函数的准备工作和善后工作中增加额外的代码,在最糟糕的情况下,要针对每一套对象初始化的情况更新状态变量。例如,上述例子中,z1应被销毁的异常环境当然与z2和 z1都应该被销毁的异常环境不同,因此,不管是在构造z1后,还是继而在构造z2后,VC++都要分别在状态变量中更新(存储)新的值。

  所有这些表,函数调用的准备和善后工作,状态变量的更新,都会使异常处理功能造成可观的内存空间和运行速度开销。正如我们所见,即使在没有使用异常处理的函数中,该开销也会发生。

  幸运的是,一些编译器可以提供编译选项,关闭异常处理机制。那些不需要异常处理机制的代码,就可以避免这些额外的开销了。

  小结

  好了,现在你可以写C++编译器了(开个玩笑)。

  在本文中,我们讨论了许多重要的C++运行实现问题。我们发现,很多美妙的C++语言特性的开销很低,同时,其他一些美妙的特性(译者注:主要是和 “虚”字相关的东西)将造成较大的开销。C++很多实现机制都是在后台默默地为你工作。一般说来,单独看一段代码时,很难衡量这段代码造成的运行时开销,必须把这段代码放到一个更大的环境中来考察,运行时开销问题才能得到比较明确的答案。

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

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

热 点 导 读
特 别 推 荐