C++中new和delete的多重面孔:operator new、new operator与placement new解析
《More Effective C++:35个改善编程与设计的有效方法》
读书笔记:了解各种不同意义的new和delete
在C++的内存管理体系中,new
和delete
看似简单,实则隐藏着多层逻辑。许多开发者对 new operator、operator new 和 placement new 的区别感到困惑。本文将逐层拆解这些概念,帮你掌握内存分配与对象构造的底层逻辑。
一、new operator:语言内置的“双任务”操作符
我们日常写的 new Type(...)
其实是 new operator(新表达式),它的行为由语言内置,不可直接修改。其核心工作分为两步:
- 分配内存:调用
operator new
函数,申请足够容纳Type
对象的内存; - 构造对象:调用
Type
的构造函数,初始化刚分配的内存。
示例:
string* ps = new string("Memory Management");
编译器会隐式生成类似以下逻辑:
// 1. 分配内存(调用 operator new)
void* raw_memory = operator new(sizeof(string));
// 2. 构造对象(编译器通过 placement new 实现)
string* ps = new (raw_memory) string("Memory Management");
二、operator new:可重载的内存分配函数
operator new
是普通函数,负责纯粹的内存分配,不涉及构造函数。它的原型为:
void* operator new(size_t size);
关键特性:
- 可定制性:可全局或类内重载,实现自定义内存分配策略(如内存池、统计分配次数);
- 直接调用:仅分配内存,返回未初始化的
void*
,需后续手动构造对象。
示例:
// 仅分配内存,无构造函数调用
void* raw_memory = operator new(sizeof(string));
三、placement new:在指定内存上构造对象
placement new 是 operator new 的特殊重载版本,允许在已有的内存地址上构造对象,跳过“分配内存”步骤。
核心逻辑:
- 原型(标准库实现):
void* operator new(size_t, void* location) { return location; // 直接返回传入的内存地址,不分配新内存 }
- 使用场景:
- 内存池:预先分配大块内存,后续在其上构造对象(减少分配开销);
- 共享内存/内存映射IO:对象必须位于特定地址。
使用步骤:
- 准备原始内存:可以是栈内存、共享内存等(需保证内存大小和对齐正确);
- 调用 placement new 构造对象;
- 手动调用析构函数(因
delete
会调用operator delete
,而此处内存可能非operator new
分配,故不能直接delete
); - 释放原始内存(若内存是动态分配的)。
示例:
class Widget {
public: Widget(int size) {} ~Widget() {}
}; // 步骤1:准备栈内存(也可是共享内存等)
alignas(Widget) char buffer[sizeof(Widget)]; // 步骤2:在 buffer 上构造 Widget
Widget* pw = new (buffer) Widget(10); // 步骤3:手动析构(必须!否则析构函数不会被调用)
pw->~Widget(); // 步骤4:若 buffer 是动态分配,需释放(此处栈内存自动释放,故省略)
注意:使用 placement new 需包含头文件 <new>
。
四、delete的对称逻辑:delete operator与operator delete
delete
操作符(delete operator)与 new
对称,也分两步:
- 析构对象:调用对象的析构函数;
- 释放内存:调用
operator delete
函数释放内存。
示例:
delete ps;
编译器隐式生成:
ps->~string(); // 析构对象
operator delete(ps); // 释放内存
特殊情况:placement new构造的对象
若对象由 placement new 构造(内存非 operator new
分配,如栈内存、共享内存),不能直接用 delete
,否则 operator delete
会错误释放内存。正确流程:
// 假设 pw 构造在共享内存上
pw->~Widget(); // 手动析构
freeShared(pw); // 释放共享内存(而非 operator delete)
五、数组的处理:new[]与delete[]
当用 new Type[size]
分配数组时,实际调用的是 数组版 new operator,流程为:
- 调用
operator new[]
分配内存(可重载); - 为数组每个元素调用默认构造函数(若
Type
有默认构造函数)。
对应的 delete[]
会:
- 为数组每个元素调用析构函数;
- 调用
operator delete[]
释放内存(可重载)。
关键注意:
- 配对使用:
new[]
必须与delete[]
配对,否则可能漏调析构函数(如用delete
代替delete[]
,仅调用第一个元素的析构); - 旧编译器兼容:
operator new[]
支持较晚,旧编译器可能 fallback 到全局operator new
,导致数组内存分配难以定制。
总结:概念地图
概念 | 角色 | 核心行为 | 可定制性 |
---|---|---|---|
new operator | 语言操作符 | 分配内存(调用 operator new) + 构造对象(placement new 隐式调用) | 不可直接定制 |
operator new | 内存分配函数 | 仅分配内存,返回 void* | 可重载(全局/类) |
placement new | operator new 的重载版本 | 在指定内存地址上构造对象(通过额外参数指定地址) | 可自定义重载 |
delete operator | 语言操作符 | 析构对象 + 释放内存(调用 operator delete) | 不可直接定制 |
operator delete | 内存释放函数 | 仅释放内存 | 可重载(全局/类) |
实践建议
- 普通堆对象:直接用
new
/delete
(new operator + delete operator); - 定制内存分配:重载
operator new
/operator delete
; - 指定内存构造:用 placement new,配合手动析构和内存释放;
- 数组:严格配对
new[]
和delete[]
,避免漏调析构。
理解这些概念后,你就能灵活应对复杂内存管理场景(如内存池、对象池),精准控制对象的生命周期与内存分配!