6.2 new 和 delete 运算符
运算符 new 的使用,看起来似乎是一个单一运算:
int *pi=new int(5);
事实上它由两个步骤完成:
1、配置所需的内存:
int *pi=__new(sizeof(int)); // 调用函数库中的new 运算符
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 = nullptr; // 最好加上,因为pi值不一定为空,随机的
}
pi所指对象的生命会因delete而结束,所以后继任何对pi的参考操作就不能再保证有良好的行为,并因此会被视为是一种不好的程序风格。然而,把pi继续当做一个指针来使用,仍然是可以的。
// 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); }
如果出现excepting handling(异常处理),那么转换结果会更加复杂:
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 )
{
Point3d::~Point3d(origin); // 析构对象,再释放内存
__delete(origin); // 最好加上 origin = nullptr;
}
一般 library 对于 new 运算符的实现操作都很直接,但有两个精巧之处:
extern void* operator new(size_t size)
{
if( size == 0 ){ size=1; } ·// new T[0] 也是对的
void *last_alloc;
while( !(last_alloc = malloc(size)) )
{
if(__new_handler){ (*__new_handler)(); }
else{ return 0; }
}
return last_alloc;
}
1、语言要求每一次对new的调用都必须传回一个独一无二的指针,解决该方法是传回一个指针,指向一个默认为1-byte的内存区块。2、另一个有趣之处是,它允许使用者提供一个属于自己的_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 object所组成的数组的每一个元素身上。倒是 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所组成的数组。 // bzf: 感觉要认为提供
Point3d *p_array=new Point3d[10];
通常会被编译为:
Point3d *p_array;
p_array = vec_new( 0, sizeof(Point3d), 10, &Point3d::Point3d, &Point3d::~Point3d );
只有已经构造妥当的元素才需要destructor的施行,因为他们的内存已经被配置出来了,vec_new()有责任在exception发生的时候把那些内存释放掉。
当我们写下:
int array_size=10;
Point3d *p_array = new Point3d[array_size];
那么当需要删除数组时,可以这样写:
delete [array_size] p_array;
只有在中括号出现时,编译器才寻找数组的维度,否则它便假设只有单独一个objects要被删除,如果没有提供中括号:
delete p_array;
那么只有第一个元素会被解构,其他的元素仍然存在。
新的版本中: 不用指出数组长度了,可以省略:
delete [] p_array;
那么编译器如何记录元素数目呢?一个明显的方法就是为 vec_new() 所传回的每一个内存区块配置一个额外的 word,记录元素数目。这种被记录的数值被称为所谓的cookie。cookie策略有一个普遍引起忧虑的就是,如果一个坏指针应该被交给 delete_vec(),因为取出来的cookie是不合法的。一个不合法的元素数目和一个错误的起始地址,会导致 destructor 以非预期的次数被施行于一段非预期的区域。
如果我们配置一个数组,内带有10个 Point3d objects,我们会预期Point和Point3d的constructor被调用各10次,每次作用于数组的一个元素:
//完全不是一个好主意
Point *ptr = new Point3d[10];
而当 delete “由ptr所指向的10个Point3d元素” 时,很明显需要虚拟机制的帮助,以获得预期的 Point destructor 和 Point3d destructor各十次调用,每一次作用于数组中的每一个元素:
// 只有Point::~Point被调用。。。
delete [ ] ptr;
施行于数组上的 destructor,是根据交给vec_delete()函数之“被删除的指针类型的destructor”--本例中是 Point destructor,这很明显并非我们希望的。此外,每一个元素的大小也一并被传递过去,这就是vec_delete()如何迭代走过每一个数组元素的方式。本例中被传递过去的是Point class object的大小而不是Point3d class object的大小。
应该避免以一个base class 指针指向一个derived class objects所组成的数组--如果derived class object 比其 base 大的话,那么就只能程序员手动来处理:
for (int i = 0; i < elem_count; ++i)
{
Point3d *p = &((Point3d*)ptr)[i];
delete p;
}
程序员必须迭代走过整个数组,把delete运算符实施于每一个元素身上,调用操作将是virtual ,所以,Point3d 和 Point 的 destructor 都会施行于数组中的每一个 objects 身上。
placement operator new 的语义
有一个预先定义好的重载的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;
}
如果只是传回其第二个参数,那么它的价值?
1、什么是placement new operator能够有效运行的另一半部扩充(而且是“arena的显式指定操作(explicit assignment)”所没有提供的)?
2、什么是arena指针的真正类型?该类型暗示了什么?
placement new operator 所扩充的另一半边是将Point2w constructor自动施于arena所指的地址上:
Point2w *ptw = (Point2w*) arena; // 让对象指针指向一个地址
if( ptw!=0 ){ ptw->Point2w::Point2w(); } // 在此地址上创建对象
这正是使placement operator new威力如此强大的原因,这一份码决定objects被放置在哪里,编译系统保证object的constructor会施行于其上。
却有一个轻微的不良的行为:
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会调用析构函数)
delete p2w; // 析构函数会被调用,相应的内存也会被释放掉,
p2w = new( arena ) Point2w; // 此时p2w是不确定的!
但问题来了,这样回将这块空间回收,那么接下来无法继续使用该空间继续去new。正确的方法是显式调用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的位置,而此行为已在良好控制之下。被交给new的指针,应该适当地指向一块预先配置好的内存。placement new operator 并不支持多态。如果派生类比 base class 大,该内存会被严重的破坏:
Point2w *p2w = new (arena) Point3w;

浙公网安备 33010602011771号