对于Widget*pw = new Widget;共有两个函数被调用;一个是用以分配内存的operatornew,一个是Widget的默认构造函数。
假设第一个函数调用成功,第二个函数却抛出异常。步骤一的内存分配所得必须取消并恢复旧观,否则会造成内存泄露。在这个时候,客户没有能力归还内存,因为如果Widget构造函数抛出异常,pw尚未被赋值,客户手上也就没有指针指向被归还的内存。取消步骤一并恢复旧观的责任因此落到C++运行期系统身上。
运行期系统会高高兴兴地调用步骤一所调用的operator new的相应的operatordelete版本,前提是它必须知道哪一个(因为可能有许多个)operatordelete该被调用。如果目前面对的是拥有正常签名式的new和delete,这并不是问题,因为正常的operator new:
void* operator new(size_t)throw(bad_alloc);
对应于正常的operator delete:
void operator delete(void*rawMemory)throw(); //全局作用域中的正常签名式
void operator delete(void* rawMemory,size_tsize)throw();//类作用域中典型的签名式
因此,当你只使用正常形式的new和delete,运行期系统毫无问题可以找出那个“知道如何取消new所作所为并恢复旧观”的delete。然而当你开始声明非正常形式的operatornew,也就是带有附加参数的operatornew,“究竟哪一个delete伴随这个new”的问题便浮现了。
例如:假设你写了一个class专属的operatornew,要求接受一个ostream,用来记录相关的分配信息,同时又写了一个正常形式的类专属operator delete:
class Widget
{
public:
...
static void* operatornew(size_t size,ostream&logStream)throw(bad_alloc);//非正常形式的new
static void operatordelete(void* rawMemory,size_t size)throw(); //正常的类专属delete
...
};
这个设计有问题,但是让我们先讨论一些术语。
如果operatornew接受的参数除了一定会有的那个size_t之外还有其他,这便是个所谓的placement new。因此,上述的operatornew是个placement版本。众多placementnew版本中特别有用的一个是“接受一个指针指向对象该被构造之处”,那样的operator new长相如下:
void* operator new(size_t size,void*pMemory)throw(); //placementnew
这个版本的new已被纳入C++标准程序库,你只要#include<new>就可以取用它。这个new的用途之一是负责在vector的未使用空间上创建对象。它同时也是最早的placement版本。实际上它正是这个函数的命名根据:一个特定位置上的new。以上说明意味术语placementnew有多重定义。当人们谈到placementnew,大多数时候他们谈的是此一特定版本,也就是“唯一额外实参是个void*”,少数时候才是指接受任意额外实参之operatornew。上下文语境也能够使意义不明确的含糊话语清晰起来,但了解这一点相当重要:一般术语“placementnew”意味带任意参数的new。因为另一个术语“placement delete”直接派生自它。
现在让我们回到Widget类的声明式,也就是先前说设计有问题的那个。这里的技术困难在于,那个类将引起微妙的内存泄露。考虑以下客户代码,它在动态创建一个Widget时将相关的分配信息记录于cerr:
Widget* pw = new(std::cerr)Widget;//调用operatornew并传递cerr为其ostream实参:这个动作会在Widget
//构造函数抛出异常时泄露内存
再强调一次,如果内存分配成功,而Widget构造函数抛出异常,运行期系统有责任取消operatornew的分配并恢复旧观。然而运行期系统无法知道真正被调用的那个operatornew如何运作,因此它无法取消分配并恢复旧观,所以上述做法行不通。取而代之的是,运行期系统寻找“参数个数和类型都与operatornew相同”的某个operator delete。如果找到,那就是它的调用对象。既然这里的operatornew接受类型为ostream&的额外实参,所以对应的operator delete应该是:
void operatordelete(void*,std::ostream&)throw();
类似于new的placement 版本,operator delete如果接受额外参数,便称为placementdelete。现在,既然Widget没有申明placement 版本的operatordelete,所以运行期系统不知道如何取消并恢复原先对placementnew的调用。于是什么也不做。本例之中如果Widget构造函数抛出异常,不会有任何operator delete被调用。
规则很简单:如果一个带额外参数的operatornew没有“带相同额外参数”的对应版operatordelete,那么当new的内存分配动作需要取消并恢复旧观时就没有任何operatordelete会被调用。因此,为了消除早先代码中的内存泄露,Widget有必要申明一个placementdelete,对应于那个有记录功能的placement new:
class Widget
{
public:
...
staticvoid* operator new(size_t size,ostream&logStream)throw(bad_alloc);
staticviod operator delete(void* pMemory)throw();
staticvoid operator delete(void* pMemory,ostream&logStream)throw();
...
};
这样改变之后,如果以下语句引发Widget构造函数抛出异常:
Widget* pw = new(std::cerr)Widget; //一如既往,但这次不再泄露
对应的placement delete会被自动调用,让Widget有机会确保不泄露任何内存。
然而如果没有抛出任何异常(通常如此),而客户代码中有个对应的delete,会发生什么事:
deletepw; //调用正常的operator delete
正如注释所言,调用的是正常形式的operator delete,而非其placement版本。placement delete只有在“伴随placementnew调用而触发的构造函数”出现异常才会被调用。对着一个指针(例如上述的pw)施行delete绝不会导致调用placementdelete,绝对不会!!!
这意味如果要对所有与placementnew相关的内存泄露宣战,我们必须同时提供一个正常的placementdelete(用于构造期间无任何异常被抛出)和一个placement版本(用于构造期间有异常被抛出)。后者的额外参数必须和operator new一样。
附带一提,由于成员函数的名称会遮掩其外围作用域中的相同名称,你必须小心避免让类专属版本的new遮掩客户期望的其他new(包括正常版本)。假设你有一个基类,其中唯一一个placementoperator new,客户端会发现他们无法使用正常形式的new:
class Base
{
public:
...
static void* operator new(size_t size,ostream&logStream)
throw(bad_alloc);//这个new会遮掩正常的全局形式operator new
...
};
Base* pb = newBase; //错误,因为正常形式的operator new被遮掩
Base* pb =new(std::cerr)Base; //正确,调用Base的placementnew
同样道理,派生类中的operator new会遮掩全局版本和继承而得的operator new版本:
class Drived:public Base //继承先前的Base
{
public:
...
static void* operator new(size_tsize)throw(bad_alloc);//重新申明正常形式的new
...
};
Drived* pd = new(std::clog)Derived;//错误!因为Base的placementnew被遮掩了
Drived* pd = newDrived; //没问题,调用Drived的operator new
对于撰写内存分配函数,你需要记住的是,缺省情况下C++在全局作用域内提供以下形式的operatornew:
void* operator new(size_t)throw(bad_alloc);//正常的new
void* operatornew(size_t,void*)throw();//placement new
void* operator new(size_t,conststd::nothrow_t&)throw();
如果你在类内声明任何operatornew,它会遮掩上述这些标准形式。除非你的意思就是要阻止类的客户使用这些形式,否则请确保它们在你所生成的任何定制型operatornew之外还可用。对于每一个可用的operator new也请确定提供对应的operatordelete。如果你希望这些函数有着平常的行为,只要令你的类专属版本调用全局版本即可。
完成以上所言的一个简单做法是,建立一个基类,内含所有正常形式的new和delete。
class StandardNewDeleteForms
{
public:
//normal new/delete
static void* operatornew(size_t size)throw(bad_alloc)
{return ::operatornew(size);}
static void operatordelete(void* pMemory)throw()
{::operatordelete(pMemory);}
//placement new/delete
static void* operatornew(size_t size,void* ptr)throw()
{return ::operatornew(size,ptr);}
static void operatordelete(void* pMemory,void* ptr)throw()
{::operatordelete(pMemory,ptr);}
//nothrow new/delete
static void* operatornew(size_t size,const std::nothrow_t&nt)throw()
{return ::operatornew(size,nt);}
static void operatordelete(void* pMemory,conststd::nothrow_t&)throw()
{::operatordelete(pMemory);}//少一个参数
};
凡是想以自定义形式扩充标准形式的客户,可利用继承机制及using声明式取得标准形式:
class Widget:public StandardNewDeleteForms //继承标准形式
{
public:
using StandardNewDeleteForms::opertaornew //让这些形式可见
using StandardNewDeleteForms::operator delete;
static void* operator new(size_t size,ostream&logStream)
throw(bad_alloc);//添加一个自定的placement new
static void* operator delete(void* pMemory,ostream&logStream)
throw();//添加一个对应的placement delete
...
};
总结:(1)当你写一个placement operator new,请确定也写出了对应的placementoperator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄露;
(2)当你声明placementnew和placement delete,请确定不要无意识(非故意)地遮掩了它们的正常版本。
文章转自:http://blog.sina.com.cn/s/blog_6002b970010170ag.html