C++ / a working model

42 / 80   ·   C++11   ·   约 11 分钟

new/delete 表达式与分配函数

先记住这句话

new 表达式通常包含取得存储和初始化对象,delete 表达式通常包含析构和释放存储;operator new/operator delete 是其中的底层分配函数,不等同于完整表达式。理解这层区别才能正确处理异常、数组和原位构造。

本篇内容
  1. 表达式负责完整对象操作
  2. 数组、初始化与失败协议
  3. 展开步骤只用于底层封装
  4. 运行示例
  5. 动手练习

表达式负责完整对象操作

new T(args) 是语言表达式,它选择合适的分配函数取得存储,并执行初始化。直接调用 ::operator new(sizeof(T)) 得到的是原始存储,不等于构造完成的任意 T。对应地,delete 表达式负责对象销毁流程,直接调用 operator delete 不会替你调用类析构函数。

new 不是总要请求新堆块。标准的非分配 placement new 接受调用者提供的地址,在已有存储中构造对象。调用者必须先保证尺寸、对齐和生存期条件,并自行安排最终归还存储;不能把该地址随便交给普通 delete。

数组、初始化与失败协议

new Tnew T[n] 必须分别匹配 delete 与 delete[],数组释放需要处理所有元素。new int 没有初始化整数值,而 new int{} 将其值初始化为零。不要通过读取未初始化整数来演示区别,说明规则就足够。

通常的抛异常分配失败以 bad_alloc 报告,nothrow 分配形式失败返回空指针;但构造函数自己的异常仍可以从 nothrow new 表达式中传播。如果普通 new 表达式已经分配成功、随后构造抛异常,会调用匹配的释放函数归还这份存储。

展开步骤只用于底层封装

示例直接取得存储,立即放进使用 operator delete 的拥有者;然后 placement new 构造 Token,验证状态并显式析构。Token 构造与析构都不抛异常,示例访问区也没有可抛业务操作,所以清理顺序完整而可追踪。

真实业务若在构造后执行可抛操作,还需单独保证对象析构,不能只给原始存储加释放守卫。通常直接 make_unique 就已正确组合这些步骤。malloc 同样不负责构造,但它具有不同的失败和释放协议;不能因某实现内部复用 malloc 就把接口混用。

容易答错的地方

  • 对 placement new 使用的自动数组或外部分配器存储执行普通 delete,会违反其释放协议。
  • nothrow 只改变相关分配失败的报告方式,不是包住整个构造过程的异常吞掉开关。

运行一个例子

最低标准 C++11 · 完整程序 · 下载 .cpp

#include <cassert>
#include <memory>
#include <new>

struct Deallocate {
    void operator()(void* p) const noexcept { ::operator delete(p); }
};
struct Token {
    static int alive;
    int value;
    explicit Token(int n) noexcept : value(n) { ++alive; }
    ~Token() noexcept { --alive; }
};
int Token::alive = 0;

int main() {
    std::unique_ptr<void, Deallocate> storage(::operator new(sizeof(Token)));
    assert(Token::alive == 0);
    Token* token = ::new (storage.get()) Token(7);
    assert(Token::alive == 1 && token->value == 7);
    token->~Token();
    assert(Token::alive == 0);
}

在本地编译

g++ -std=c++11 -Wall -Wextra -Wpedantic -pthread memory-new-delete.cpp -o example && ./example

预期结果

预期:正常退出、无输出;所有 assert 通过。

CHECK YOUR UNDERSTANDING

合上答案,试着解释。

直接调用 operator new 后再 placement new 构造 T,若 T 构造抛异常,谁归还之前申请的存储?

查看参考答案

标准非分配 placement new 对应的 placement delete 不会归还这块外部存储,责任仍属于调用者。可以像示例那样先建立原始存储拥有者,使构造失败时自动调用匹配的 operator delete。若改成普通 new T(args),表达式会在构造失败时处理与其分配相匹配的释放;更高层可直接使用 make_unique。

继续查证

标准草案链接会随工作草案更新;本文版本标记对应示例最低要求,不表示草案中的所有新规则都适用于旧标准。

回到目录