跳至内容

动态内存:new、delete 及其危险

学习动态分配如何通过 new 和 delete 工作,了解内存泄漏、双重释放和释放后使用的危险,并发现为什么现代 C++ 应该包含零个裸 new/delete。

使用 new 和 delete 进行动态分配

栈快速但大小和作用域有限。当你需要超出其创建作用域的对象,或其大小在编译时未知时,你需要在堆(自由存储区)上分配。

new T(args) 为一个 T 分配内存,调用其构造函数,并返回 T*。delete p 调用析构函数并释放内存。对于数组:new T[n] 和 delete[] p。将 new 与 delete[] 或 new[] 与 delete 混用是未定义行为 ── 编译器不会捕获这个错误。

new、delete 及其数组变体

这是原始分配 API。学习它以便你能在遗留代码中识别它,但永远不要在新代码中这样写 ── 使用智能指针代替。

new_delete.cpp
#include <iostream>
#include <cstring>

struct Widget {
    int id;
    Widget(int i) : id(i) { std::cout << "Widget " << id << " created\n"; }
    ~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};

int main() {
    // Single object
    Widget* w = new Widget(1);
    std::cout << "Using widget " << w->id << '\n';
    delete w;  // destructor called, memory freed

    // Array
    Widget* arr = new Widget[3]{{10}, {20}, {30}};
    for (int i = 0; i < 3; ++i)
        std::cout << arr[i].id << ' ';
    std::cout << '\n';
    delete[] arr;  // MUST use delete[] for new[]

    // UNDEFINED BEHAVIOR — don't do these:
    // delete arr;      // wrong: used delete instead of delete[]
    // delete[] w;      // wrong: used delete[] instead of delete
}

定位 new

定位 new在特定内存地址构造对象而不进行分配。它被分配器、内存池和容器(如 std::vector)内部使用。你必须显式调用析构函数 ── 对定位 new 创建的对象不能使用 delete。

placement_new.cpp
#include <iostream>
#include <new>       // required for placement new
#include <cstddef>   // std::byte
#include <memory>    // std::align

struct Sensor {
    int id;
    double reading;
    Sensor(int i, double r) : id(i), reading(r) {
        std::cout << "Sensor " << id << " init\n";
    }
    ~Sensor() { std::cout << "Sensor " << id << " shutdown\n"; }
};

int main() {
    // Pre-allocate raw storage (aligned)
    alignas(Sensor) std::byte buffer[sizeof(Sensor)];

    // Construct in pre-allocated memory
    Sensor* s = new (buffer) Sensor(42, 3.14);
    std::cout << "Reading: " << s->reading << '\n';

    // Must destroy explicitly — do NOT use delete
    s->~Sensor();  // explicit destructor call
    // delete s;    // WRONG — memory was not allocated by new
}

三大致命内存 bug

裸 new/delete 使你暴露于三类 bug:

1. 内存泄漏 ── 使用 new 分配但从未调用 delete。内存永远不会返回给操作系统(直到进程退出)。在长时间运行的服务器中,这最终会耗尽所有可用 RAM。

2. 双重释放 ── 对同一个指针调用 delete 两次。第二次 delete 会破坏堆分配器的内部数据结构,通常导致在无关代码中很久之后才崩溃。这极难调试。

3. 释放后使用 ── 在 delete 之后访问内存。该内存可能已被重新用于其他分配,因此你可能在读写另一个对象的数据。这是一个安全漏洞(CVE 级别的 bug)。

常见陷阱

考虑:f(new A(), new B())。编译器可能以任何顺序求值参数,并且可能交错它们:

1. 为 A 分配内存
2. 为 B 分配内存
3. 构造 A
4. 构造 B

如果第 4 步抛出异常,A 就泄漏了 ── 没有人调用 delete。即使 f(std::shared_ptr(new A()), std::shared_ptr(new B())) 在 C++17 之前也有这个问题(C++17 保证每个参数在下一个之前被完全求值)。修复方法:始终使用 std::make_unique 或 std::make_shared ── 它们将分配和构造作为一个单一的、异常安全的操作执行。