条款52:写了 placement new 也要写 placement delete
下面的代码调用了两个函数:1、调用operator new来分配内存;2、调用Widget的默认构造函数。
//如果第2步发生异常,撤销第1步的责任必须落在C++运行时系统上
Widget *pw = new Widget;
如果你正在处理具有普通签名的new和delete版本,这不是问题
void* operator new(std::size_t)
void operator delete(void* rawMemory) noexcept; // 全局作用域的普通签名
//类作用域的典型普通签名
void operator delete(void* rawMemory, std::size_t size) noexcept;
有一种特别有用的定位new,它接受一个指针,指定在何处构建对象:
//这个版本的new是C++标准库的一部分,当#include <new>时,便可以访问它。
void* operator new(std::size_t, void* pMemory) noexcept; // "定位new"
假设写了一个类专用new运算符,要求指定一个ostream,并记录分配信息,还写了一个普通的类专用delete运算符:
class Widget {
public:
...
//当new运算符接受size_t之外的形参时,该函数被称为定位 new。
static void* operator new(std::size_t size, std::ostream& logStream);// 非普通的new
static void operator delete(void* pMemory, std::size_t size) noexcept); // 普通的类专属的delete
...
//运行时系统会寻找一个delete运算符的版本,它需要接受与new运算符相同数量和类型的额外参数(void operator delete(void *, std::ostream&) noexcept;)
};
Widget* pw = new (std::cerr) Widget; // 如果Widget构造函数抛出异常,就会泄漏内存
Widget需要声明一个与日志定位new对应的放置delete:
class Widget {
public:
...
static void* operator new(std::size_t size, std::ostream& logStream);
static void operator delete(void*pMemory) noexcept;
static void operator delete(void*pMemory,std::ostream& logStream) noexcept;
...
};
但是,考虑一下如果没有抛出异常(通常情况下是这样),并且我们在客户端代码中执行delete操作会发生什么:
delete pw; // 调用普通的delete运算符
如果有一个基类,只声明了定位版本的new,会发现普通的new不可用了:
class Base {
public:
...
// 这个new隐藏了全局的普通new
static void* operator new(std::size_t size, std::ostream& logStream);
...
};
Base* pb = new Base; // 错误! 普通new运算符被隐藏了
Base* pb = new (std::cerr) Base; // 没问题,调用Base类的定位new
类似地,派生类中的 new 会同时隐藏全局版本和继承版本的 new:
class Derived : public Base {
public:
...
static void* operator new(std::size_t size); // 重新声明普通的new
...
};
Derived* pd = new (std::clog) Derived; // 错误! Base的定位new被隐藏了
Derived* pd = new Derived; // 没问题, 调用 Derived的new
C++在全局作用域中提供了下列形式的new运算符:
void* operator new(std::size_t); // 普通 new
void* operator new(std::size_t, void*) noexcept; // 定位 new
void* operator new(std::size_t,const std::nothrow_t&) noexcept;//nothrow new
//如果在类中声明任何news运算符,就会隐藏所有这些标准的形式。
//如果希望这些函数像往常一样工作,只需要让特定于类的版本调用全局版本。
class StandardNewDeleteForms { //基类提供所有的版本
public:
//普通 new/delete
static void* operator new(std::size_t size){return ::operator new(size);}
static void operator delete(void* pMemory) noexcept{::operator delete(pMemory);}
// 定位 new/delete
static void* operator new(std::size_t size, void* ptr) noexcept{return ::operator new(size, ptr);}
static void operator delete(void* pMemory, void* ptr) noexcept
{return ::operator delete(pMemory, ptr);}
// nothrow new/delete
static void* operator new(std::size_t size, const std::nothrow_t& nt) noexcept
{return ::operator new(size, nt);}
static void operator delete(void* pMemory, const std::nothrow_t&) noexcept
{::operator delete(pMemory);}
};
class Widget : public StandardNewDeleteForms { // 继承所有标准的形式
public:
using StandardNewDeleteForms::operator new; // 让这些形式可见
using StandardNewDeleteForms::operator delete;
static void* operator new(std::size_t size, std::ostream& logStream) ;// 添加一个自定义定位new
// 添加相应的定位delete
static void operator delete(void* pMemory, std::ostream& logStream) noexcept;
...
};
总结:
- 在编写定位版本的new运算符时,一定要编写相应的定位版本的delete运算符。如果不这样做,程序可能会遇到微妙的、间歇性的内存泄漏。
- 当你声明new和delete的定位版本时,请确保不要无意中隐藏了这些函数的普通版本。