Item 16:成对使用相同形式的 new 和 delete
对齐 Effective C++ 第三版 Item 16:匹配 new/delete 与 new[]/delete[],避免 typedef 隐藏数组性,并用 vector、array、unique_ptr 单对象/数组及 custom deleter 编码释放协议。
学习目标
- 能解释 new/delete 与 new[]/delete[] 在元素构造、array cookie、逆序析构和 deallocation 上的协议差异
- 能分析并识别 typedef、raw factory 与 allocator family 隐藏的释放形式,并改为 vector/array/typed owner
- 能回答:给定
new、new[]、malloc和跨模块 factory,如何为每个入口选择匹配 deleter 并用测试证据证明它
猜一猜:把 new Widget[4] 改成 delete,哪一条释放证据会先失真——元素析构次数、array cookie,还是 allocator family?
Protocol lab
先预测:哪一个 delete 能证明释放协议匹配?
选择 allocation form,观察构造/析构证据和最终 deallocation family;故障注入只改变释放形式。
当前判定
匹配:new[] / delete[]
4 个元素 → 逆序析构 → cookie/数组释放入口完整回收
审计提示:不要根据一次“没崩”推断安全;用非平凡析构对象、sanitizer 和真实 allocator 配置复核。
从“pointer 类型为什么看不出数组”开始
Widget* single = new Widget;
Widget* many = new Widget[10];两个变量类型都显示 Widget*,但释放协议不同:
delete single;
delete[] many;Item 16 要求 Use the same form in corresponding uses of new and delete(成对使用相同形式的 new 和 delete)。 简化成审计规则就是 same form in new and delete:单对象、数组、对齐、C heap 与平台 allocator 都必须从创建入口追踪到同家族释放入口。
数组需要知道析构多少元素
↡部分 new[] 实现在返回 pointer 前保存的数组元素数量或其他元数据。delete[] 需要逐个调用非平凡 destructor,典型实现通过 cookie 找到数量。标准不规定具体布局,但程序必须使用匹配表达式。
struct Probe {
static inline int alive{};
Probe() { ++alive; }
~Probe() { --alive; }
};
Probe* probes = new Probe[4];
delete[] probes;
assert(Probe::alive == 0);错误 delete probes 是未定义行为,可能只析构一个元素、读取错误 cookie 或传错 deallocator。
delete[] 也负责构造失败回滚
↡new[] 构造第 N 个元素失败时,已构造元素自动逆序析构并释放整块存储的保证。class Element {
public:
Element()
{
if (++constructed == failAt)
throw Failure{};
}
~Element() { --alive; }
};new[] expression 自己管理回滚。手工 malloc 加 placement new 数组则必须自行记录已构造数量并在异常时逆序清理。
↡在原始存储指定地址上显式开始对象生命周期、不会分配存储的构造形式。这属于低层容器实现,不能和普通 new[] 混为一谈。
typedef 会隐藏数组性
原书特别警告:
typedef std::string AddressLines[4];
std::string* lines = new AddressLines;
delete[] lines; // 必须知道 typedef 是数组不要对数组类型 typedef 后暴露 raw new/delete。现代写:
using AddressLines = std::array<std::string, 4>;
auto lines = std::make_unique<AddressLines>();调用方只销毁一个 AddressLines 对象,内部数组由其 destructor 正确处理。
动态序列优先 vector
↡拥有动态连续元素、记录 size/capacity 并自动构造析构的标准值容器。std::vector<Widget> widgets(10);比 new Widget[10] 多提供 size、迭代器、异常安全和移动。大多数应用代码无需直接数组 new。
把 Item 16 从运行时纪律变成类型保证。
unique_ptr 单对象与数组特化
↡默认 deleter 使用 delete 的独占单对象 owner,并提供解引用和箭头。 ↡默认 deleter 使用 delete[] 的独占数组 owner,并提供下标访问。auto singleOwner = std::make_unique<Widget>();
auto arrayOwner = std::make_unique<Widget[]>(10);类型不同,deleter 与 API 不同。不能把 new Widget[10] 放进 unique_ptr<Widget>。
让错误在构造 owner 时暴露,而不是销毁时。
make_unique 避免裸 new 窗口
↡分配并构造对象后直接返回正确 unique_ptr 类型的标准 factory。auto image = std::make_unique<Image>(width, height);
auto pixels = std::make_unique<Pixel[]>(count);减少重复类型和 raw pointer 暴露;异常路径自动清理。
↡资源已由 new 获取但尚未交给 owner、异常可能造成泄漏的短暂代码区间。Item 17 会继续讨论函数实参求值中的这一风险。
malloc/free 是另一分配家族
↡C heap 只分配和释放原始字节、不自动开始或结束 C++ 对象生命周期的 API。void* memory = std::malloc(bytes);
if (!memory)
throw std::bad_alloc{};
std::free(memory);不能 delete memory,也不能 free(new T)。
不仅是括号匹配,还包括 API/heap/参数。
custom deleter 编码外部协议
struct FreeDeleter {
void operator()(void* pointer) const noexcept { std::free(pointer); }
};
using MallocBuffer = std::unique_ptr<void, FreeDeleter>;平台资源同理:fopen/fclose、CreateFile/CloseHandle、GPU create/destroy。
factory 应直接返回带 deleter 的 owner。
aligned allocation 还要匹配参数
↡以超出默认 new 对齐的 alignment 请求分配存储的 C++ 分配形式。void* memory = ::operator new(bytes, std::align_val_t{64});::
operator delete(memory, std::align_val_t{64});编译器生成 delete-expression 会选择正确 overload;手工 operator new/delete 调用必须自己配对。Item 51/52 会深入 custom new/delete。
跨模块 allocator
↡对象在一个 DLL/共享库 heap 创建,却在另一个不兼容 heap 释放的错误。库导出 raw pointer 并让调用方 delete 可能跨 CRT。安全接口导出配对 destroy:
extern "C" ApiObject* create_object();
extern "C" void destroy_object(ApiObject*) noexcept;调用方用 custom deleter 回到创建模块。
↡资源由创建模块提供的函数释放,以保持 allocator、ABI 和版本一致的规则。这是 allocator-family pairing 的模块级形式。
API 不应返回模糊 raw array
↡函数只返回 T*,调用方无法从类型知道长度、ownership 和 delete 形式的接口。Widget* makeWidgets(std::size_t count); // 谁拥有?delete 还是 delete[]?替代:返回 vector、unique_ptr array 加 count、span borrow,或 caller-provided buffer。
↡返回 owner 和元素数量,使数组释放和范围信息同时明确的结果类型。struct WidgetArray {
std::unique_ptr<Widget[]> data;
std::size_t size{};
};通常 vector 更完整;特殊 ABI 才需要自定义。
形式匹配审计
↡追踪每个分配入口到 owner/deleter、元素数量、模块和最终释放入口的检查。先预测:new Probe[4] 配 delete 时 destructor ledger 会剩多少;不要在主进程依赖 UB 结果,使用 sanitizer 子测试。
- 单对象/数组 Probe constructor/destructor 计数完全匹配。
- new[] 第 N 个构造失败时此前元素逆序析构。
- unique_ptr single/array 类型与 factory 返回一致。
- malloc/custom platform owner 使用正确 deleter,未调用 delete。
- aligned operator delete 收到同 alignment。
- 跨模块创建销毁在真实 allocator 配置下运行。
小结
- new 对应 delete,new[] 对应 delete[];raw pointer 类型无法显示数组性
- array cookie 是典型实现细节,程序仍必须遵守数组完整生命周期协议
- 避免数组 typedef 配 raw new/delete,优先 array/vector
- unique_ptr single 与 array form 用类型编码正确 deleter
- malloc/free、aligned new/delete、平台 Create/Destroy 和模块 heap 都要按家族配对
- allocation-pair audit 与 destructor ledger 证明构造析构和释放入口一致
名词解释
本章出现的专业名词,用大白话再讲一遍。
- single-object allocation form
new 一个对象并用 delete 销毁释放的协议。
- array allocation form
new[] 多个元素并用 delete[] 完整销毁的协议。
- array cookie
实现为数组析构保存的元素数等元数据。
- element-wise destruction
数组元素按逆序逐个执行析构。
- partial-array construction cleanup
数组中途构造失败时自动逆序回滚。
- placement construction
在已有原始存储上显式开始对象生命周期。
- std::array value
封装固定元素数量和生命周期的标准值容器。
- vector ownership
动态连续元素由 vector 自动拥有管理。
- container-managed lifetime
由容器管理对象构造析构的设计。
- unique_ptr single form
默认执行 delete 的独占单对象 owner。
- unique_ptr array form
默认执行 delete[] 的独占数组 owner。
- typed deallocation protocol
owner 类型携带正确释放形式的协议。
- make_unique
直接创建并返回正确 unique owner 的 factory。
- unowned allocation window
new 成功到 owner 建立之间的泄漏窗口。
- malloc/free family
只管理原始字节的 C heap 分配家族。
- allocator-family pairing
创建和释放必须来自同一协议家族。
- custom deleter
owner 保存并调用的特定释放函数对象。
- external allocation family
平台 API 提供的 create/destroy 资源协议。
- over-aligned allocation
请求超默认 alignment 的存储分配。
- deallocation-parameter pairing
释放时匹配 alignment/size 等参数。
- cross-module heap mismatch
创建与释放位于不兼容模块 heap 的错误。
- creator-owned deallocation
由创建模块的 destroy 函数释放资源。
- ambiguous raw allocation API
无法从 T* 得知数组长度和释放形式的接口。
- sized array owner
同时携带 array owner 与元素数量的类型。
- allocation-pair audit
追踪每次分配到最终匹配释放的审计。
练习
- 问题 1:修复数组 typedef。 旧接口 typedef 数组并返回 raw pointer,设计现代替代和迁移测试。
- 问题 2:配对三类 allocator。 同一模块中同时有 new、malloc、平台 CreateBuffer,设计 owner 类型。
- 问题 3:验证跨模块释放。 动态库创建对象、应用持有,设计不会跨 heap delete 的接口。