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

第6章:执行期语意学

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

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

其中xx和yy定义为:

X xx;

Y yy;

Class Y定义为:

Class Y{

Public:

Y( );

~Y( );

Bool operator==(const Y&) const;

//...

};

Class X定义为:

Class X{

Public:

X( );

~X( );

Operator Y( ) const;

X getValue( );

//...

};

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

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

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

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

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

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

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

X temp1=xx.getValue( );

n 产生一个临时的class Y object,放置operator Y( )的返回值:

Y temp2=temp1.operator Y( );

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

Int temp3=yy.operator==(temp2);

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

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

{

X temp1=xx.getValue( );

Y temp2=temp1.operator Y( );

Int temp3=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版之前并不支持nonclass object的静态初始化操作:也就是说,C语言的限制仍然残留着。所以,像下面这样的例子,每一个初始化都被表示为不合法:

Externint i;

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

Int j =I;

Int* pi= new int(i);

Doublesal = cpmpute_sal(get_employee(i));

 

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

//constant expression

Vertex3d* pv=new Pvertex;

Point3d* p3d=pv;

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

//Point是Point3d的一个virtual base class

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

Point* pt=p3d;

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

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

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

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

局部静态对象(Local Static Objects)

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

Const Matrix&

Identity( ){

Static Matrix mat_identity;

//...

Return mat_identity;

}

Local static class object保证了什么样的语意?

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

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

编译器的策略之一就是,无条件地在程序其实(startup)时构造出对象来。然而这会导致所有的local static class objects都在程序起始时被初始化,即使它们所在的那个函数从不曾被调用过。因此,只在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)中存取它。解决的方法有点诡异:取出local object的地址。(由于object是static,其地址在downstream component中将会被转换到程序内用来放置global object的data segment中)下面是cfront的输出: 

 


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

 

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

对象数组

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

Point knots[10];

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

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

Void*

Vec_new(

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

Size_t elem_size, //每一个class object的大小

Int elem_count, //数组中的元素数目

Void (*constructor)(void*),

Void (*destructor)(void*,char)

}

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

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

Point knots[10];

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

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

Void*

Vec_delete(

Void* array,

Size_t elem_size,

Int elem_count,

Voie (*destructor)(void* ,char)

}

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

Point knots[10]={

Point( ),

Point(1.0,1.0,0.5),

-1.0

};

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

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,将无法(不被允许)存取default argument values

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

Class complex{

Complex(double=0.0,double=0.0);

...

}

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

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

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

但程序员写出:

Complex c_array[10];

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

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

&complex::complex,0);

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

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

//内部产生的stub constructor

//用以支持数组的构造

Complex::complex( )

{

Complex(0.0,0.0);

}

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

6.2 newdelete运算符

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

Int *pi=new int(5);

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

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

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

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

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

*pi=5;

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

Int* pi;

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

*pi=5;

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

Delete pi;

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

If(pi!=0)

__delete(pi);

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

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

If(pi==sentinel)...

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

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

Point3d* origin=new Point3d;

被转换为:

Point3d* origin;

//C++伪代码

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

Origin=Point3d::Point3d(origin);

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

//C++伪代码

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

{

Try{

Origin=Point3d::Point3d(origin);

}

Cathc(...)

{

//调用delete library function

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

__delete(origin);

//将原来的exception上传

Throw;

}

}

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

Delete origin;

会变成:

If(origin!=0)

{

//C++伪代码

Point3d:: ~Point3d(origin);

__delete(origin);

}

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

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

Extern void*

Operator new(size_t size)

{

If(size==0)

Size=1;

Void *last_alloc;

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

{

If(__new_handler)

(*__new_handler)( );

Else

Return 0;

}

Return last_alloc;

}

虽然这样写是合法的:

New T[0];

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

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

Extern void

Operator delete(void* ptr)

{

If(ptr)

Free((char*)ptr);

}

针对数组的new语意

当我们这么写:

Int* p_array=new int[5];

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

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

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

//struct simple_aggr{float f1,f2;};

Simple_aggr* p_aggr=new simple_aggr[5];

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

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

Point3d* p_array=new Point3d[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要被删除。如果程序员没有提供必须的中括号,像这样:

Delete p_array;

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

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

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

//完全不是个好主意

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

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

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

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

Delete[] ptr;

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

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

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

{

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

Delete p;

}

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

Placement Operator new的语意

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

Point2w* ptw=new(arena) Point2w;

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

Void* operator new(size_t,void* p)

{

Return p;

}

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

Placement new operator所扩充的另一半是将Point2w constructor自动实施于arena所指的地址上:

Point2w* ptw=(Point2w*)arena;

If(ptw!=0)

Ptw->Point2w::Point2w( );

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

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

//arena称为全局性定义

Void fooBar( ){

Point2w* p2w=new(arena)Point2w;

//...do it ...

//...now manipulate a new object...

P2w=new (arena)Point2w;

}

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

Delete p2w;

P2w=new(arena) Point2w;

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

P2w->~Point2w( );

P2w=new(arena)Point2w;

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

Point2w* p2w=new(arena) Point2w;

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

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

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

Char* arena=new char[sizeof(Point2w)];

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

Point2w* arena=new Point2w;

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

Point2w* p2w=new(arena) Point3w;

Point3wconstructor将会导致严重的破坏。

Placement new operator被引入C++2.0时,最晦涩隐暗的问题就是下面这个由Jonathan Shopiro提出的问题:

Struct Base{int j;virtual void f( );}

Struct Derived:Base{void f( );};

Void fooBar( )

{

Base b;

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

B.~base( );

New(&b)Derived;//1

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

}

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

6.3、临时性对象

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

T operator+(const T&,const T&);

以及两个T objects,ab,那么:

a+b;

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

T a,b;

T c=a+b;

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

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

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

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

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

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

T operator+(const T&,const T&);

T T::operator+(const T&);

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

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

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

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

//C++伪代码

//T temp=a+b;

T temp;

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

//c=temp

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

temp.T::~T( );

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

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

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

取代为其copy assignment运算符的隐含调用操作,以及一系列的destructorcopy construction:

//C++伪代码

c.T::~T( );

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

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

T c=a+b;

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

c=a+b;

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

a+b;//no target

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

String s("hello"),t("world"),u("!");

那么不论:

String v;

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时,例如:

Bool verbose;

...

String progNameVersion=

!Verbose

?0

:progName+progVersion;

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

String operator+(const string &,const string&);

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

progNameVersion.String::String(temp);

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

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

Const char* progNameVersion=

progName+progVersion;

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

String temp;

Operator+(temp,progName,progVersion);

progNameVersion=temp.String::operator char*( );

temp.String::~String( );

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

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

Const String& space="";

产生出这样的代码:

String temp;

temp.String::String("");

Const string &space=temp;

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

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值