`
xitonga
  • 浏览: 588409 次
文章分类
社区版块
存档分类
最新评论

深度探索C++对象模型:6.执行期语意学

 
阅读更多

第6章:执行期语意学

想象一下我们有下面这个简单的式子:

if(yy ==xx.getValue())...

其中xx和yy定义为:

Xxx;

Yyy;

ClassY定义为:

ClassY{

Public:

Y();

~Y();

Booloperator==(constY&)const;

//...

};

ClassX定义为:

ClassX{

Public:

X();

~X();

OperatorY()const;

XgetValue();

//...

};

首先,让我们决定equality(等号)运算符所参考到的真正实体。在这个例子中,它将被决议(resolves)为“被overloaded的Y成员实体”。下面是该式子的第一次转换:

If(yy.operator==(xx.getValue()))

Y的equality(等号)运算符需要一个类型为Y的参数,然而getValue()传回的却是一个类型为X的object。若非有什么方法可以把一个Xobject转换为一个Yobject,那么这个式子就算错!本例中X提供一个conversion运算符,把一个Xobject转换为一个Yobject。它必须施行于getValue()的返回值身上。下面是该式子的第二次转换:

If(yy.operator==(xx.getValue().operatorY()))

到目前为止所发生的一切都是编译器根据class的隐含语意,对我们的程序代码所做的“增胖”操作。如果我们需要,我们也可以明确地写出那样的式子。不,我们并不建议那么做,不过你如果那么做,会使编译器速度稍微快一些。

虽然程序的语意是正确的,当其教育性却尚不能说是正确的。接下来我们必须产生一个临时对象,用来放置函数调用所传回的值:

n产生一个临时的classXobject,用以放置getValue()的返回值:

Xtemp1=xx.getValue();

n产生一个临时的classYobject,放置operatorY()的返回值:

Ytemp2=temp1.operatorY();

n产生一个临时的intobject,放置equality(等号)运算符的返回值:

Inttemp3=yy.operator==(temp2);

最后,适当的desturctor将被施行于每一个临时性的classobject身上。这导致我们的式子被转换为以下形式:

//以下是条件句if(yy==xx.getValue())...的转换

{

Xtemp1=xx.getValue();

Ytemp2=temp1.operatorY();

Inttemp3=yy.operator==(temp2);

If(temp3)...

Temp2.Y::~Y();

Temp1.X::~X();

}

哦,代码似乎不少!这是C++的一件困难事情:不太容易从程序代码看出表达式的复杂度。

6.1对象的构造和析构

一般而言,constructor和destructor的安插都如你预期:

//C++伪代码

{

Point point;

//point.Point::Point( )一般而言会被安插在这里

...

//point.Point::~Point( )一般而言会被安插在这里

}

如果一个区段(译注:以{}括起来的区域)或函数中有一个以上的离开点,情况会稍微混乱一些。Destructor必须被放在每一个离开点(当时object还存活)之前,例如:

在这个例子中,point的destructor必须在switch指令四个出口的return操作前被生产出来。另外也很可能在这个区段的结束符号(右大括号)之前被生成出来——即使程序分析的结构发现绝不会进行到那里。

一般而言我们会把object尽可能放置在使用它的那个程序区段附近,这样做可以节省不必要的对象产生操作和摧毁操作。

全局对象

如果我们有以下程序片段:

Matrixidentity;

Main()

{

//identity必须在此被初始化

Matrixml=identity;

...

Return0;

}

C++保证,一定会在main( )函数中第一次用到identity之前,把identity构造出来,而在main()函数结束之前把identity摧毁掉。像identity这样的所谓的globalobject如果有constructor和destructor的话,我们说它需要静态的初始化和内存释放操作。

C++程序中所有的globalobjects都被放置在程序的datasegment中。如果明确指定给它一个值,object将以该值为初值。否则object所配置的内存内容为0。因此在下面这段代码中:

Intv1=1024;

Int v2;

V1和v2都被配置于程序的datasegment,v1值为1024,v2值为0(这和C略有不同,C并不自动设定初值)。在C语言中一个globalobject只能够被一个常量表达式(可以在编译时期求其值的那种)设定初值。当然,constructor并不是常量表达式,虽然classobject在编译时期可以被放置于datasegment中并且内容为0,但constructor一直要到程序激活(startup)时才会实施。必须对一个“放置于programdata segment中的object的初始化表达式”做评估,这正是为什么一个object需要静态初始化的原因。

当cfront还是唯一的C++编译器,而且跨平台移植性比效率的考虑更重要的时候,有一个可移植但成本颇高的静态初始化(以及内存释放)方法,我把它称为munch。下面是munch策略的实现:

1、为每一个需要静态初始化的档案产生一个__sti()函数,内带必要的constructor调用操作或inlineexpansions。

2、类似情况,在每一个需要静态的内存释放操作的文件中,产生一个__std()函数。

3、提供一组runtimelibrary "munch"函数:一个_main( )函数(用以调用可执行文件中的所有__sti( )函数),以及一个exit( )函数(以类似方式调用所有的__std( )函数)。

如图所示:


Cfront在你的程序中安插一个_main()函数调用操作,作为main()函数的第一个指令。这里的exit()和Clibrary的exit()不同。为了链接前者,在cfront的CC命令中必须先制定C++standard library。

最后一个需要解决的问题是,如何收集一个程序中的各个objectfiles的__sti()函数和__std()函数。

我们的解决方法是使用nm命令。nm会倾印出object file的符号表格项目。一个可执行文件系由.o文件产生出来,nm将施行于可执行文件身上。其输出被导入(“pipedinto”)munch程序中。Munch程序会“用力咀嚼”符号表格中的名称,搜寻以__sti或__std开头的名称,然后把函数名称加到一个sti()函数和std()函数的跳离表格中。接下来它把这个表格写到一个小的programtext文件中,然后,CC命令被重新激活,将这个内容表格的文件加以编译。然后整个可执行文件被重新链接。_main()和exit()于是在各个表格上走访一遍,轮流调用每一个项目(代表一个函数地址)。

Cfront2.0版之前并不支持nonclassobject的静态初始化操作:也就是说,C语言的限制仍然残留着。所以,像下面这样的例子,每一个初始化都被表示为不合法:

Externint i;

//以下操作都要求静态初始化,在cfront2.0前,这些都不合法

Int j =I;

Int* pi= new int(i);

Doublesal = cpmpute_sal(get_employee(i));

支持“nonclassobjects的静态初始化”,在某种程度上是支持virtualbaseclasses的一个副产品。Virtualbaseclasses怎么会扯进这个主题呢?哦,以一个derivedclass的pointers或reference来存取virtualbaseclasssubobject,是一种nonconstantexpression,必须在执行期才能加以评估求值。例如,尽管下面程序片段在编译器时期可知:

//constantexpression

Vertex3d*pv=newPvertex;

Point3d*p3d=pv;

其virtualbaseclassPoint的subobject在每一个derivedclass中的位置却可能会变动,因此不能够在编译时期设定下来.。下面的初始化操作:

//Point是Point3d的一个virtualbaseclass

//pt的初始化操作需要某种形式的执行器评估

Point*pt=p3d;

需要编译器提供内部扩充,以支持classobject的静态初始化(至少涵盖classobject的指针和references)。例如:

Point*pt=p3d->vbcPoint; //这是在执行期里做的评估

提供必要的支持以涵盖所有的nonclassobjects。

使用被静态初始化的objects有一些缺点。例如,如果exceptionhandling被支持,那些objects将不能够被放置于try区段之内。这对于被静态调用的constructors可能是特别无法接受的。因为任何的throw操作将必然触发exceptionhandlinglibrary默认的terminate()函数。另一个缺点是为了控制“需要跨越模块做静态初始化”objects的相依顺序而扯出来的复杂度。

局部静态对象(Local Static Objects)

假设我们有以下程序片段:

ConstMatrix&

Identity(){

StaticMatrixmat_identity;

//...

Returnmat_identity;

}

Localstaticclassobject保证了什么样的语意?

1.Mat_identity的constructor必须只能施行一次,虽然上述函数可能会被调用多次。

2.Mat_identity的destructor必须只能施行一次,虽然上述函数可能会被调用多次。

编译器的策略之一就是,无条件地在程序其实(startup)时构造出对象来。然而这会导致所有的localstaticclassobjects都在程序起始时被初始化,即使它们所在的那个函数从不曾被调用过。因此,只在identity()被调用时才把mat_identity构造起来,是比较好的做法(现在的C++Standard已经强制要求这一点)。我们该怎么做呢?

以下就是cfront之中的做法:首先,导入一个临时性对象以保护mat_identity的初始化操作。第一次处理identity时,这个临时对象被评估为false,于是constructor会被调用,然后临时对象被改为true。这样就解决了构造的问题。而在相反的那一端,destructor也需要有条件的施行与mat_identity身上,但只有在mat_identity已经被构造起来时才算数。要判断mat_identity是否被构造,很简单。如果那个临时对象为ture,就表示构造好了。困难的是,由于cfront产生C码,mat_identity对函数而言仍然是local,因此没有办法在静态的内存释放函数(std)中存取它。解决的方法有点诡异:取出localobject的地址。(由于object是static,其地址在downstreamcomponent中将会被转换到程序内用来放置globalobject的datasegment中)下面是cfront的输出:


最后,destructor必须在“与textprogramfile”有关联的静态内存释放函数中被有条件的调用:

请记住,指针的使用是cfront所特有的:然而条件是解构则是所有编译器都需要的。

对象数组

假设我们有下列的数组定义:

Pointknots[10];

需要完成什么东西呢?如果Point既没有定义一个constructor也没有定义一个destructor,那么我们的工作不会比建立一个“内建类型所组成的数组”更多。

然而Point的确定义了一个defaultdestructor,所以这个destructor必须轮流施行与每一个元素之上。一般而言这是经由一个或多个runtimelibrary函数达成。在cfront中,一个被命名为vec_new()的函数,产生出以classobject构造而成的数组。函数类型通常如下:

Void*

Vec_new(

Void*array,//数组起始地址

Size_telem_size,//每一个classobject的大小

Intelem_count,//数组中的元素数目

Void(*constructor)(void*),

Void(*destructor)(void*,char)

}

其中的constructordestructor参数是这个classdefaultconstructordefaultdestructor的函数指针。参数array带有的若不是具名数组的地址,就是0。如果是0,那么数组将经由应用程序的new运算符,被配置与堆上。

参数elem_count表示数组中的元素数目。在vec_new()中,constructor施行与elem_count个元素之上。对于支持exceptionhandling的编译器而言,vec_newdestructor的提供是必要的。下面是编译器可能针对我们的10Point元素所做的vec_nec)调用操作:

Pointknots[10];

Vec_new(&knots,sizeof(Point),10,&Point::Point,0);

如果Point也定义了一个destructor,当knots的生命结束时,该destructor也必须施行与那10Point元素身上。

Void*

Vec_delete(

Void*array,

Size_telem_size,

Intelem_count,

Voie(*destructor)(void*,char)

}

如果程序员提供一个或多个明显初值给一个用classobjects组成的数组,像下面这样,会如何:

Pointknots[10]={

Point(),

Point(1.0,1.0,0.5),

-1.0

};

对于那些明显获得初值的元素,vec_new()不再有必要。对于那些尚未被初始化的元素,vec_new()的施行方式就像面对“由classelemetns组成的数组,而该数组没有explicitinitializationlist”一样。因此上一个定义很可能被转换为:

Point knots[10];

Point::Point(&knots[0]);

Point::Point(&knots[1], 1.0, 1.0, 0.5);

Point::Point(&knots[2], -1.0, 0.0, 0.0);

//vec_new初始化后的7个元素

vec_new( &knots+3, sizeof(Point), 7, &Point::Point, 0);

DefaultConstructors和数组

如果你想在程序中取出一个constructor的地址,这是不可以的。当然啦,这是编译器在支持vec_new()时该做的事情。然而,经由一个指针来激活constructor,将无法(不被允许)存取defaultargumentvalues

举个例子,在cfront2.0之前,声明一个由classobjects所组成的数组,意味着这个class必须没有声明constructors或一个defaultconstructor(没有参数那种)。一个constructor不可以去一个或一个以上的默认参数值。这是违反直觉的,会导致以下的大错。下面是在cfront1.0中对于复数库的声明,你能够看出其中的错误吗?

Classcomplex{

Complex(double=0.0,double=0.0);

...

}

在当时的语言规则下,此复数函数库的使用者没办法声明一个由complexclassobjects组成的数组。

再一次地,让我们花点时间考虑,如何支持以下句子:

Complex::complex(double=0.0,double=0.0);

但程序员写出:

Complexc_array[10];

时,而编译器最终需要调用:

Vec_new(&c_array,sizeof(complex),10,

&complex::complex,0);

默认的参数如何能够对vec_new()而言有用?

很明显,有多种可能的实现方法。Cfrotn所采用的方法是产生一个内部的stubconstructor,没有参数。在其函数内部调用由程序员提供的constructor,并将default参数值明确地指定过去(由于constructor的地址已被取得,所以它不能够成为一个inline):

//内部产生的stubconstructor

//用以支持数组的构造

Complex::complex()

{

Complex(0.0,0.0);

}

编译器自己又一次违反了一个明显的语言规则:class如今支持了两个没带参数的constructors。当然,只有在classobjects数组真正被参生出来时,stub实体才会被产生以及被使用。

6.2 newdelete运算符

运算符new的使用,看起来似乎是个单一运算,像这样:

Int*pi=newint(5);

但事实上它是由以下两个步骤完成:

1、通过适当的new运算符函数实体,配置所需的内存:

//调用函数库中的new运算符

Int*pi=__new(sizeof(int));

2、给配置得来的对象设立初值:

*pi=5;

更进一步地,初始化操作应该在内存配置成功(经由new运算符)后才执行:

Int*pi;

If(pi=__new(sizeof(int)))

*pi=5;

Delete运算符的情况类似,当程序员写下:

Deletepi;

时,如果pi的值是0C++语言会要求delete运算符不要有操作。因此编译器必须为此调用构造一层保护膜:

If(pi!=0)

__delete(pi);

请注意pi并不会因此被自动清除为0,因此像这样的后继行为:

Pi所指对象之生命会因delete而结束。所以后继任何对pi的参考操作就不再保证有良好的行为,并因此被视为是一种不好的程序风格。然而,把pi继续当做一个指针来用,仍然是可以的,例如:

If(pi==sentinel)...

在这里,使用指针pi和使用pi所指之对象,其差别在于哪一个的生命已经结束了。虽然该地址上的对象不再合法,但地址本身却仍然代表一个合法的程序空间。因此pi能够继续被使用,当只能在受限制的情况下。

constructor来配置一个classobject,情况类似。例如:

Point3d*origin=newPoint3d;

被转换为:

Point3d*origin;

//C++伪代码

If(origin=__new(sizeof(Point3d)))

Origin=Point3d::Point3d(origin);

如果实现出exceptionhandling,那么转换结果可能会更复杂些:

//C++伪代码

If(origin=__new(sizeof(Point3d)))

{

Try{

Origin=Point3d::Point3d(origin);

}

Cathc(...)

{

//调用deletelibraryfunction

//释放因new而配置的内存

__delete(origin);

//将原来的exception上传

Throw;

}

}

Destructor的应用极为类似。下面的式子:

Deleteorigin;

会变成:

If(origin!=0)

{

//C++伪代码

Point3d::~Point3d(origin);

__delete(origin);

}

如果在exceptionhandling的情况下,destructor应该被放在一个try区段中。Exceptionhandler会调用delete运算符,然后再一次丢出该exception

一般的library对于new运算符的实现操作都很直接了当,但有两个精巧之处值得斟酌:

Externvoid*

Operatornew(size_tsize)

{

If(size==0)

Size=1;

Void*last_alloc;

While(!(last_alloc=malloc(size)))

{

If(__new_handler)

(*__new_handler)();

Else

Return0;

}

Returnlast_alloc;

}

虽然这样写是合法的:

NewT[0];

但语言要求每一次对new的调用都必须传回一个独一无二的指针。解决该问题的传统方法是传回一个指针,指向一个默认为1byte的内存区块(这就是为什么程序代码中的size被设为1的原因)。这个实现技术的另一个有趣之处是,它允许使用者提供一个属于自己的__new_handler()函数。这正是为什么每一次循环都调用__new_handler()之故。

New运算符实际上总是以标准的Cmalloc()完成,虽然并没有规定一定得这么做不可。相同的情况,delete运算符也重视以标准的cfree()完成:

Externvoid

Operatordelete(void*ptr)

{

If(ptr)

Free((char*)ptr);

}

针对数组的new语意

当我们这么写:

Int*p_array=newint[5];

时,vec_new()不会真正被调用,因为它的主要功能是把defaultconstructor施行于classobjects所组成的数组的每一个元素身上。倒是new运算符函数会被调用:

Int*p_array=(int*)__new(5*sizeof(int));

相同的情况,如果我们写:

//structsimple_aggr{floatf1,f2;};

Simple_aggr*p_aggr=newsimple_aggr[5];

Vec_new()也不会被调用。为什么呢?因为simple_aggr并没有定义一个constructordestructor,所以配置数组以及清除p_aggr数组的操作,只是单纯地获得内存和释放内存而已。这些操作由newdelete运算符来完成就绰绰有余了。

然而如果class定义有一个defaultconstructor,某些版本的vec_new()就会被调用,配置并构造classobjects所组成的数组。例如这个算式:

Point3d*p_array=newPoint3d[10];

通常会被编译为:

Point3d*p_array;

P_array=vec_new(0,sizeof(Point3d),10,

&Point3d::Point3d,

&Point3d::~Point3d);

还记得吗,在个别的数组元素构造过程中,如果发生exceptiondestructor就会被传递给vec_new()。只是已经构造妥当的元素才需要destructor的施行,因为它们的内存已经被配置出来了,vec_new()有责任在exception发生的时候把哪些内存释放掉。

程序员不需要在delete时指定数组元素的数目,寻找数组的大小由编译器来完成,因此我们现在可以这样写:

Delete[]p_array;

寻找数组维度给delete运算符的效率带来极大的影响,所以才导致这样的妥协:只有在中括号出现时,编译器才寻找数组的维度,否则它便假设只有单独一个objects要被删除。如果程序员没有提供必须的中括号,像这样:

Deletep_array;

那么就只有第一个元素会被解构。其它的元素仍然存在——虽然其相关的内存已经要求归还。

应该如何记录元素数目?一个明显的方法就是为vec_new()所传回的每一个内存区块配置一个额外的word,然后把元素数目包藏在那个word之中。通常这种被包藏的数值称为所谓的cookie(小甜饼)。

如果我们配置一个数组,内带10Point3dobjects,我们会预期PointPoint3dconstructor被调用各10次,每次作用于数组中的一个元素:

//完全不是个好主意

Point*ptr=newPoint3d[10];(现代版的编译器VC6.0可以正确的处理)

而当我们delete“由ptr所指向的10Point3d元素”时,会发生什么事情呢?很明显,我们需要虚拟机制的帮助,以获得预期的PointdestructorPoint3ddestructor10次的呼唤(每一次作用于数组中的一个元素):

//哦欧:这并不是我们所要的

//只有Point::~Point被调用......

Delete[]ptr;

施行于数组上的destructor,如我们所见,是根据交给vec_delete()函数之“被删除的指针类型的destructor”——本例中正是Pointdestructor

程序员应该怎样做才好?最好就是避免以一个baseclass指针指向一个derivedclassobjects所组成的数组——如果derivedclassobject比起base大的话。如果你真的一定得这样子写程序,解决之道在于程序员层面,而非语言层面:

For(intix=0;ix<elem_count;++ix)

{

Point3d*p=&((Point3d*)ptr)[ix];

Deletep;

}

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

PlacementOperatornew的语意

有一个预先定义好的重载的(overloaded)new运算符,称为placementoperatornew,它需要第二个参数,类型为void*。调用方式如下:

Point2w*ptw=new(arena)Point2w;

其中arena指向内存中的一个区块,用以放置新产生出来的Point2wobject。这个预先定义好的placementoperatornew的实现方法简直是出乎意料的平凡。它只要将“获得的指针”所指的地址传回即可:

Void*operatornew(size_t,void*p)

{

Returnp;

}

哦,事实上这只是所发生的操作的一半而已。另外一半无法由程序员产生出来

Placementnewoperator所扩充的另一半是将Point2wconstructor自动实施于arena所指的地址上:

Point2w*ptw=(Point2w*)arena;

If(ptw!=0)

Ptw->Point2w::Point2w();

这正是使placementoperatornew威力如此强大的原因。这一份代码决定objects被放置在哪里:编译器系统保证objectconstructor会施行于其上。

然而却有一个轻微的不良行为。下面是个有问题的程序片段:

//arena称为全局性定义

VoidfooBar(){

Point2w*p2w=new(arena)Point2w;

//...doit...

//...nowmanipulateanewobject...

P2w=new(arena)Point2w;

}

如果placementoperator在原已存在的一个object上构造新的object。而该像有的object有一个destructor,这个destructor并不会被调用。调用该destructor的方法之一是将那个指针delete掉。不过本例中你不能像下面这样做:

Deletep2w;

P2w=new(arena)Point2w;

是的,delete运算符会发生作用,这的确是我们所期望的。但是它也会释放由p2w所指的内存,这却不是我们所希望的,因为下一个指令就要用到p2w了。因此,我们应该明确地调用destructor并保留储存空间,以便在使用:

P2w->~Point2w();

P2w=new(arena)Point2w;

剩下的唯一问题是一个设计上的问题:在我们的例子中对placementoperator的第一次调用,会将新object构造与原已存在的object之上?还是会构造与全新地址上?也就是说,如果我们这样写:

Point2w*p2w=new(arena)Point2w;

我们如何知道arena所指向的这块区域是否需要先解构?这个问题在语言层面上并没有解答。一个合理的习俗是另执行new的这一端也要负责执行destructor的责任。

另一个问题关系到arena所表现的真正指针类型。C++Standard说它必须指向相同类型的class,要不就是一块“新鲜”内存,足够容纳该类型的object。注意,derivedclass很明显并不在被支持之列。对于一个derivedclass,或是其它没有关联的类型,其行为虽然并非不合法,却也未经定义。

“新鲜”的存储空间可以这样配置而来:

Char*arena=newchar[sizeof(Point2w)];

相同类型的object则可以这样获得:

Point2w*arena=newPoint2w;

不论哪一种情况,新的Point2w的存储空间的确是覆盖了arena的位置,而此行为已在良好的控制之下。然而,一般而言,placementnewoperator并不支持多态。被交给new的指针,应该适当地指向一块原先配置好的内存。如果derivedclass比其baseclass大,例如:

Point2w*p2w=new(arena)Point3w;

Point3wconstructor将会导致严重的破坏。

Placementnewoperator被引入C++2.0时,最晦涩隐暗的问题就是下面这个由JonathanShopiro提出的问题:

StructBase{intj;virtualvoidf();}

StructDerived:Base{voidf();};

VoidfooBar()

{

Baseb;

B.f();//base::f()被调用

B.~base();

New(&b)Derived;//1

B.f()//哪一个f()被调用

}

由于上述两个classes有相同的大小,故把derivedobject放在baseclass而配置的内存中是安全的。然而,要支持这一点,或许必须放弃对于“经由objects静态调用所有virtualfunctions(比喻b.f())”通常都会有优化处理。结果,placementnewoperator的这种使用方式在StandardC++中未能获得支持。于是上述程序的行为没有明确定义:我们不能过斩钉截铁地说哪一个f()函数实体会被调用。尽管大部分使用者可能以为调用的是Derived::f(),但大部分编译器调用的却是Base::f()

6.3、临时性对象

如果我们有一个函数,形式如下:

Toperator+(constT&,constT&);

以及两个Tobjects,ab,那么:

a+b;

可能会导致一个临时性对象,以放置传回的对象。是否导致一个临时性对象,视编译器的进取性以及上述操作发生时的程序上下关系而定。例如下面这个片段:

Ta,b;

Tc=a+b;

编译器会产生一个临时性对象,放置a+b的结果,然后再使用Tcopyconstructor,把该临时性对象当作c的初始值。然而更可能的转换时直接以拷贝构造的方式,将a+b的值放到c中,于是就不需要临时性对象,以及对其constructordestructor的调用了。

此外,视operator+()的定义而定,namereturnvalue(NRV)优化也可能实施起来。这将导致直接在上述c对象中求表达式结果,避免执行copyconstructor和具名对象的destructor

三种方式所获得的c对象,结果都一样。其间的差异在于初始化的成本。一个编译器可能给我们任何保证吗?严格地说没有。C++Standard允许编译器对于临时性对象的产生有完全的自由度。

理论上,C++Standard允许编译器厂商有完全的自由度。但实际上,由于市场的竞争,几乎保证任何表达式如果有这种形式:

Tc=a+b;//初始化,不会产生一个临时对象

而其中的加法运算符被定义为:

Toperator+(constT&,constT&);

TT::operator+(constT&);

那么实现时根本不产生一个临时性对象。

然而请注意,意义相当的assignment叙述句:

c=a+b;//赋值,会产生一个临时对象

不能够忽略临时性对象。相反,它会导致下面的结果:

//C++伪代码

//Ttemp=a+b;

Ttemp;

temp.operator+(a,b);//1

//c=temp

C.operator=(temp);//(2)

temp.T::~T();

标示为(1)的那一行,未构造的临时对象被赋值给operator+()。这意思是要不是“表达式的结果被copyconstructed至临时对象中”,就是“以临时对象取得NRV”。在后者中,原本要施行与NRVconstructor,现在将施行与该临时对象。

不管哪一种情况,直接传递c(上例赋值操作的目标对象)到赋值运算符函数中是有问题的,必须在此调用之前先调用destructor。然而,“转换”语意将被用来将下面的assignment操作:

c=a+b;//c.operator=(a+b);

取代为其copyassignment运算符的隐含调用操作,以及一系列的destructorcopyconstruction:

//C++伪代码

c.T::~T();

c.T::T(a+b);

Copyconstructor,destructor以及copyassignmentoperator都可以由使用者供应,所以不能够保证上述两个操作会导致相同的语意。因此,以一连串的destructioncopyconstruction来取代assignment,一般而言是不安全的,而且会产生临时对象,所以这样的初始化操作:

Tc=a+b;

总是比下面的操作更有效率地被编译器转换:

c=a+b;

第三种运算形式是,没有出现目标对象:

a+b;//notarget

这时候有必要产生一个临时对象,以放置运算后的结果。虽然看起来有点怪异,但这种情况实际上在子表达式中十分普遍,例如,如果我们这样写:

Strings("hello"),t("world"),u("!");

那么不论:

Stringv;

v=s+t+u;

Printf("%s\n",s+t);

都会导致产生一个临时对象,与s+t相关联。

最后一个表达式带来一些秘教式的论题,那就是“临时对象的生命期”。这个论题颇值得深入探讨。

C++Standard标准规格上说:临时性对象的被摧毁,应该是对完整表达式求值过程中的最后一个步骤。该完整表达式造成临时性对象的产生。

什么是一个完整表达式?非正式地说,它是被涵括的表达式中最外围的那个。下面这个式子:

((objA>1024)&&(objB>1024))

?objA+objB:foo(objA,objB);

一共有五个子算式,内带在一个“?:完整表达式”中。任何一个子表达式所产生的任何一个临时对象,都应该在完整表达式被求值完成后,才可以毁去。

当临时性对象是根据程序的执行器语意有条件地被产生出来时,临时性对象的生命规则就显得有些复杂了。举例如下:

If(s+t||u+v)

其中的u+v子算式只有在s+t被评估为false时,才会开始被评估。与第二个子算式有关的临时性对象必须被摧毁。但是,不可以被无条件地摧毁。也就是说,我们希望只有在临时性对象被产生出来的情况下才去摧毁它。

但临时性对象的生命规则有两个例外。第一个例外发生在表达式被用来初始化一个object时,例如:

Boolverbose;

...

StringprogNameVersion=

!Verbose

?0

:progName+progVersion;

其中progNameprogVersion都是Stringobjects。这时候会生出一个临时对象,放置加法运算符的运算结果:

Stringoperator+(conststring&,conststring&);

临时对象必须根据对verbose的测试结果有条件地解构。在临时对象的生命规则之下,它应该在完整的“?:表达式”结束之后尽快被摧毁。然而,如果progNameVersion的初始化需要调用一个copyconstructor:

progNameVersion.String::String(temp);

那么临时性对象的解构(在“?:完整表达式”之后)当然就不是我们所期望的。C++Standard要求说:凡含有表达式执行结果的临时性对象,应该存留到object的初始化操作完成为止。

甚至即使每一个都坚守C++Standard中的临时对象生命规则,程序员还是有可能让一个临时对象在他们的控制中被摧毁。其间的主要差异在于这时候的行为有明确的定义。例如,在新的临时对象规则中,下面这个初始化操作保证失败:

Constchar*progNameVersion=

progName+progVersion;

其中progNameprogVersion都是Stringobjects。产生出来的代码看起来像这样:

Stringtemp;

Operator+(temp,progName,progVersion);

progNameVersion=temp.String::operatorchar*();

temp.String::~String();

此刻progNameVersion指向未定义的heap内存!

临时性对象的生命规则的第二个例外是“当一个临时性对象被一个reference绑定”时,例如:

ConstString&space="";

产生出这样的代码:

Stringtemp;

temp.String::String("");

Conststring&space=temp;

很明显,如果临时性对象现在被摧毁,那个reference也就差不多没什么用了,。所以规则上说:

如果一个临时性对象被绑定于一个reference,对象将残留,直到被初始化之reference的生命结束,或知道临时对象的生命范畴结束——视哪一种情况先到达而定。

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics