《inside the c++ object model》读书笔记 之六 执行期语意学


6.1 对象的构造和解构:
  如果一个区段(以{}括起来的区域)或是函数中有一个以上的离开点,情况会混乱一些,因为destructor必须放在每一个离开点(当object还活着时)之前,同样,goto语句也可能需要许多个destructor调用操作,一般而言,我们会把object尽可能放置在使用它的那个程序区段附近,这样做可以节省不必要的对象产生和摧毁操作.

...全局对象:
例如有如下程序片段:

Matrix identity;

main()
{
    Matrix mi=identity;
    //...
    //...
    return 0;
}

  C++保证,一定会在main()函数中第一次用到identity之前,把indentity构造出来,而在main()函数结束之前把indentity摧毁掉,像indentity这样的global object,如果有constructor和destructor的话,我们说它需要静态的初始化操作和内存释放操作.
  C++程序中所有的global objects都被放置在程序的data segment中,如果明确指定给它一个值,object将以该值为初值,否则object所配置到的内存内容为0.
  虽然class object 在编译时期可以被放置于data segment中并且内容为0,但constructor一直要到程序激活时才会实施,必须对一个"放置区program data segment中的object初始化表达式"做评估,这正是为什么一个object需要静态初始化得原因.

  在cfront还是C++唯一的编译器时,静态初始化采用一个所谓的munch方案:
  1)为,诶一个需要静态初始化的档案产生一个_sti()函数,内带必要的constructor调用操作或inline expansions.
  2)在每一个需要静态初始化的内存释放操作的文件中,产生一个_std()函数,内带必要的destructor操作,或是其inline expansions
  3)提供一组runtime library "munch"函数:一个_main()函数(用以调用可执行文件中的所有_sti()函数),以及一个_exit()函数(以类似的方法调用所有的_std()函数).

  支持"nonclass objects"的静态初始化,在某种程度上是支持virtual base classes的一个副产品,因为,以一个derived class的pointer或是reference来存取virtual base class subobject,是一种nonconstant expression,必须在执行期才能评估求值.

...局部静态对象:

假设有如下程序片段:

const Matrix& inentity()
{
    static Matrix mat_identity;
    //...
    return mat_indentity;
}

Local static class object必须保证:
  1)mat_identity的constructor必须只能施行一次,虽然上述函数可能会被调用多次.
  2)mat_identity的destructor必须只能施行一次,虽然上述函数可能会被调用多次.

  为了支持上述特点,以前编译器的策略之一是,在程序起始(startup)时构造出对象来,这将导致所有的local static class object都必须在起始时被初始化,即使它们所在的函数不曾被调用,因此只有在indentity()函数被调用时才构造 mat_identity是比较好的做法(现在已经被强制要求做到这一点).
  为了实现这一点,在cfront之中的做法是:首先,导入一个临时性对象以保护mat_identity的初始化操作,第一次处理identity()函数时,该值为false,于是constructor被调用,然后临时对象的值被改为true,同样这destructor这一端,只有在临时对象的值为true时(对象被构造出来),才能调用destructor,但是由于cfront产生C码,mat_identity对函数而言还是local,没办法在静态的内存释放函数中存取它,所以一个比较诡异的解决方法就是:去处local object的地址,最后,destructor必须在"与text program file有关联的静态内存释放函数"中被有条件地调用.

...对象数组:
  例如有如下定义:
  Point knots[10];
  如果Point既没有定义一个constructor也没有定义一个destructor,那么我们的工作不会建立一个"内建类型所组成的数组"更多,也就是说我们只需配置10个连续的Point元素即可.
  然而如果一个class定义了一个default constructor,所以这个destructor必须轮流施行与每一个元素之上.一般而言,这是经由一个或多个runtime library函数达成,在cfront中,我们使用一个被命名为vec_new()的函数,产生出一class objects构造而成的数组,比较晚的编译器如:Borland,Microsoft...则是提供两个函数,一个用来"没有virtual base class"的class,另一个用来处理"内带virtual base class"的class,后哟个函数通常被称为vec_vnew().
  如果class也定义了一个destructor,当object数组的生命结束时,该destructor也必须施行于那10个objects上.类似地,这是经由一个类似的vec_delete()或是vec_vdelete()(带virtual base class的object)runtime library函数完成.
  如果程序员提供一个或多个明显的初值给一个有class objects组成的数组,那么,对于那些明显获得初值的元素,vec_new()不在有必要,对于那些尚未被初始化的元素,vec_new()的施行方式就像面对"由class elements组成的数组,而该数组没有explicit initialization list"一样.

