Effective C++读书笔记——item52(如果编写了 placement new,就要编写 placement delete)
在 C++ 中,当编写自定义的 placement new
时,需要同时编写对应的 placement delete
,并且要注意避免覆盖 new
和 delete
的常规版本,以防止内存泄漏和使用异常。下面结合代码详细介绍相关知识点。
1. placement new
和 placement delete
的基本概念
当使用 new
表达式创建对象时,会先调用 operator new
分配内存,再调用对象的构造函数。若构造函数抛出异常,C++ 运行时系统需撤销之前的内存分配,这就要求有对应的 operator delete
函数。
常规的 operator new
和 operator delete
匹配关系明确,但当 operator new
带有额外参数时,就形成了 placement new
,此时需要有对应的 placement delete
(带有相同额外参数)来处理构造函数抛出异常时的内存释放。
2. 内存泄漏问题及解决方法
如果 placement new
没有对应的 placement delete
,当构造函数抛出异常时,运行时系统无法撤销内存分配,会导致内存泄漏。
#include <iostream>
#include <new>
class Widget {
public:
// 自定义的 placement new
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc) {
logStream << "Allocating memory for Widget" << std::endl;
return ::operator new(size);
}
// 常规的 operator delete
static void operator delete(void* pMemory) throw() {
::operator delete(pMemory);
}
// 缺少对应的 placement delete,会导致内存泄漏
Widget() {
// 模拟构造函数抛出异常
throw std::bad_alloc();
}
};
int main() {
try {
Widget* pw = new (std::cerr) Widget;
} catch (const std::bad_alloc& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
// 由于缺少对应的 placement delete,内存泄漏
}
return 0;
}
为了解决这个问题,需要添加对应的 placement delete
。
#include <iostream>
#include <new>
class Widget {
public:
// 自定义的 placement new
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc) {
logStream << "Allocating memory for Widget" << std::endl;
return ::operator new(size);
}
// 常规的 operator delete
static void operator delete(void* pMemory) throw() {
::operator delete(pMemory);
}
// 对应的 placement delete
static void operator delete(void* pMemory, std::ostream& logStream) throw() {
logStream << "Deallocating memory for Widget" << std::endl;
::operator delete(pMemory);
}
Widget() {
// 模拟构造函数抛出异常
throw std::bad_alloc();
}
};
int main() {
try {
Widget* pw = new (std::cerr) Widget;
} catch (const std::bad_alloc& e) {
std::cerr << "Exception caught: " << e.what() << std::endl;
// 构造函数抛出异常时,对应的 placement delete 会被调用,避免内存泄漏
}
return 0;
}
需要注意的是,只有在调用 placement new
关联的构造函数时发生异常,placement delete
才会被调用;正常使用 delete
指针时,调用的是常规的 operator delete
。
3. 避免覆盖 new
和 delete
的常规版本
在类中声明自定义的 operator new
会覆盖全局的标准形式,包括常规的 new
、placement new
和 nothrow new
。为了避免这种情况,可以创建一个包含所有标准形式的基类,并使用 using
声明让派生类可见。
#include <iostream>
#include <new>
// 包含所有标准形式的基类
class StandardNewDeleteForms {
public:
// 常规的 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);
}
};
class Widget : public StandardNewDeleteForms {
public:
using StandardNewDeleteForms::operator new;
using StandardNewDeleteForms::operator delete;
// 自定义的 placement new
static void* operator new(std::size_t size, std::ostream& logStream) throw(std::bad_alloc) {
logStream << "Allocating memory for Widget" << std::endl;
return ::operator new(size);
}
// 对应的 placement delete
static void operator delete(void* pMemory, std::ostream& logStream) throw() {
logStream << "Deallocating memory for Widget" << std::endl;
::operator delete(pMemory);
}
};
int main() {
// 使用常规的 new
Widget* pw1 = new Widget;
delete pw1;
// 使用自定义的 placement new
Widget* pw2 = new (std::cerr) Widget;
try {
delete pw2;
} catch (...) {}
// 使用 placement new 的标准形式
char buffer[sizeof(Widget)];
Widget* pw3 = new (buffer) Widget;
pw3->~Widget();
// 使用 nothrow new
std::nothrow_t nt;
Widget* pw4 = new (nt) Widget;
if (pw4) {
delete pw4;
}
return 0;
}
总结要点
- 对应关系:编写
operator new
的placement
版本时,必须同时编写operator delete
的相应placement
版本,以避免构造函数抛出异常时的内存泄漏。 - 避免覆盖:声明
new
和delete
的placement
版本时,要确保不会无意中覆盖这些函数的常规版本,可以通过继承包含标准形式的基类并使用using
声明来解决。