Item16–`new` 与 `delete` 的对应规则

👨‍🏫 条款 16 详解:newdelete 的对应规则

1. 核心规则回顾

分配 (new) 释放 (delete) 语义
new T delete p 分配/释放单个对象
new T[ ] delete [ ] p 分配/释放对象数组

违反这条规则会导致未定义行为 (Undefined Behavior, UB)。 在 C++ 中,未定义行为意味着程序的行为是不可预测的,可能表现为内存泄漏、数据损坏或程序崩溃。

2. 深入探究:为什么会出错?

当涉及拥有析构函数的类类型对象时,形式不匹配的问题最为严重。

A. 错误一:new T[] 配上 delete p

// 假设 ResourceHolder 是一个拥有析构函数的类
ResourceHolder* pArray = new ResourceHolder[5];
// ...
delete pArray; // 错误: new[] 却用了 delete

🔍 原理分析:

  1. new ResourceHolder[5] 的操作: 编译器在分配内存时,通常会在实际对象数组之前(或之后)预留一块空间,用于存储数组的元素个数(即 5)。接着,它会循环调用 5 次 ResourceHolder构造函数

  2. delete pArray 的操作: 运行时看到没有方括号的 delete,它假设这是单个对象的释放。它执行以下操作:

    • 只调用一次 ResourceHolder析构函数(针对数组的第一个元素 pArray[0])。

    • 释放整个内存块。

⚠️ 后果:

  • 资源泄漏: pArray[1]pArray[4] 这 4 个对象的析构函数没有被调用。如果这些对象在析构函数中释放了资源(如文件句柄、网络连接或内部堆内存),这些资源将无法被释放,造成典型的资源泄漏

B. 错误二:new T 配上 delete[] p

ResourceHolder* pSingle = new ResourceHolder;
// ...
delete [] pSingle; // 错误: new 却用了 delete[]

🔍 原理分析:

  1. new ResourceHolder 的操作: 仅为单个对象分配内存,并调用一次构造函数。内存块中没有存储数组大小的额外信息。

  2. delete [] pSingle 的操作: 运行时看到带方括号的 delete[],它会去寻找存储在内存块开头的数组大小信息。由于没有这个信息,它会读取该内存位置上的垃圾数据,并将其误认为是数组大小。

⚠️ 后果:

  • 程序崩溃: 运行时可能会基于这个错误的“大小”多次调用析构函数,这不仅是不必要的,而且极有可能访问到不属于该对象的内存,导致堆损坏程序立即崩溃

3. 解决方案:使用智能指针

条款 16 是 C++ 内存管理中一个常见的错误源。为了彻底避免这类问题,最好的办法是不要自己管理内存

推荐做法:使用 std::unique_ptr 管理动态分配的内存。

自 C++11 起,std::unique_ptr 被设计成能安全地处理单个对象和数组:

场景 代码 自动释放
单个对象 auto p = std::make_unique<T>(); p 超出作用域时,自动调用 delete p.release()
对象数组 auto p = std::make_unique<T[]>(N); p 超出作用域时,自动调用 delete [] p.release()
// 推荐示例:
#include <memory>

// 单个对象
auto p1 = std::make_unique<ResourceHolder>(); 
// p1 离开作用域时,自动调用 delete

// 对象数组 (N=5)
auto p2 = std::make_unique<ResourceHolder[]>(5);
// p2 离开作用域时,自动调用 delete[] (且会调用所有5个析构函数)

使用 std::unique_ptr 不仅保证了异常安全(即使发生异常也会释放资源),还自动选择了正确的 delete 形式,完全消除了条款 16 中描述的风险。

发表评论