...default constructor和数组:
  如果在一个程序中存取出一个constructor的地址,这是不可以的,这是编译器在支持vec_new()时该做的事情,然而经由一个指针来激活constructor,将无法存取default argument values.
  声明一个由class objects所组成的数组,而不赋初值,意味着这个class必须没有声明带参数的constructor或只有一个default constructor(不带参数或者是所有的参数都有默认值),当声明一个带有参数的constructor的class objects数组时,则必须为数组的每一个元素赋初值,调用构造函数.
  在cfront中的解决方法是,内部产生一个stub constructor,没有参数,在其函数内调用由程序员提供的constructor,并将default constructor参数数值明确地指定过去.
  编译器有一次违反了一个明显的语言规则:class如今支持了连个没有带参数的constructors,当然,只有当class objects数组真正被产生出来时,stub实体此才会被产生以及被使用.

6.2 new和delete运算符:
  当用new配置一个内建类型,比如:
  int *pi=new int(5);
  事实上它是有两个步骤完成:
  1)通过适当的new运算符函数实体,配置所需的内存.
  int *pi=_new(sizeof(int));
  2)给配置的得来的对象设立初值.
  *pi=5;
  更进一步,初始化操作应该在内存配置成功以后才执行:
  int *pi;
  if(pi=_new(sizeof(int)))
    *pi=5;
  delete的情况类似,对于:
  delete pi;
  如果pi的值是0,C++语言会要求delete运算符不要有操作:
  if(pi!=0)
    _delete(pi);
注:pi并不会因此被自动清除为0.
  pi所指向的对象之生命会因delete而结束.所以后继任何对pi的参考操作就不再保证有良好的行为,并因此被视为是一种不好的程序风格,然而,把pi继续当做一个指针来使用,仍然是可以的,在这里使用指针pi和使用pi所指的对象,其差别在于哪一个的声明已经结束了,虽然该地址上的对象不再合法,但地址本身确仍然代表一个合法的程序空间.因此pi能够继续被使用,但只能在受限制的情况下使用,很像一个void*指针的情况.

  以constructor来配置一个class object,情况类似:
  class T;
  T *pt=new T;
  转化如下:
  T *pt;
  if(pt=_new(sizeof(T)))
    pt=T:T(pt);
  destructor的应用极为类似:
  delete pt;
  转化如下:
  if(pt!=0)
  {
    T::~T(pt);
    _delete(pt);
  }
注:在exception情况下,转化的代码会复杂一些...

  一般的library对new运算符的实现操作都很直接了当,new运算符实际上是总以标准的C malloc()完成.催然并没有规定一定的如此,相同的情况下delete运算符也总是以标准的C free()完成.

...数组的new语意:
  例如如下程序片段:
  int *p_array=new int[5];
  其中vec_new不会真正被调用,因为它的主要功能是把default constructor施行与于class objects所组成的数组的每一个元素身上,倒是new运算符函数会被调用.
  有如:
  struct simple_aggr{float f1,f2;};
  simple_aggr *p_aggr=new simple_aggr[5];
  vec_new()也不会被调用,因为simple_aggr并没有定义一个constructor或destructor,所以配置数组以及清除p_aggr数组的操作,只是单纯地获得内存和释放而已,这些操作有new和delete运算符来完成就可以了.
  然而如果class定义了,一个default constructor,某些版本的vec_new就会调用,配置class objects所组成的数组.
  在delete数组时,应该这样写:
  delete []p_aggr;

注:一个比较好的设计习惯:最好避免一个base clss指针指向一个derived class objects所组成的数组-如果derived class object比起base 大的话(一般会如此),如果一定得这样做的话,解决之道在于程序员,而不再语言层:
  Point *ptr=new Point3d[10];//Point3d is a derived                 //class of Point
  for(int ix=0;ix<elem_count;++ix)
  {
    Point3d *p=&((Point3d*)ptr)[ix];
  }

  基本上,程序员必须迭代走过整个数组,把delete运算符实施于每一个元素身上,以此方式,调用操作将是virtual,因此,Point3d和Point 的destructor都会施行与数组中的每一个objects身上.

...placement operator new语意:
  有一个预先定义好的重载(overloaded)new运算符,称为placement operator new,它需要第二个参数,类型为void*,调用如下:
  class T;
  T *pt=new(arena)T;
  其中arena指向内存中的一个区块,用以方盒子新产生出来的T object.这个预先定义好的placement operator new的实现方法只要将"获得的指针(上例中的arena)"所指的地址传回即可:
  void *operator new(size_t,void *p)
  {
    return p;
  }
  其实,传回地址只是发生操作的一半,另一半是将T constructor自动实施于arena所指的地址上,所以完整的转化代码应该是:
  T *pt=(T*)arena;
  if(pt!=0)
  {
    pt->T::T();
  }
  如果placement operator在原已存在的一个object上构造新的object,而该现有的object有一个destructor,这个destructor并不会调用,调用该destructor的方法之一就是将那个指针delete掉,不过如下程序绝对是个错误:
  delete pt;
  pt=new(arena) T;
  的确,这样做会释放pt所指的内存,但是,下一个指令就要用到pt,因此,应该明确地调用destructor并保留储存空间,以便在使用:
  pt->~T();
  pt=new(arena) T;
