Effective C++ 条款52:写了placement new也要写placement delete
Effective C++ 条款52:写了placement new也要写placement delete
核心思想:当你重载一个带有额外参数的operator new
(即“placement new”)时,必须同时重载带有完全相同额外参数的operator delete
。否则,当对象的构造函数在placement new分配的内存上抛出异常时,会发生微妙而严重的内存泄漏。同时,要注意避免无意中掩盖全局的正常版本。
⚠️ 1. 问题的根源:构造函数异常
1.1 C++的内存分配与构造流程:
operator new
分配原始内存- 在该内存上调用构造函数
- 如果步骤2的构造函数抛出异常,运行时系统必须能够自动释放在步骤1中分配的内存,以避免内存泄漏。
1.2 运行时系统的职责:
当构造函数抛出异常,运行时系统需要找到与调用operator new
时签名完全相同的operator delete
来释放内存。如果找不到,则什么也不做,导致内存泄漏。
🚨 2. placement new/delete 的匹配规则
2.1 标准placement new:
最常用的placement new是接收一个void*
指针参数,用于在指定地址构造对象。
// 标准库提供的placement new
void* operator new(std::size_t, void* pMemory) noexcept;
// 对应的placement delete
void operator delete(void* pMemory, void* pLocation) noexcept;
2.2 自定义placement new:
你可以定义接收任意额外参数的operator new
。
class Widget {
public:// 自定义的placement new(额外带一个int参数)static void* operator new(std::size_t size, int extraParam) {std::cout << "Custom placement new called with: " << extraParam << std::endl;return ::operator new(size); // 这里为了简单,仍使用全局new}// ⚠️ 必须提供对应的placement delete!// 参数列表:(size_t) + 与placement new完全相同的额外参数列表(int)static void operator delete(void* pMemory, int extraParam) noexcept {std::cout << "Custom placement delete called with: " << extraParam << std::endl;::operator delete(pMemory);}// ... 通常的operator delete也不能少static void operator delete(void* pMemory) noexcept;
};
使用示例与潜在问题:
try {// 调用自定义的placement newWidget* pw = new (100) Widget; // 传递额外参数100// ... 如果Widget构造函数在此处抛出异常...// 运行时系统会自动调用 operator delete(pw, 100)
} catch (...) {// 如果没有定义 operator delete(void*, int),内存将在此泄漏
}
⚖️ 3. 避免名称隐藏问题
3.1 默认的名称隐藏(Name Hiding):
在类中声明任何operator new
(包括placement版本)都会隐藏全局的、标准的operator new
。这意味着new Widget
会编译失败,因为找不到标准的operator new(size_t)
。
3.2 解决方案:提供标准版本并using基类版本:
为了同时使用自定义placement new和标准new,必须在类中同时声明它们,并使用using
引入基类的operator new以确保继承链正常工作。
class StandardNewDeleteBase {
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[]
};class Widget : public StandardNewDeleteBase {
public:using StandardNewDeleteBase::operator new;using StandardNewDeleteBase::operator delete;// 自定义placement newstatic void* operator new(std::size_t size, int extraParam) {// ... 自定义实现return ::operator new(size);}// 对应的placement deletestatic void operator delete(void* pMemory, int extraParam) noexcept {// ... 自定义实现::operator delete(pMemory);}
};// 现在以下调用都是合法的:
Widget* pw1 = new Widget; // 正确,调用了被using引入的标准new
Widget* pw2 = new (100) Widget; // 正确,调用了自定义的placement new
💡 关键设计原则
- 成对实现
每一个自定义的placementoperator new
(即任何非标准的、带有额外参数的new)都必须有一个参数列表完全匹配的placementoperator delete
伴随左右。这是防止构造函数异常导致内存泄漏的唯一安全网。 - 理解调用时机
placementoperator delete
只有在与之匹配的placementoperator new
成功分配内存,但后续的对象构造函数抛出异常时,才会被运行时系统自动调用。如果你正常地delete
一个对象,即使它是用placement new创建的,被调用的也永远是普通的operator delete(void*)
。 - 管理名称空间
在类内部重载operator new/delete
会隐藏全局版本。务必使用using ::operator new;
和using ::operator delete;
(或使用如上面示例中的基类技巧)来确保所有需要的版本都可见,保持代码的灵活性。
进阶提示:大小感知的placement delete (C++14+)
现代C++允许placement delete也接受大小参数,这在实现自定义内存池时非常有用,可以优化释放操作。class Widget {// ...// 大小感知的placement delete (更优)static void operator delete(void* pMemory, std::size_t size, int extraParam) noexcept {std::cout << "Sized placement delete. Size: " << size << ", Param: " << extraParam << std::endl;::operator delete(pMemory);} };
编译器会优先选择最匹配的版本。提供大小感知的版本通常是最佳实践。
总结:
placement new和placement delete是“成对出现”的生死之交。定义任何形式的placement operator new
(即带有额外参数的new)时,都必须毫不例外地定义与之精确匹配的placement operator delete
。这是确保在对象构造失败时系统能自动清理内存、避免资源泄漏的黄金法则。此外,要小心类内重载带来的名称隐藏问题,通过使用using
声明或继承体系来确保标准的new/delete版本依然可用。遵守此条款,你才能安全地利用placement new的强大功能进行底层内存管理。