想象一下咱们有下面这个简单的式子:node
if( yy == xx.getValue() ) ...
其中xx和yy定义为:程序员
X xx; Y yy;
class Y定义为:express
class Y { public: Y(); ~Y(); bool operator==( const Y& ) const; // ... };
class X定义为:数组
class X { public: X(); ~X(); operator Y() const; // conversion运算符 // ... };
先看看开始那个表达式该如何处理。安全
首先,让咱们决定equality(等号)运算符所参考到的真正实例。在这个例子中,它将被决议cookie
为“被overloaded的Y成员实例”。下面是该式子的第一次转换:ide
// resolution of intended operator if( yy.operator==( xx.getValue() ) )
Y的equality(等号)运算符须要一个类型为Y的参数,然而getValue()传回的倒是一个类型函数
为X的object。若非有什么办法能够把一个X object转换为一个Y object,那么这个式子就算错!测试
本例中X提供一个conversion运算符,把一个X object转换为一个Y object。它必须施行于优化
getValue()的返回值上。下面是该式子的第二次转换:
// conversion of getValue()'s return value if( yy.operator==( xx.getValue().operator Y() ) )
到目前为止所发生的一切都是编译器根据class的隐含语意,对咱们的程序代码所作的增胖
操做。若是咱们须要,咱们也能够明确地写出那样的式子。不过不建议,这样作会使编译速度
稍微快一些。
虽然程序的语意是正确的,其教育性却尚不能说是正确的。接下来咱们必须产生一个临时对
象,用来放置函数调用所传回的值:
1)产生一个临时的class X object,放置getValue()的返回值:
X temp1 = xx.getValue();
2)产生一个临时的class Y object,放置operator Y()的返回值:
Y temp2 = temp1.operator Y();
3)产生一个临时的int object,放置equality(等号)运算符的返回值:
int temp3 = yy.operator==( temp2 );
最后,适当的destructor将被施行于每个临时性的class object身上。这致使咱们的式子被
转换为如下形式:
// C++伪码 // 如下是条件句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++的一件困难事情:不太容易从程序源码看出表达式的复杂度。下面就是执行期所发
生的一些转换。
通常而言,constructor和destructor的安插都如你所预期的那样:
// C++伪码 { Point point; // point.Point::Point() 通常而言会安插在这里 ... // point.Point::~Point() 通常而言会被安插在这里 }
若是一个区段({}括起来的区域)或函数中有一个以上的离开点,状况会稍微混乱一些。
Destructor必须被放在每个离开点(当时object还存活)以前,例如:
{ Point point; // constructor 在这里行动 switch( int( Point.x() ) ) { case -1: // mumble; // destructor 在这里行动 return; case 0: // mumble; // destructor 在这里行动 return; case 1: // mumble; // destructor 在这里行动 return; default: // mumble; // destructor 在这里行动 return; } // destructor 在这里行动 }
在这个例子中,point的destructor必须在switch指令4个出口的return操做前被生成出来。另
外也颇有可能在这个区段的结束符号(右大括号)以前被生成出来——即便程序分析的结果发
现毫不会进行到那里。
一样的道理,goto指令也可能须要许多个destructor调用操做。例以下面的程序片断:
{ if( cache ) // 检查cache;若是吻合就传回1 return 1; Point xx; // xx的constructor 在这里行动 while( cvs.iter( xx ) ) if( xx == value ) goto found; // xx的destructor 在这里行动 return 0; found: // cache item // xx的destructor 在这里行动 return 1; }
Destructor调用操做必须被放在最后两个return指令以前,可是却没必要被放在最初的return之
前,那固然是由于那时object还没有被定义出来!
通常而言咱们会把object尽量放置在使用它的那个程序区段附近,这么作能够节省非必要
的对象产生操做和摧毁操做。以本例而言,若是咱们在检查cache以前就定义了Point object,
那就不够理想。这个道理彷佛很是明显,但许多Pascal或C程序员使用C++的时候,仍然习惯把
全部的objects放在函数或某个区段的起始处。
若是咱们有如下程序片断:
Matrix identity; main() { // identity 必须在此被初始化 Matrix m1 = identity; ... return 0; }
C++保证,必定会在main()函数中第一次用到identity以前,把identity构造出来,而在main()
函数结束以前把identity摧毁掉。像identity这样的所谓global object若是有constructor和
destructor的话,咱们就说它须要静态的初始化操做和内存释放操做。
C++程序中全部的global objects都被放置在程序的data segment中。若是显式指定给它一个
值,此object将以该值为初值。不然object所配置到的内存内容为0。所以在下面这段代码中:
int v1 = 1024; int v2;
v1和v2都被配置于程序的data segment,v1值为1024,v2值为0(这和C略有不一样,C并不
自动设定初值)。在C语言中一个global object只可以被一个常量表达式(可在编译时期求其值
的那种)设定初值。固然,constructor并非常量表达式。虽然class object在编译时期能够被
放置于data segment中而且内容为0,但constructor一直要到程序启动(startup)时才会实施。
必须对一个“放置于program data segment中的object的初始化表达式”作评估,这正式为何一
个object须要静态初始化的缘由。
当cfront仍是惟一的C++编译器,并且跨平台移植性比效率的考虑更重要的时候,有一个可
移植但成本颇高的静态初始化(以及内存释放)方法,称为munch。cfront的束缚是,它的解决
方案必须在每个UNIX平台上——从Cray到VAX,再从Sun到UNIX PC——都有效。所以不论
是相关的linker或object-file format,都不能预先作任何假设。因为这样的限制,下面这些munch
策略就浮现出来了:
1)为每个须要静态初始化的文件产生一个_sti()函数,内含必要的constructor调用操做或
inline expansions。例如前面所说的identity对象会在matrix.c中产生出下面的_sti()函数(sti就是
static initialization的缩写):
_sti_matrix_c_identity() { // C++代码 identity.Matrix::Matrix(); // 这就是static initialization }
其中matrix_c是文件名编码,_identity表示文件中所定义的一个static object。在_sti以后附
加上这两个名称,能够为可执行文件提供一个独一无二的标识符号。
2)相似状况,在每个须要静态的内存释放操做(static deallocation)的文件中,产生一
个_std()函数(std就是static deallocation的缩写),内含必要的destructor调用操做,或是其
inline expansions。在咱们的例子中会有一个_std()函数被产生出来,针对identity对象调用
Matrix destructor。
3)提供一组runtime library “munch”函数:一个_main函数(用以调用可执行文件中的全部
_sti()函数),以及一个exit()函数(用相似的方式调用全部的_std()函数)。
cfront在程序中安插一个_main()函数调用操做,做为main()函数的第一个指令。这里的exit
和C library的exit()不一样,为了连接前者,在cfront的CC命令中必须先指定C++ standard
library。通常而言这样就能够了,但有些编译系统拒绝munch exit()函数。
最后一个须要解决的问题是,如何收集一个程序中各个object files的_sti()函数和_std()函
数。它必须是可移植的——虽然移植性限制在UNIX平台。
解决办法是使用nm命令。nm会倾印(dump)出object file的符号表格项目(symbol table
entries)。一个可执行文件是由.o文件产生出来的,nm将施行于可执行文件身上。其输出被导
入(“piped into”)munch程序中。munch程序会分析符号表格中的名称,搜寻以_sti或_std开头
的名称,而后把函数名称加到一个sti()函数和std()函数的跳离表格(jump table)中。接下来它
把这个表格写到一个小的program text文件中,而后,CC命令被从新激活,将这个内含表格的
文件加以编译。整个可执行文件而后被从新连接。_main()和exit因而在各个表格上走访一遍,
轮流调用每个项目(表明一个函数地址)。
这个作法能够解决问题,但彷佛离正统的计算机科学远了一些。其修补版(patch)假设
可执行文件是System V COFF(Common Object File Format)格式,因而它检验可执行文件并
找出那些“有着_link node”并内含一个指针,指向_sti()和_std()函数“的文件,将它们通通串链在
一块儿。接下来它把链表的根源设为一个全局性的_head object(定义于新的patch runtime library
中)。这个patch library内含另外一个不一样的_main()函数和exit()函数,将以_head为起始的链表走
访一遍。最后针对Sun、BSD以及ELF的其余patch libraries终于也有各个使用者团体捐赠出
来,用以和各式各样的cfront版本搭配。
当特定平台上的C++编译器开始出现,更有效率的方法也就有可能随之出现,由于各平台有
可能扩充连接器和目的文件格式(object file format),以求直接支持静态初始化和内存释放操
做。例如,System V的Executable and Linking Format(ELF)被扩充以增长支持.init和.fini两
个section,这两个sections内含对象所须要的信息,分别对应于静态初始化和释放操做。编译
器特定(Implementation-specific)的startup函数(一般名为crt0.o)会完成平台特定
(platform-specific)的支持(分别针对静态初始化和释放操做的支持)。
cfront 2.0版以前并不支持nonclass object的静态初始化操做:也就是说C语言的限制仍然残
留着。因此,像下面这样的例子,每个初始化操做都被标示为不合法:
extern int i; // 所有都要求静态初始化(static initialization) // 在2.0版之前的C和C++中,这些都是不合法的 int j = i; int *pi = new int( i ); double sal = compute_sal( get_emplopyee( i ) );
支持“nonclass objects的静态初始化”,在某种程度上,是支持virtual base classes的一个副
产品。virtual base classes怎么会扯进这个主题呢?以一个derived class的pointer或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的初始化操做须要 // 某种形式的执行期评估(runtime evaluation) Point *pt = p3d;
须要编译器提供内部扩充,以支持class object的静态初始化(至少涵盖class objects的指针
和reference)。例如:
// Initial support of virtual base class conversion // requires non-constant initialization support Point *pt = p3d->vbcPoint;
提供必要的支持以涵盖全部的nonclass objects,并不须要走太远的路。
使用被静态初始化的objects,有一些缺点。例如,若是exception handling被支持,那些
objects将不能被放置于try区段以内。这对于被静态调用的constructors多是特别没法接受的,
由于任何的throw操做将必然触发exception handling library默认的terminate()函数。另外一个缺点
是为了控制“须要跨越模块作静态初始化”之objects的相依顺序,而扯出来的复杂度。建议根本
不要用那些须要静态初始化的global 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是否被构造起来,很简单,若是那个临时对象为true,
就表示构造好了。困难的是,因为cfront产生C码,mat_identity对函数而言仍然是local,所以我
没办法在静态的内存释放函数(static deallocation function)中存取它。解决办法优势诡异,结
构化语言避之惟恐不及:取出local object的地址。(因为object是static,其地址在downstream
component中将会被转换到程序用来放置global object的data segment中)。下面是cfront的输
出:
// 被产生出来的临时对象,做为戒护之用 static struct Matrix *_0_F3 = 0; // C++的reference在C中是以pointer来代替的 // identity()的名称会被mangled struct Matrix* identity_Fv() { // _1 反映出语汇层面的设计, // 使得C++得以支持这样的代码: // int val; // int f() { int val; // return val + ::val; } // 最后一行会变成: // ... return _lval + val; static struct Matrix _lmat_identity; // 若是临时性的保护对象已被设立,那就什么也别作,不然 // (a) 调用constructor:_ct_6MatrixFv // (b) 设定保护对象,使它指向目标对象 _0_F3 ? 0 : ( _ct_6MatrixFv( &_lmat_identity ), ( _0_F3 = ( &_lmat_identity ) ) ); ... }
最后,destructor必须在“与text program file(也就是本例中的stat_0.c)有关联的静态内存内
存释放函数 (static deallocation function)“中被有条件地调用:
char _std_stat_0_c_j() { _0_F3 ? _dt_6MatrixFv( _0_F3, 2 ) : 0; ... }
请记住,指针的使用是cfront所特有的:然而条件式析够则是全部编译器都须要的。C++标
准委员会新的规则要求编译单位中的static local class objects必须被摧毁——以构造的的相反顺
序摧毁。因为这些objects是在须要时才被构造(例如每个含有static local class objects的函
数第一次被进入时),因此编译时期没法预期其集合以及顺序。为了支持新的规则,可能须要
对被产生的static class objects保持一个执行期链表。
假设咱们有下列的数组定义:
Point knots[ 10 ];
若是Point既没有定义一个constructor也没有定义一个destructor,那么咱们的工做不会比建
立一个”内建(build-in)类型所组成的数组“更多,也就是说咱们只要配置足够内存以存储10个
连续的Point元素便可。
然而Point的肯定义了一个default destructor,因此这个destructor必须轮流施行于每个元
素之上。通常而言这是经由一个或多个runtime library函数达成的。在cfront中,咱们使用一个被
命名为vec_new()的函数,产生出以class objects构造而成的数组。比较新近的编译器,包括
Borland、Microsoft和Sun,则是提供两个函数,一个用来处理”没有virtual base class“的
class,另外一个用来处理”内含virtual base class“的class。后一个函数一般被称为vec_vnew()。
函数类型一般以下(固然在各平台上可能会有些许差别):
void* vec_new( void *array, // 数组起始地址 size_t elem_size, // 每个class object的大小 int elem_count, // 数组中的元素个数 void ( *constructor )( void* ), void ( *destructor )( void*, char ) )
其中的constructor和destructor参数是这一class之default constructor和default destructor的函
数指针。参数array持有的若不是具名数组(本例为knots)的地址,就是0。若是是0,那么数组
将经由应用程序的new运算符,被动态配置于heap中。Sun把”由class objects所组成的具名数
组“和”动态配置而来的数组“的处理操做分为两个library函数:_vector_new2和_vector_con,它
们各自拥有一个virtual base class函数实例。
参数elem_size表示数组中的元素个数。在vec_new()中,constructor施行于elem_count个元
素上,对于支持exception handling的编译器而言,destructor的提供是必要的。下面下面是编
译器可能针对咱们的10个Point元素所作的vec_new()调用操做:
Point knots[ 10 ]; vec_new( &knots, sizeof( Point ), 10, &Point::Point, 0 );
若是Point也定义了一个destructor,当knots的生命结束时,该destructor也必须施行于那10
个Point元素身上。这是经由一个相似的vec_delete()(或是一个vec_vdelete()——若是classes
拥有virtual base classes的话)的runtime library函数完成(Sun对于”具名数组“和”动态配置而
来的数组“,处理方式不一样)的,其函数类型以下:
void* vec_delete( void *array, // 数组起始地址 size_t elem_size, // 每个class object的大小 int elem_count, // 数组的元素个数 void ( *destuctor )( void*, char ) )
有些编译器会另外增长一些参数,用以传递其余数值,以便可以有条件地导引vec_delete()的
逻辑。在vec_delete()中,destructor被施行于elem_count个元素身上。
若是程序员提供一个或多个明显初始值给一个由class objects组成的数组,像下面这样,会
如何:
Point knots[ 10 ] = { Point(), Point( 1.0, 1.0, 0.5 ), -1.0 };
对于那些明显得到初值的元素,vec_new()再也不有必要。对于那些还没有被初始化的元
素,vec_new()的施行方式就像面对”由class elements组成的数组,而该数组没有explicit
initialization list“同样。所以上一个定义极可能被转换为:
Point knots[ 10 ]; // C++伪码 // 显式地初始化前3个元素 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 );
四、Default Constructors和数组
若是想要在程序中取出一个constructor的地址,是不能够的。固然,这是编译器在支持vec_new()时该作的事情。然而, 经由一个指针来启动constructor,将没法(不被容许)存取default argument values。
例如,在cfront2.0以前,声明一个由class objects所组成的数组,意味着这个class必须没有
声明constructors或一个default constructor(没有参数那种)。一个constructor不能够取一个或
一个以上的默认参数值。这是违反直觉的,会致使如下的大错。下面是cfront 1.0中对于负数函
数库(complex library)的声明,能看出其中的错误?
class complex { complex( double = 0.0, double = 0.0 ); ... }
在当时的语言规则下,此复数函数库的使用者没有办法声明一个由complex class objects组
成的数组。显然咱们在语言的一个陷阱上被绊倒了。在1.1版,修改的是class library;然而在2.0
版,修改是语言自己。
再一次,如何支持如下句子:
complex::complex( double = 0.0, double = 0.0 );
当程序员写出:
complex c_array[ 10 ];
时,而编译器最终须要调用:
vec_new( &c_array, sizeof( complex ), 10, &complex::complex, 0 );
默认参数如何可以对vec_new()而言有用?
很明显,有数种可能的实现方法。cfront所采用的方法是产生一个内部的stub constructor,
没有参数。在其函数内调用由程序员提供的constructor,并将default参数值显示地指定过去
(因为constructor的地址已被取得,因此它不可以成一个inline):
// 内部产生的stub constructor // 用以支持数组的构造 complex::complex() { complex( 0.0, 0.0 ); }
编译器本身又一次违反了一个明显的语言规则:class现在支持了两个没有带参数的
constructors。固然当class object数组真正被产生出来时,stub实例才会被产生以及被调用。
运算符new的使用,看起来彷佛时单一运算,像这样:
int *pi = new int( 5 );
但事实上是由两个步骤完成的:
1)经过适当的new运算符函数实例,配置所需的内存:
// 调用函数库中的new运算符 int *pi = _new( sizeof( int ) );
2)将配置得来的对象设立初值:
*pi = 5;
更进一步地说,初始化操做应该在内存配置成功(经由new运算符)后才执行:
// new运算符的两个分离步骤 // given: int *pi = new int( 5 ); // 重写声明 int *pi; if( pi = _new( sizeof( int ) ) ) *pi = 5; // 成功了才初始化
delete运算符的状况相似。当写下:
delete pi;
时,若是pi的值是0,C++语言会要求delete运算符不要有操做。所以编译器必须为此调用构
造一层保护膜:
if( pi != 0 ) _delete( pi );
请注意pi并不会所以被自动清除为0,所以像这样的后继行为:
// 没有良好的定义,可是合法 if( pi && *pi == 5 ) ...
虽然没有良好的定义,可是可能(也可能不)被评为真。这是由于对于pi所指向以内存的变
更或再使用,可能(也可能不)会发生。
pi所指对象的生命会因delete而结束。因此后继任何对pi的参考操做就再也不保证有良好的行
为,并所以被视为一种很差的程序风格。然而,把pi继续当作一个指针来用,仍然是能够的
(虽然其使用受到限制),例如:
// ok:pi仍然指向合法空间 // 甚至即便存储于其中的object已经再也不合法 if( pi == sentinel ) ...
在这里,使用指针pi,和使用pi所指的对象,其差异在于哪个的生命已经结束了。虽然该
地址上的对象再也不合法,地址自己却仍然表明一个合法的程序空间。所以pi可以继续被使用,
但只能在受限制的状况下,很像一个void*指针的状况:
以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 ); } catch( ... ) { // 调用delete library function以释放new而配置内存 _delete( origin ); // 将原来的exception上传 throw; } }
在这里,若是new运算符配置object,而其constructor抛出一个exception,配置得来的内存
就会被释放掉。而后exception再被抛出去(上传)。
Destructor的应用极其相似。下面的式子:
delete origin;
会变成:
if( origin != 0 ) { // C++伪码 Point3d::~Point3d( origin ); _delete( origin ); }
若是在exception handling的状况下,destructor应该被放在一个try区段中。exception
handler会调用delete运算符,而后再一次抛出该exception。
通常的library对于new运算符的实现都很直截了当,可是两个精巧之处值得斟酌(如下版本
并未考虑exception handling):
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_hander()函
数。这正是为何每一次循环都调用_new_hanlder()之故。
new运算符实际上老是以标准的C malloc()完成,虽然并无规定必定得这么作不可。相同
状况,delete运算符也老是以标准的C free()完成:
extern void operator delete( void *ptr ) { if( ptr ) free( ( char* )ptr ); }
当咱们这么写:
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并无定义一个constructor或destructor,因此配
置数组以及清除p_aggr数组操做,只是单纯地得到内存和释放内存而已。这些操做由new和
delete运算符来完成绰绰有余了。
然而若是class定义了一个default constructor,某些版本的vec_new()就会被调用,配置并构
造class objects所组成的数组。例如这个算式:
Point3d *p_array = new Point3d[ 10 ];
一般会被编译为:
Point3d *p_array; p_array = vew_new( 0, sizeof( Point3d ), 10, &Point3d::Point3d, &Point3d::~Point3d );
在个别的数组元素构造过程当中,若是发生excpetion,destructor就会传递给vec_new()。只有
已经构造稳当的元素才须要destructor的施行,由于它们的内存已经被配置出来了,vec_new()
有责任在exception发生的时机把那些内存释放掉。
在C++2.0版本以前,将数组的真正大小提供给程序的delete运算符,是程序员的责任。所以
若是咱们原先写下:
int array_size = 10; Point3d *p_array = new Point3d[ array_size ];
那么咱们就必须对应地写下:
delete [ array_size ] p_array;
在2.1版中,这个语言有了一些函数,程序员再也不须要在delete时指定数组元素的个数,所以
咱们如今能够这样写:
然而为了回溯兼容,两种形式均可以接受。支持。支持此种新形式的第一个编译器固然就是
cfront。这项技术支持须要知道的首先是指针所指的内存空间,而后时其中的元素个数。
寻找数组维度,对于delete运算符的效率带来极大的冲击,因此才致使这样的妥协:只有在中
括号出现时,编译器才寻找数组的维度,不然它便假设只有单独一个objects要被删除。若是程
序员没有提供必须的中括号,像这样:
delete p_array;
那么就只有第一个元素会被析构。其余的元素仍然存在——虽然其相关的内存已经被要求归
还了。
各家编译器之间存在一个有趣的差别,那就是元素个数若是被显示指定,是否被拿去利用。
在Jonathan的原始版本中,优先采用使用者(程序员)显式指定的值。下面是他所写的原始码
的虚拟版本(pseudo-version),附带注释:
// 首先检查是狗最后一个被配置的项目(_cache_key) // 是目前要被delete的项目 // // 若是是,就不须要作搜寻操做了 // 若是不是,就寻找元素个数 int elem_count = _cache_key == pointer ? ( ( _cache_key = 0 ), _cache_cout ) : // 取出元素个数 // num_elem: 元素个数,将传递给vec_new()。 // 对于配置于heap中的数组,只有面对如下形式,才会设定一个值: // delete [10] ptr; // 不然cfront会传-1以表示取出。 if( num_elem == -1 ) // prefer explicit user size if choice! num_elem = ans;
然而几乎新近全部的C++编译器都不考虑程序员的显示指定(若是有的话)。
此一性质被导入的时候,没有任何程序代码会不“显示指定数组大小”。时代演化到
cfront4.0,咱们会把此习惯贴上“落伍”的标记,而且产生一个相似的警告信息。
应该如何记录元素个数?一个明显的方法就是为vec_new()所传回的每个内存区块配置
一个额外的word,而后把元素个数包藏在那个word之中。一般这种包藏的数值称为所谓的
cookie。然而,Jonathan和Sun编译器决定维护一个“联合数组(associative array)”,放置指
针及大小。Sun也把destructor的地址维护于数组之中。
cookie策略有一个广泛引发忧虑的话题就是,若是一个坏指针应该被交给delete_vec(),
取出来的cookie天然是不合法的。一个不合法的元素个数和一个坏的起始地址,会致使
destrcutor以非预期的次数被施行于一段非预期的区域。然而在“联合数组”的政策之下,坏指针
的可能结果就只是取出错误的元素个数而已。
在原始编译器中,有两个主要函数用来存储和取出所谓的cookie:
// array_key是新数组的地址 // mustn't either be 0 or already entered // elem_count is the count;it may be 0 typedef void *PV; extern int _insert_new_array( PV array_key, int elem_count ); // 从表格中取出(并去除)array_key // 若不是传回elem_count,就是传回-1 extern int _remove_old_array( PV array_key );
下面是cfront中的vec_new()原始内容通过修润后的一份呈现,并附加注释:
PV _vec_new( PV ptr_array, int elem_count, int size, PV construct ) { // 若是ptr_array是0,从heap之中配置数组。 // 若是ptr_array不是0,表示程序员写的是: // T array[ count ] // 或 // new ( ptr_array ) T[ 10 ]; int alloc = 0; // 咱们要在vec_new中配置吗? int array_sz = elem_count * size; if( alloc = ptr_array == 0 ) // 全局运算符 new ... ptr_array = PV( new char[ array_sz ] ); // 在exception handling之下: // 将抛出exception bad_alloc if( ptr_array == 0 ) return 0; // 把数组元素个数放到cache中 int status = _insert_new_array( ptr_array, elem_count ); if( status == -1 ) { // 在exception handling之下将抛出exception // 将抛出exception bad_alloc if( alloc ) delete ptr_array; return 0; } if( construct ) { register char* elem = ( char* )ptr_array; register char* lim = elem + array_sz; // PF是一个typedef,表明一个函数指针 register PF fp = PF( constructor ); while( elem < lim ) { // 经过fp调用constructor做用于 // ‘this’元素上(由elem指出) ( *fp )( ( void* )elem ); // 前进到下一个元素 elem += size; } } return pV( ptr_array ); }
vec_delete()操做差很少,但其行为不老是C++程序员所预期或需求的。例如,已知下面两
个处理class声明:
class Point { public: Point(); virtual ~Point(); // ... }; class Point3d : public Point { public: Point3d(); virtual ~Point3d(); // ... };
若是咱们配置一个数组,内含10个Point3d objects,咱们会预期Point和Point3d的
constructor被调用各10次,每次做用于数组的一个元素:
// 彻底不是个好主意 Point *ptr = new Point3d[ 10 ];
而当咱们delete“由ptr所指向10个Point3d元素”时,会发生什么事情?很明显,咱们须要虚
拟机制的帮助,以得到预期的Point destructor和Point3d destructor各10次的调用(每一次做用
于数组中的一个元素):
// 这并非咱们所须要的 // 只有Point::~Point被调用…… delete [] ptr;
施行于数组上数组上的destructor,如咱们所见,是根据交给vec_delete()函数的“被删除之
指针类型的destructor”——本例中正是Point destructor。这很明显并不是咱们所但愿。此外,每一
个元素的大小也一并被传递过去。这就是vec_delete()如何迭代走过每个数组元素的方式。本
例中被传递过去的是Point class object的大小而不是Point3d class object的大小。整个运做过程
很是不幸地失败了,不仅是由于执行了错误的destructor,并且自从第一个元素以后,该
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,所以,Point3d和Point的destructor都会施行于数组中的每个
objects身上。
有一个预先定义好的重载的(overload)new运算符,称为placement operator new。它需
要第二个参数,类型为void *。调用方式以下:
Point2w *ptw = new( arena ) Point2w;
其中arena指向内存中的一个区块,用以放置新产生出来的Point2w object。这个预先定义好
的placement operator new的实现方法简直是出乎意料的平凡。它只要将“得到的指针(上例
arena)”所指的地址传回便可:
void* operator new( size_t, void* p ) { return p; }
若是它的做用只是传回第二个参数,那么它有什么价值呢?也就是说,为何不简单地这么
写算了(这不就是实际所发生的操做吗):
Point2w *ptw = ( Point2w* ) arena;
事实上这只是所发生的操做的一半而已。另一半没法由程序员产生出来。以下问题:
1)什么是使placement new operator可以有效运行的另外一半扩充(并且是“arena的显式指定
操做(explicit assignment)”所没有提供的)?
2)什么是areana指针的真正类型?该类型暗示了什么?
Placement new operator所扩充的另外一半操做是将Point2w constructor自动实施于areana所
指的地址上:
// C++伪码 Point2w *ptw = ( Point2s* ) arena; if( ptw != 0 ) ptw->Point2w::Point2w();
这正是使placement operator new威力如此强大的缘由。这一份代码决定objects被放置在哪
里:编译系统保证object的constructor会施于其上。
然而却有一个轻微的不良行为。下面是一个有问题的程序片断:
// 让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
掉。不过在此例中若是你像下面这样作,绝对是个错误:
// 如下并非实施destructor的正确方法 delete p2w; p2w = new ( arena ) Point2w;
是的,delete运算符会发生做用,这的确是咱们所期待的。可是它会释放由p2w所指的内
存,它却不是咱们所但愿的,由于下一个指令就要用到p2w了。所以,咱们应该显式调用
destructor并保留存储空间以便再使用:
// 施行destructor的正确方法 p2w->~Point2w; p2w = new ( arena ) Point2w;
剩下的惟一问题是一个设计上的问题:在咱们的例子中对placement operator的第一次调
用,会将新object构造于原已存在的objecct之上吗?仍是会构造于全新地址上?也就是说,如
果咱们这样写:
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并不支持多态(polymorphsim)。被叫
给new的指针,应该适当地指向一块预先配置好的内存。若是derived class比其base class大,
例如:
Point2w *p2w = new ( arena ) Point3w;
Point3d的constructor将会致使严重的破坏。
Placement new operator被引入C++2.0时,最晦涩隐暗的问题是下面这个:
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()。
若是咱们有一个函数,形式以下:
T operator+( const T&, const T& );
以及两个T objects,a和b,那么
a + b;
可能会致使一个临时性对象,以放置传回的对象。是否会致使一个临时性对象,视编译器
的进取性(aggressiveness)以及上述操做发生时的程序语境(program context)而定。例如
下面这个片断:
T a, b; T c = a + b;
编译器会产生一个临时性放置a+b的结果,而后再使用T的copy constructor,把该临时性对
象当作c的初始值。然而比较更可能的转换是直接以拷贝构造的方式将a+b的值放到c中,因而就
不须要临时对象,以及对其constructor和destructor的调用了。
此外,视operator+()的定义而言,named return value(NRV)优化也可能被实施起来。这
将致使直接在上述c对象中求表达式结果,避免执行copy constructor和具名对象(named
object)的destructor。
三种方式所得到的c对象,结果都同样。其间的差别在于初始化的成本。一个编译器可能给
咱们任何保证吗?严格地说是没有。C++ Standard容许编译器对于临时性对象的产生有彻底有
彻底的自由度。
理论上,C++ Standard容许编译器厂商有彻底的自由度。但实际上,因为市场的竞争,几
乎保证任何表达式(expression)若是有这种形式:
T c = a + b;
而其中的加法运算符被定义:
T operator+( const T&, const T& );
或
T T::operator+( const T& );
那么实现时根本不产生一个临时性对象。
然而请注意,意义至关的assignment叙述句(statement):
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 cnstructed至临时对象中”,就是“以临时对象取代NRV”。在后者中,本来要施行于
NRV的constructor,如今将施行于此临时对象。
无论是哪种状况,直接传递c(上例赋值操做的目标对象)到运算符函数中都是有问题
的。因为运算符函数并不为其外加参数调用一个destructor(它指望一块“新鲜的”内存),因此
必须在此调用以前先调用destructor。然而,“转换”语意被用来将下面的assignment操做:
c = a + b; // c.operator=( a + b );
取代为其copy assignment运算符的隐式调用操做,以及一系列的destructor和copy
construction:
// C++伪码 c.T::~T(); c.T::T( a + b );
copy constructor、destructor以及copy assignment operator均可以由使用者供应,因此不
可以保证上述两个操做会致使相同的语意。所以以一连串的destruction和copy construction来取
代assignment通常而言是不安全的,并且会产生临时对象。因此这样的初始化操做:
T c = a + b;
老是比下面的操做更有效率地被编译器转换:
c = a + b;
第三种运算形式是,没有出现目标对象:
a + b; // no target
这时候有必要产生一个临时对象以放置运算后的结果。虽然看起来有点怪异,但这种状况
实际上在子表达式(subexpression)中十分广泛,例如,若是咱们这样写:
String s( "hello" ), t( "world" ), u( "!" );
那么不论:
String v; v = s + t + u;
或
printf( "%s\n", s + t );
最后一个表达式来一些密教式的论题,那就是“临时对象的生命期”。这个论题颇值得深刻
探讨。在Standard C++以前,临时对象的生命(也就是说他destructor什么时候实施)并无显式指
定,而是由编译厂商自行决定。换句话说,上述的printf(0并不保证安全,由于它的正确性和s+t
什么时候被摧毁有关。
(本例的一个可能性是,String class 定义了一个conversion运算符以下:
String::operator const char*() { return _str; }
其中_str是一个private member addressing storage,在String object构造时配置,在其
destructor中被释放。)
所以若是临时对象在调用printf()以前就被摧毁了,经由conbertion运算符交给它的地址就不
合法。真正的结果视底部的delete运算符在释放内存时的进取性而定。某些编译器可能会把这块
内存标示为free,不以任何方式改变其内容。在这块内存被其余地方宣称“主权”以前,只要它还
没有被deleted掉,它就能够被使用。虽然对于软件工程而言这不足以做为模范,但像这样,在
内存被释放以后又再被使用,并不是罕见。事实上malloc()的许多编译器会提供一个特殊的调用操
做:
malloc( 0 );
它正是用来保证上述行为的。
例如,下面是对于该算式的一个可能的pre-Standard转化。虽然在pre-Standard语言定义中
是合法的,却可能形成重大灾难:
// C++伪码:pre-Standard的合法转换 // 临时性对象被摧毁得太快了 String temp1 = operator+( s, t ); const char *temp2 = temp1.operator const char*(); // 合法可是有欠考虑,太太轻率 temp1.~String(); // 这时候并未定义temp2指向何方 printf( "%s\n", temp2 );
另外一种转换方式是在调用printf()以后实施String destuctor。在C++ Standard之下,这正是
该表达式的标准转换方式。标准规格上这么说:
临时性对象的被摧毁,应该是对完整表达式(full-expression)求值过程当中的最后一个步
骤。该完整表达式形成临时对象的产生。
什么是一个完整表达式(full-expression)?非正式地说,它是被涵括的表达式中最外围的
那个。下面这个式子:
// tertiary full expression with 5 sub-expressions ( ( objA > 1024 ) && ( objB > 1024 ) ) ? objA + objB : foo( objA, objB );
一共有五个子算式(subexpressions),内含在一个“?:完整表达式”中。任何一个子表达
式所产生的任何一个临时对象,都应该在完整表达式被求值完成后,才能毁去。
当临时性对象是根据程序的执行期语意,有条件地被产生出来时,临时性对象的生命规则就
显得有些复杂了。举个例子,像这样的表达式:
if( s + t || u + v )
其中的u+v子算式只有在s+t被评估为false时,才会开始被评估。与第二个子算式有关的临
时对象必须被摧毁,可是,不能够被无条件地摧毁。也就是说,咱们但愿只有在临时性对象必
须被摧毁,可是,很明显,不能够被无条件地摧毁。也就是说,咱们但愿只有在临时性对象被
产生出来的状况下,才去摧毁它。
在讨论临时对象的生命规则以前,标准编译器将临时对象的构造和析构附着于第二个子算
式的评估程序。例如,对于如下的class声明:
class X { public: X(); ~X(); operator int(); X foo(); private: int val; };
以及对于class X的两个objects的条件测试:
main() { X xx; Y yy; if( xx.foo() || yy.foo() ) ; return 0; }
cfront对于main()产生出如下的转换结果:
int main( void ) { struct X _1xx; struct X _1yy; int _0_result; // name_mangled default constructor; // X:X( X *this ) _ct_1xFv( &_1xx ); _ct_1xFv( &_1yy ); { // 被产生出来的临时对象 struct X _0_Q1; struct X _0_Q2; int _0_Q3; /* 每一端变成一个附逗点的表达式, * 有着下列顺序: * * tempQ1 = xx.foo(); * tempQ3 = tempQ1.operator int(); * tempQ1.X::~X(); * tempQ3; */ // _opi_1xFv ==> X::operator int() if (((( _0_Q3 = _opi_1xFv((( _0_Q2 = foo_1xFv( &_1xx ) ), ( &_0_Q1 )))), _dt_1xFv( &_0_Q1, 2 )), _0_Q3 ) || ((( _0_Q3 = _opi_1xFv((( _0_Q2 = foo_1xFv( &_1yy ) ), ( &_0_Q2 )))), _dt_1xFv( &_0_Q2, 2 )), _0_Q3 )) { _0_result = 0; _dt_1xFv( &_1yy, 2 ); _dt_1xFv( &_1xx, 2 ); } return _0_result; } }
把临时性对象的destructor放在每个子算式的求值过程当中,能够免除“努力追踪第二个子算
式是否真的须要被评估”。然而在C++ Standard的临时对象生命规则中,这样的策略再也不被允
许。临时性对象在完整表达式还没有评估彻底以前,不得被摧毁。也就是说某些形式的条件测试
如今必须被安插进来,以决定是否要摧毁和第二算式有关的临时对象。
临时性对象的生命规则有两个例外。第一个例外发生在表达式被用来初始化一个object时,
例如:
bool verbose; ... string progNameVersion = !verbose ? 0 : progName + progVersion;
其中progName和progVersion都是String objects。这时候会生出一个临时对象,放置加法
运算符的运算结果:
String operator+( const String&, const String& );
临时对象必须根据对verbose的测试结果,有条件地析构。在临时对象的生命规则之下,它
应该在完整的”?:表达式“结束评估以后尽快被摧毁。然而,若是progNameVersion的初始化需
要调用一个copy constructor:
// C++伪码 proNameVersion.String::String( temp );
那么临时性对象的析构(在”?:完整表达式“以后)固然就不是咱们所指望的。C++
Standard要求说:
......凡持有表达式执行结果的临时性对象,应该存留到object的初始化操做完成为止。
甚至即便每个人都坚守C++ Standard中的临时对象生命规则,程序员仍是可能对象在他
们的控制中被摧毁。其间的主要差别在于这时候的行为有明确的定义。例如,在新的临时对象
生命规则中,下面这个初始化操做保证失败:
// 不是个好主意 const char *progNameVersion = progName + proVersion;
其中progName和progVersion都是String objects。产生出来的程序代码看起来像这样:
// C++ pseudp Code String temp; operator+( temp, progName, progVersion ); progNameVersion = temp.String::operator char*(); temp.String::~String();
此刻progNameVersion指向未定义的heap内存!
临时性对象的生命规则的第二个例外是”当一个临时性对象被一个reference绑定“时,例如:
const String &space = " ";
产生出像这样的程序代码:
// C++ pseudo Code String temp; temp.String::String( " " ); const String &space = temp;
很明显,若是临时性对象如今被摧毁,那么reference也就差很少没什么用了。因此规则上
说:
若是一个临时性对象被绑定于一个reference,对象将残留,直到被初始化之reference的生命
结束,或直到临时对象的生命范畴(scope)结束——视哪种状况先到达而定。
临时对象的优化,反聚合(disaggregation)的优化。