注:C++ Standard中以一个placement operator delete矫正了这个错误,它会对object实施destructor,但不释放内存,所以就不必再直接调用destructor了.
  但是还剩下两个问题,一个是:如何知道arena所指的这块区域是否需要先解构?这个问题在语言层上并没有回答,一个合理的习俗是另执行new的这一端也要负责执行destructor的责任.
  另一个问题关系到arena所表现的真正指针类型.C++ Standard说它必须指向相同类型的class,要不就是一块"新鲜"内存,足够容纳该类型的object,显然,derived class不咋被支持之列.\
  新鲜内存可以这样获得:
  char *arena=newchar[sizeof(Point3d)];
  相同类型的object则可以这样获得:
  Point3d *arena=new Point3d;
  一般而言,placement new operator并不支持多态,被交给neew的指针,应该适当指向一块预先配置好的内存,如果derived class比base class大,那么就会产生很严重的问题.
  在placemen operator被引入C++2.0时会产生一个比较隐晦的问题,比如:
  class Base{int j;virtual void f()=0;};
  class Derived:Base{void f();}
  void f_n()
  {
    Base b;
    b.f();
    b.~Base();
    new(&b)Derived;
    b.f();
  }
  由于上述两个class有相同的大小,所以把derived class放在为base class配置的内存中是安全的,然而要支持对于"经由objects静态调用所有virtual funtions"通常会优化处理,结果,placement new operator的这种使用方式在 C++ Standard中未能获得支持,于是上述程序的行为没有明确的定义:不过大部分编译器的调用结果却是Base::f().

6.3 临时性对象:
  C++ Standard允许编译器对于临时性对象的产生有完全的自由度,但实际上,由于种种原因,几乎任何表达式如果有这种形式:
  T c=a+b;
  而其中的加法运算符被定义为:
  T operator+(const T&,const T&);
  或是:
  T operator+(const T&);
  那么实现时根本不产生一个临时对象,不过意义相当的assignment叙述句(statement):
  c=a+b;
  就不能够忽略临时对象.
  不管哪一种情况,直接传递c(上例中的目标对象)到运算符函数中是有问题的,由于运算符函数并不为其外加参数调用一个destructor(它期望一块"新鲜的"内存),所以必须在此调用之前调用destructor,然而,"转换"语意将被用来将下面的assignment操作:
  c=a+b;//c.operator(a+b);
  取代为其copy assignment运算符的隐含调用操作,以及一系列的destructor和copy constructor:
  //C++伪码
  c.T::~T();
  c.T::T(a+b);
  以上这些都可以有使用者提供,但是不能保证上述两个操作有相同的语意,因此,一连串的destruction,copy constructor取代assignment,一般而言是不安全的,而且会产生临时性对象,所以这样的初始化操作:T c=a+b;是比较可取的.
  除了,以上两种形式,还有一种形式就是:
  a+b;//没有目标对象;
  这时候有必要产生一个临时对象,以放置运算后的结果.

...临时对象的声明周期:
  C++ Standard中说明:临时性对象的摧毁,应该是对完整表达式求值过程中的最后一个步骤.该完整表达式造成临时对象的产生.
  完整表达式,非正式的说法就是:它是被涵括的表达式中最外围的那个,例如((obja>1024)&&(objb>1024))?obja+objb:foo(obja,objb);
  一共有五个算式,内带在一个"?:"完整表达式中.任何一个子表达式所产生的任何一个临时对象,都应该在完整表达式被求值后,才可以销毁.
  当临时性对象是根据程序执行期语意有条件被产生出来时,临时性对象的生命规则就有点复杂了,但是,很明显只有在临时性对象被产生出来的情况下才去摧毁它.

  关于临时对象的生命规则:
  把临时对象的destructor放在每一个算式的求值过程中,可以免除"努力追踪第二个子算式是否真的需要被评估",然而,在C++ Standard的临时对象生命规则中国,这样的方法不在被允许.临时性对象在完整的表达式尚未评估完全之前,不得被摧毁,也就是说,某些形式的条件测试现在必须被安插进来,以决定是否要摧毁和第二个算式有关的临时对象.
  但是,有两个例外,第一个例外发生在表达式被用来初始化一个object时,凡是含有表达式执行结果的临时性对象,应该在存留到object的初始化操作完成为止.
  第二个例外是"当一个临时性对象被一个reference 绑定"时,如果一个临时性对象绑定与一个reference,对象将残留,直到被初始化之reference的声明结束,或直到临时对象的生命范畴结束(视哪一种情况先到达而定).

转载于:https://www.cnblogs.com/suiyu/archive/2012/10/30/2746089.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值