设为首页 加入收藏

TOP

谈VC++对象模型(七)
2012-11-02 08:51:57 来源: 作者: 【 】 浏览:17818
Tags:对象 模型


    如上所示,即使是在虚函数中,访问虚基类的成员变量也要通过获取虚基类表的偏移,实行计算来进行。这样做之所以必要,是因为虚函数可能被进一步继承的类所覆盖,而进一步继承的类的布局中,虚基类的位置变化了。下面就是这样的一个类:
   
    struct U : T {
   
    int u1;
   
    };
   
    在此U增加了一个成员变量,从而改变了P的偏移。因为VC++(www.cppentry.com)实现中,T::pvf()接受的是嵌套在T中的P的指针,所以,需要提供一个调整块,把this指针调整到T::t1之后(该处即是P在T中的位置)。
   
    5.6 特殊成员函数
   
    本节讨论编译器合成到特殊成员函数中的隐藏代码。
   
    5.6.1 构造函数和析构函数
   
    正如我们所见,在构造和析构过程中,有时需要初始化一些隐藏的成员变量。最坏的情况下,一个构造函数要执行如下操作:
   
    * 如果是"最终派生类",初始化vbptr成员变量,调用虚基类的构造函数;
   
    * 调用非虚基类的构造函数
   
    * 调用成员变量的构造函数
   
    * 初始化虚函数表成员变量
   
    * 执行构造函数体中,程序所定义的其他初始化代码
   
    (注意:一个"最终派生类"的实例,一定不是嵌套在其他派生类实例中的基类实例)
   
    所以,如果你有一个包含虚函数的很深的继承层次,即使该继承层次由单继承构成,对象的构造可能也需要很多针对虚函数表的初始化。
   
    反之,析构函数必须按照与构造时严格相反的顺序来"肢解"一个对象。
   
    * 合成并初始化虚函数表成员变量
   
    * 执行析构函数体中,程序定义的其他析构代码
   
    * 调用成员变量的析构函数(按照相反的顺序)
   
    * 调用直接非虚基类的析构函数(按照相反的顺序)
   
    * 如果是"最终派生类",调用虚基类的析构函数(按照相反顺序)
   
    在VC++(www.cppentry.com)中,有虚基类的类的构造函数接受一个隐藏的"最终派生类标志",标示虚基类是否需要初始化。对于析构函数,VC++(www.cppentry.com)采用"分层析构模型",代码中加入一个隐藏的析构函数,该函数被用于析构包含虚基类的类(对于"最终派生类"实例而言);代码中再加入另一个析构函数,析构不包含虚基类的类。前一个析构函数调用后一个。
   
    5.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++(www.cppentry.com)扩展了其"分层析构模型",从而自动创建另一个隐藏的析构帮助函数--"deleting析构函数",然后,用该函数的地址来替换虚函数表中"实际"虚析构函数的地址。析构帮助函数调用对该类合适的析构函数,然后为该类有选择性地调用合适的delete操作符。
   
    6 数组
   
    堆上分配空间的数组使虚析构函数进一步复杂化。问题变复杂的原因有两个:
   
    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];
   
   explain select * [] pv;// delete n WW's (sizeof(WW) >  sizeof(V))
   
    译者注:WW从W继承,增加了一个成员变量,因此,WW占用的内存空间比W大。然而,不管指针pv指向W的数组还是WW的数组,delete[]都必须正确地释放WW或W对象占用的内存空间。
   
    虽然从严格意义上来说,数组delete的多态行为C++(www.cppentry.com)标准并未定义,然而,微软有一些客户要求实现该行为。因此,在MSC++(www.cppentry.com)中,该行为是用另一个编译器生成的虚析构帮助函数来完成。该函数被称为"向量delete析构函数"(因其针对特定的类定制,比如WW,所以,它能够遍历数组的每个元素,调用对每个元素适用的析构函数)。
   
    7 异常处理
   
    简单说来,异常处理是C++(www.cppentry.com)标准委员会工作文件提供的一种机制,通过该机制,一个函数可以通知其调用者"异常"情况的发生,调用者则能据此选择合适的代码来处理异常。该机制在传统的"函数调用返回,检查错误状态代码"方法之外,给程序提供了另一种处理错误的手段。
   
    因为C++(www.cppentry.com)是面向对象的语言,很自然地,C++(www.cppentry.com)中用对象来表达异常状态。并且,使用何种异常处理也是基于"抛出的"异常对象的静态或动态类型来决定的。不光如此,既然C++(www.cppentry.com)总是保证超出范围的对象能够被正确地销毁,异常实现也必须保证当控制从异常抛出点转换到异常"捕获"点时(栈展开),超出范围的对象能够被自动、正确地销毁。
   
    考虑如下例子:
   
    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++(www.cppentry.com)都要分别在状态变量中更新(存储)新的值。
   
    所有这些表,函数调用的准备和善后工作,状态变量的更新,都会使异常处理功能造成可观的内存空间和运行速度开销。正如我们所见,即使在没有使用异常处理的函数中,该开销也会发生。
   
    幸运的是,一些编译器可以提供编译选项,关闭异常处理机制。那些不需要异常处理机制的代码,就可以避免这些额外的开销了。
   
    8 小结
   
    好了,现在你可以写C++(www.cppentry.com)编译器了(开个玩笑)。
   
    在本文中,我们讨论了许多重要的C++(www.cppentry.com)运行实现问题。我们发现,很多美妙的C++(www.cppentry.com)语言特性的开销很低,同时,其他一些美妙的特性(译者注:主要是和"虚"字相关的东西)将造成较大的开销。C++(www.cppentry.com)很多实现机制都是在后台默默地为你工作。一般说来,单独看一段代码时,很难衡量这段代码造成的运行时开销,必须把这段代码放到一个更大的环境中来考察,运行时开销问题才能得到比较明确的答案。

                  

首页 上一页 4 5 6 7 8 9 10 下一页 尾页 7/47/47
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
分享到: 
上一篇用VC++制作DLL经验 下一篇VC++数据类型

评论

帐  号: 密码: (新用户注册)
验 证 码:
表  情:
内  容:

最新文章

热门文章

C 语言

C++基础

windows编程基础

linux编程基础

C/C++面试题目