执行期语义学——new与delete运算符

前言

 

new operator看起来似乎是一个单一运算,例如:

1
int *pi = new int(5);

但实际上它由以下两个步骤组成:

  1. 通过适当的new运算符函数实体,配置所有所需内存:
    1
    int *pi = _new(sizeof(int));
  2. 给配置得来的对象设立初值:
    *pi = 5;

当然,初始化操作应当在内存配置成功后才执行:

1
2
3
//重写声明
int *pi;
if(pi = _new(sizeof(int))) *pi=5;

delete运算符性质类似,只是在使用前会判断一下参数是否为nullptr,并且需要注意的是,在执行完delete后,p并不会被置为nullptr。

如果我们new或delete的是一个具备constructor或destructor的object,那么在执行new或delete时会触发对应的函数调用(其实一直说到这都是描述new operator与operator new)。


new运算符的小机巧

 
new操作符的实现一般均很直截了当,但有两个精巧之处值得斟酌:

1
2
3
4
5
6
7
8
9
10
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 nullptr;
}
return last_alloc;
}

虽然new T[0]从语法层胶墩来说合法,但语言要求每一次对new的调用都必须传回一个独一无二的指针,这也就是每一次当size为0都将其置1的原因。此外,new允许开发者提供自己的_new_handler()函数,这也就是循环调用的原因。

实际上,new运算符总是以标准的C malloc()完成,虽然这并不是强制要求。类似地,delete也总是以标准的C free()完成:

1
2
3
4
extern void 
operator delete(void *ptr){
if(ptr) free((char*)ptr);
}


针对数组的new与delete语意

 
当我们写下

1
int* p_array = new int[5];

时,vec_new并不会真正被调用,因为其主要功能是把default constructor施行于class objects所组成的数组的每一个元素身上,倒是new运算符函数会被调用:
1
int* p_array = (int*)_new(5*sizeof(int));

对于strcut这种Plain OI’s Data,其执行类似于内置类型,因为其不具备constructor与destructor。new与delete运算符只需要获取或释放内存即可,vec_new()也不会被调用。

但如果class定义有一个default constructor,那么某些版本的vec_new()将会被调用,举例而言:

1
2
3
4
Point3d *p_array = new Point3d[10];
//编译结果
Point3d* p_array;
p_array = vec_new(0,sizeof(Point3d),10,&Point3d::Point3d(),&Point3d::~Point3d());

需要注意的是,只有已经构造完毕的object才可以经过destructor处理,在发生异常时,vec_new释放所有资源。

在很久之前,delete对象数组时必须要指定数组长度delete[10] p_array,当然现在不需要了,不过这种写法依然有效。有人会产生疑问,delete[]是如何了解当前所需要析构的元素数目?答案是vec_new所传回的内存区块内有一个word记录了数组长度。另外需要记住的是,在数组中使用多态性是非常不恰当的行为,我们在任何情况下都应当避免当一个base class 指针或引用指向derived class object组成的数组。


Placement Operator new的语意

 
new操作符可以重载,其最常见的一种形态我们称为placement operator new,它需要第二个参数,用来指定分配内存的位置:

1
2
3
void* operator new(size_t,void* p){
return p;
}

有人认为如果其作用只是传回第二参数,那它的实际意义何在?事实上,placement operator new还负责调用constructor自动实施于所指地址:
1
2
Point2w *ptw = (Point2w*) arena;
if(ptw!=0) ptw->Point2w::Point2w();

然而存在一些小问题:假若当前我们需要被构造的内存段已经存在了一个object,而这个object拥有一个destructor。在我们执行placement operator new时,该destructor并不会被调用,可能会有人想到使用delete从而触发destructor,然而这也释放掉了这一块内存,并不可,。较好的做法是直接在地址上执行destructor(在后来的C++ standrad中出现了placement perator delete,它可以析构且不释放内存)。