6.2 new 和 delete 运算符
运算符 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继续当做一个指针来用,仍然是可以的(虽然其使用受到限制),例如:
// pi仍然指向合法空间 // 甚至即使储存于其中的object已经不再合法 if (pi == sentine1) ...
在这里,使用指针pi和使用pi所指的对象,其差别在于哪一个的声明已经结束了.虽然该地址上的对象不再合法,但地址本身却仍然代表一个合法的程序空间.因此pi能够继续被使用,但只能在受限制的情况下,很像一个 void * 指针的情况.
以constructor来配置一个 class object,情况类似,例如:
Point3d *origin = new Point3d;
被转换为:
Point3d *origin; if (origin = __new(sizeof(Point3d))) origin = Point3d::Point3d(origin);
如果exception handling的情况下,destructor应该被放在一个try区段中.exception handler会调用 delete 运算符,然后再一次丢出该exception.
一般的lirary对于 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并没有定义一个constructor或destructor,所以配置数组以及清除p_aggr数组的操作,只是单纯地获得内存和释放内存而已.这些操作由 new 和 delete 运算符来完成就绰绰有余了.
然而如果 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);
在个别的数组元素构造过程中,如果发生exception,destructor就胡被传递给vec_new().只有已经构造妥当的元素才需要destructor的施行,因为它们的内存已经被配置出来了,vec_new()有责任在exception发生的时候把那些内存释放掉.
在 delete 时不需要指定数组元素的数目,如下所示:
delete []p_array;
寻找数组维度给 delete 运算符的效率带来极大的影响,所以才导致这样的妥协:只有在中括号出现时,编译器才寻找数组的维度,否则它便假设只有单独一个object要被删除.如果程序员没有提供必须的中括号,像这样:
delete p_array;
那么就只有第一个元素会被解构,其他元素仍然存在.
应该如何记录元素数目?一个明显的方法就是为vec_new()所传回的每一个内存区块配置一个额外的word,然后把元素数目包藏在那个word中.通常这种被包藏的数值称为所谓的cookie.然而Sun编译器却维护一个"联合数组",放置指针以及大小.它也把destructor的地址维护于此数组中.
cookie策略有一个普遍引起忧虑的话题,那就是如果一个坏指针被交给delete_vec(),取出来的cookie自然是不合法的.一个不合法的元素数目和一个坏的起始地址,会导致destructor以非预期的次数被施行于一段非预期的区域,然而在"联合数组"的政策下,坏指针的可能结果就只是取出错误的元素数目而已.
版权声明:本文为博主原创文章,未经博主允许不得转载。