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
  • 能回答:给定 newnew[]malloc 和跨模块 factory,如何为每个入口选择匹配 deleter 并用测试证据证明它

猜一猜:把 new Widget[4] 改成 delete,哪一条释放证据会先失真——元素析构次数、array cookie,还是 allocator family?

same form in new and deleteallocation form → object lifetime → deallocation familynew / new[]single object or arraytype + count + allocator familylifetime evidenceconstruct / destroy each elementarray cookie may store countdelete / delete[]matching release entrynever infer from T* alone三个审计问题new[] / delete[]:数组形式是否成对?typedef:别名是否隐藏了数组性?跨 heap / malloc:释放家族是否一致?typed owner(vector / array / unique_ptr<T[]> / custom deleter)让协议可检查
先锁定 allocation form,再追踪构造、析构和最终释放;pointer 类型本身不能证明数组性。

Protocol lab

先预测:哪一个 delete 能证明释放协议匹配?

选择 allocation form,观察构造/析构证据和最终 deallocation family;故障注入只改变释放形式。

数组 owner · protocol traceallocationnew Widget[4]lifetimeelements / cookiereleasedelete[] pointer应保存证据4 个元素 → 逆序析构 → cookie/数组释放入口完整回收

当前判定

匹配: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 都必须从创建入口追踪到同家族释放入口。

数组需要知道析构多少元素

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[] 也负责构造失败回滚

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

std::vector<Widget> widgets(10);

new Widget[10] 多提供 size、迭代器、异常安全和移动。大多数应用代码无需直接数组 new。

把 Item 16 从运行时纪律变成类型保证。

unique_ptr 单对象与数组特化

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 窗口

auto image = std::make_unique<Image>(width, height);
auto pixels = std::make_unique<Pixel[]>(count);

减少重复类型和 raw pointer 暴露;异常路径自动清理。

Item 17 会继续讨论函数实参求值中的这一风险。

malloc/free 是另一分配家族

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/fcloseCreateFile/CloseHandle、GPU create/destroy。

factory 应直接返回带 deleter 的 owner。

aligned allocation 还要匹配参数

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

库导出 raw pointer 并让调用方 delete 可能跨 CRT。安全接口导出配对 destroy:

extern "C" ApiObject* create_object();
extern "C" void destroy_object(ApiObject*) noexcept;

调用方用 custom deleter 回到创建模块。

这是 allocator-family pairing 的模块级形式。

API 不应返回模糊 raw array

Widget* makeWidgets(std::size_t count); // 谁拥有?delete 还是 delete[]?

替代:返回 vector、unique_ptr array 加 count、span borrow,或 caller-provided buffer。

struct WidgetArray {
    std::unique_ptr<Widget[]> data;
    std::size_t size{};
};

通常 vector 更完整;特殊 ABI 才需要自定义。

形式匹配审计

先预测: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 证明构造析构和释放入口一致

资料与写作方式声明

本章以Effective C++, Third Edition, Item 16权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

名词解释

本章出现的专业名词,用大白话再讲一遍。

single-object allocation form

new 一个对象并用 delete 销毁释放的协议。

array allocation form

new[] 多个元素并用 delete[] 完整销毁的协议。

element-wise destruction

数组元素按逆序逐个执行析构。

partial-array construction cleanup

数组中途构造失败时自动逆序回滚。

placement construction

在已有原始存储上显式开始对象生命周期。

hidden array type

typedef/alias 隐藏底层数组性的问题。

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. 问题 1:修复数组 typedef。 旧接口 typedef 数组并返回 raw pointer,设计现代替代和迁移测试。
  1. 问题 2:配对三类 allocator。 同一模块中同时有 new、malloc、平台 CreateBuffer,设计 owner 类型。
  1. 问题 3:验证跨模块释放。 动态库创建对象、应用持有,设计不会跨 heap delete 的接口。

讨论

评论区加载中…