对于:
Widget *pw = new Widget ;
共有两个函数被调用:一个是用以分配的operator new,一个是Widget的默认构造函数
假设第一个函数调用成功,第二个函数抛出异常-----第一步的内存分配所得的内存必须归还,否则内存泄漏。此时,客户没有能力归还内存,因为如果Widget构造函数抛出异常,pw尚未被赋值,客户手上也没有指针指向应该被归还的内存。取消第一步并恢复到原来的责任因此落在C++运行期系统上。
运行期系统会调用步骤一所调用的operator new的响应的operator delete版本,如果目前面对的是拥有正常签名式(signature)的new和delete,这并不是问题,因为正常的operator new:
void * operator new(std::size_t) throw(std::bad_alloc);
对应正常的operator delete:
void operator delete(void * ramMemory) throw(); //global作用域中的正常签名式
void operator delete(void * ramMemory, std::size_t size) throw(); //class作用域中的正常签名式
然而,当你声明非正常形式的operator new,也就是带有附加参数的operator new,那么找到对应的delete可能有点困难。
举个例子,假设你写了一个class专属的operator new,要求接受一个ostream,用来log相关分配信息,同时右写了一个正常形式的class专属operator delete:
// 这个设计有问题
class Widget{
public:
static void * operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc); // 非正常适应的delete
static void operator delete(void *pMemory, std::size_t) throw(); //正常的class专属delete
};
如果operator new接受的参数除了一定会有的那个size_t之外还有其他,这就是所谓的placement new。因此,上面的operator new是个placement版本。众多placement new版本中特别有用的一个是“接受一个指针指向对象该被构造之处”,长相如下:
void * operator new(std::size_t, void *pMemory) throw();
这个版本的new
已经被纳入C++标准库,你只要#include<new>
即可。这个new的用途之一是负责在vector的未使用空间上创建对象。它同时也是最早版本的placement new。实际上它是这个函数的命名由来:一个特定位置的new。当人们谈到placement new,就是谈这个版本,也就是“唯一额外参数是个void*”,少数情况下才是接受任意实参的operator new。
现在我们回到原先那个设计有问题的Widget class。这里的问题是:这个class将会引起微妙的内存泄漏。考虑如下代码,它在动态创建一个widget时将相关的log记在cerr:
Widget *pw = new(std::cerr) Widget; //调用operator new并传递cerr为ostream实参:这个动作会在widget构造函数抛出异常时泄漏内存。
但是,如果内存分配成功,widget构造函数抛出异常,运行期系统将寻找“参数类型与个数都与operator new相同的”operator delete。也就是说:
static void * operator delete(std::size_t size, std::ostream& logStream) throw()
但是找不到,所以它什么也不做,从而造成内存泄漏
如果内存分配成功,widget构造函数没有抛出异常,当客户调用:
delete pw;
调用的是正常使用的operator delete,而不是placement delete。placement delete只在“伴随placement new调用而触发的构造函数”出现异常时才会被调用。
也就是说,要解决placement new的内存泄漏,必须同时提供一个正常的operator delete(用于构造期间无任何异常时抛出)和一个placement delete(用于构造期间有异常抛出)。
另外,由于成员函数的名称会掩盖作用域中的相同名称,如果你在基类中声明唯一一个placement operator new,客户讲无法使用正常形式的new:
class Base{
public:
static void * operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc);
};
Base *pb = new Base; // errror,因为正常形式的operator new被掩盖
Base *pb = new(std::cerr)Base; //ok,调用Base的placement new
同样地,当你继承时:
class Derived: public Base{
public:
static void* operator new(std::size_t size) throw(std::bad_alloc);
};
Derived *p = new (std::clog) Derived; // Error!
Derived *p = new Derived; // OK
这是因为子类中的”normal new”隐藏了父类中的”placement new”,虽然它们的函数签名不同。
最佳实践
缺省情况下C++在全局作用域
void* operator new(std::size_t) throw(std::bad_alloc); // normal new
void* operator new(std::size_t, void*) throw(); // placement new
void* operator new(std::size_t, const std::nothrow_t&) throw(); // 见 Item 49
如果你在类内声明了任何operator new,它会遮掩上面这些标准形式。除非你的意思就是要阻止class的客户使用这些形式,否则请确保它们在你所生成的任何定制new之外还可用,对于每一个operator new也请确定提供对应的operator delete
class StandardNewDeleteForms {
public:
// normal new/delete
static void* operator new(std::size_t size) throw(std::bad_alloc) { return ::operator new(size); }
static void operator delete(void *pMemory) throw() { ::operator delete(pMemory); }
// placement new/delete
static void* operator new(std::size_t size, void *ptr) throw() { return ::operator new(size, ptr); }
static void operator delete(void *pMemory, void *ptr) throw() { return ::operator delete(pMemory, ptr); }
// nothrow new/delete
static void* operator new(std::size_t size, const std::nothrow_t& nt) throw() { return ::operator new(size, nt); }
static void operator delete(void *pMemory, const std::nothrow_t&) throw() { ::operator delete(pMemory); }
};
凡是想以自定形式扩充标准的客户,可以利用继承机制以及using声明式取得标准形式:
class Widget: public StandardNewDeleteForms { // inherit std forms
public:
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete; // 让这些形式可见
static void* operator new(std::size_t size, std::ostream& log) throw(std::bad_alloc); // 自定义 placement new
static void operator delete(void *pMemory, std::ostream& logStream) throw(); // 对应的 placement delete
};