运算符new的使用,事实上经过两个步骤完成:
int* pi = new int(5);
//1.通过new运算符,配置所需要的内存
int* pi =_new(sizeof(int));
//2.将配置的对象设立初值
*pi = 5;
更进一步说,初始化操作应该再内存成功(经由new运算符)后才执行:
//new运算符的两个分离步骤
//int* pi = new int(5);
//重写声明
int* pi;
if(pi = _new(sizeof(int)))
*pi = 5;
//delete的分离步走
//delete pi;
//重写声明
if(pi != 0)
_delete(pi);
如果pi的值时0,C++语言会要求delete运算符不会执行,pi并不会自动清0,像下面这样的后继行为,虽然没有良好定义,但是可能(也可能不)被评估为真,这是因为对于pi所指之内存的变更或再使用,可能(也可能不)会发生。
if(pi && pi == 5) ...
pi所指对象的生命会因delete而结束,所以后继任何对pi的参考操作就不再包良好的行为,并因此被视为一种不好的程序风格。然而,把pi继续当一个指针来用,仍然可以的(虽然使用受到限制),例如:
//ok,pi仍然指向合法的空间
//甚至几十存储其中的object不再合法
if(pi == sentinel) ...
在这里,使用指针pi,和使用pi所指的对象,其差别在于哪一个生命已经结束了。虽然该地址的对象不再合法,地址本身却仍然代表一个合法的程序空间,因此pi能够继续使用,但只能在受限制的情况下使用,很想一个void*指针的情况。
以constructor来配置一个class object情况类似:
//被转化为
Point3d origin;
//C++ pseudo code
if(origin = _new(sizeof(Point3d)))
origin = Point3d::Point3d(origin);
如果实现出exception handling,那么转换结果可能复杂些:
//C++ pseudo code
if(origin = _new(sizeof(Point3d)))
{
try{
origin = Point3d::Point3d(origin);
}
catch(...){
//调用delete library funct释放内存
_delete(origin);
//将原来的exception上传
throw;
}
}
destructor应用类似:
delete origin;
//被内部转化
if(origin != 0)
{
Point3d::~Point3d(origin);
_delete(origin);
}
以下是library中new的实现方法(以下版本并没考虑exception handing):
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的调用都必须传回一个独一无二的指针,解决此问题的方法是传统的传回一个指针,指向一个默认为1byte的内存空间,另一个有趣之处是,它允许使用者提供一个属于自的_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数组的操作,只是单纯地获取内存和释放内存而已。
然而如果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。
对于写下如下的代码,析构函数对应的写法:
int array_size = 10;
Point3d* p_array = new Point3d[10];
//C++2.0之前必须这么写
delete [array_size] p_array;
//C++2.0之后可以写么写
delete [] p_array;
为了回溯兼容,两种形式都可以接受。
对于如下的destructor操作而言:
delete p_array;
那么只有第一个元素被析构,其他元素仍然存在——虽然其相关的内存已经归还了。
Placement Operator new的语意
有一个预先定义好的重载的(overloaded)new运算符,成为placement operator new。它需要第二个参数,类型为void*,调用方式如下:
Poin2w* ptw = new(arena)Poin2w;
其中arena指向内存中的一块区域,用以放置新产生出来的Point2w object。它的实现方法只要将“获得的指针(arena)”所指的地址传回即可:
void* operator new(size_t,void* p)
{
return p;
}
Placement new operator所扩充的另一半是将Point2w constructor自动实施于arena所指的地址上:
//C++ pseudo code
Poin2w* ptw = (Poin2w*)arena;
if(ptw != 0)
ptw->Point2w::Point2w();
然而却有一个轻微不良行为:
//让arena成为全局性定义
vod 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;
//施行destructor的正确方法
p2w->~Point2w;
p2w = new(arena)Point2w;
剩下的问题有一个是设计的问题:在例子中第placement operator第一次调用,会将新object构造于原已存在的object至上,还是会析构于全新地址上?我们如何知道arena所指的这块区域是否需要先析构?这个问题在语言层面没有解答,一个合理的习惯是指向new这一段也要负责执行destructor。
另一个问题关系到arena所表现的类型,C++ Standard说它必须指向相同类型的class,要不就是一块“新鲜”内存,足够容纳该类型的object。但是,derived class并不被支持。对于一个derived class,或是其他没有关联的类,其行为虽然并非不合法,却是未定义的。
新的内存空间可以这样配置:
char* arena = new char[sizeof(Point2w)];
相同类型的object可以这样获得:
Poin2w* arena = new Point2w;
不论哪种情况,新的Point2w的存储空间的确覆盖了arena的位置,此行为良好。然而,placement new operator并不支持多态。对于derived class比其base class大的话,derived class 的constructor会被破坏。
还有一个问题是:
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;
b.f(); //哪一个f()被调用
}
由于上述两个classes有相同的大小,把derived object放在base class而配置的内存中是安全的,然而,要支持这一点,就必须放弃动态的virtual function。尽管大部分使用者以为调用时Derived的,但大部分编译器调用却是Base。