C# · 12月 20, 2021

《Effective C++》读书笔记 条款52:写了placement new也要写placement delete

当写一个new表达式

Widget* pw = new Widget;

一共有两个函数会被调用:一个是用以分配内存的operator new,一个是Widget的default构造函数。如果第一个函数调用成功,第二个函数却抛出异常,为了防止内存泄漏,步骤一就需要调用相应的delete以恢复旧观。运行期系统会调用相应的operator delete,在知道哪一个delete该被调用的时候。对于正常的operator new,运行期系统会调用正常的operator delete。但是如果你声明了非正常的operator new,也就是有附带参数的new,(除了size还有其他参数)即placement new,该选择哪一个delete的问题便出现了。

假设,写了一个class专属的operator new,要求接受一个ostream,用来志记相关分配信息,同时又写了一个正常形式的class专属operator delete:

class Widget

{

public:

static void* operator new(std::size_t,std::ostream logStream) throw(std::bad_alloc);

static void operator delete(void* pMemory,std::size_t size) throw();

};

现在考虑动态创建一个Widget

Widget* pw = new (std::cerr) Widget;//调用operator new并传递cerr为其ostream实参;

//这个动作会在构造函数抛出异常时泄漏内存

现在,如果构造函数抛出异常,运行期系统会寻找参数个数和类型都和operator new相同的某个operator delete,operator delete应该也是placement delete:

void operator delete(void*,std::ostream&) throw();

而Widget中没有该函数,所以会造成内存泄漏。所以正确的应该是

class Widget

{

public:

static void* operator new(std::size_t,std::ostream logStream) throw(std::bad_alloc);

static void operator delete(void* pMemory) throw();

static void operator delete(void* pMemory,std::ostream& logStream) throw();

};

如果分配成功,调用delete pw;调用的是正常形式的operator delete,绝对不会调用placemenet delete。

由于成员函数的名称会掩盖外围作用域中的相同名称,所以必须小心避免让class专属的news掩盖其他news。同样的derived class会掩盖global版本和继承而来的operator new版本。对于delete也是如此,不能掩盖与new相关的delete。

解决方式,就是在类中包含所有正常形式的new和delete,对于继承,可以利用继承机制和using声明式取得标准形式。

请记住

1.当你写一个placement operator new,请确定也写出了对应的placement operator delete。如果没有这样做,你的程序可能会发生隐微而时断时续的内存泄漏。

2.当你声明placement new和placement delete,请确定不要无意识地掩盖了它们的正常版本。