placement new和placement delete

对于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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值