C++动态内存分配超详细讲解 前言动态内存分配解决的是「对象数量或大小在编译期无法确定」的问题。C 提供了两套工具C 风格的malloc/free以及 C 的new/delete。二者最根本的区别不是语法而是是否构造和析构对象new在分配到的存储上调用构造函数delete先调用析构函数再释放存储malloc/free只处理字节。常见的误解集中在三处。第一以为new只是「带类型的 malloc」忽略了构造函数的调用和失败时的异常。第二以为delete之后指针会自动置空实际上它只是释放内存指针仍指向原地址再次使用就是悬垂访问。第三以为new[]和delete可以混用或者malloc出来的内存可以用delete释放——这些都是未定义行为可能当场崩溃也可能安静地破坏堆。本文按「表达式做了什么—失败怎么处理—数组与对齐—现代替代」的顺序展开。所有示例以 C17 为基准涉及 C17 才有的对齐分配会单独说明。需要提前强调现代 C 的首要原则是尽量不直接写new和delete用容器和智能指针表达所有权本文讲底层机制是为了让你在维护旧代码或实现底层组件时能看懂、改对。一、new 表达式到底做了什么一个new表达式包含两个步骤顺序固定调用分配函数取得足够大的、正确对齐的原始存储默认是全局operator new。在这块存储上构造对象调用构造函数。delete表达式则相反先调用析构函数再调用释放函数默认是全局operator delete。如果构造函数抛出异常第 1 步分配的内存会被自动释放不会泄漏——这一点常被误解为「构造函数抛异常一定泄漏」。// 适用于 C17#include newstruct Point {int x;int y;Point(int a, int b) : x(a), y(b) {}};void basic_usage() {Point* p new Point(1, 2); // 分配 构造delete p; // 析构 释放void* raw ::operator new(sizeof(Point)); // 只分配原始存储Point* q new (raw) Point(3, 4); // placement new只构造不分配q-~Point(); // 必须手动析构::operator delete(raw); // 再释放存储}这里出现了三个层次必须分清层次写法是否分配是否构造/析构new 表达式new Point(1, 2)是构造配对delete析构分配函数::operator new(n)是否placement newnew (ptr) Point(1, 2)否只构造需手动析构operator new和operator delete可以被全局替换也可以按类重载这是内存池、对象计数、调试追踪的实现基础。替换全局版本会影响整个程序属于底层设施业务代码不应随意做。类内重载只影响该类型及其派生类的new表达式。还有一个容易忽略的点new返回的是已构造对象的指针类型正确malloc返回void*需要显式转换且转换前后都没有类型检查。二、分配失败、nothrow 与 new_handler默认的operator new在分配失败时抛std::bad_alloc定义在new。这与malloc返回空指针的行为不同也是 C 代码通常不需要在每个new后判空的原因——失败会以异常形式向上传播。如果确实需要「失败返回空指针」的语义可以用nothrow形式// 适用于 C17#include new#include iostreamvoid try_allocate() {int* p new (std::nothrow) int[1024];if (p nullptr) {std::cerr allocation failed\n;return;}delete[] p;}必须注意std::nothrow只影响分配失败的行为不影响构造函数。如果构造函数抛异常异常照常传播内存仍会被自动释放。把nothrow理解成「这个 new 绝不抛异常」是错的。标准库还提供了std::set_new_handler注册一个在分配失败时被调用的函数。处理函数可以释放一些预留内存再返回让分配重试如果它无法腾出空间应当抛出bad_alloc或终止程序否则operator new会不断重试形成死循环。这套机制适合做全局的内存紧张兜底不适合细粒度的错误处理。三、数组、对齐与未定义行为数组形式的new[]与delete[]必须严格配对。原因不只是「记错运算符」而是两者的内存布局可能不同对于有非平凡析构函数的类型实现通常会在返回指针之前保存元素个数俗称 array cookiedelete[]依靠它逐个调用析构函数。标准没有规定这个 cookie 的具体形式但要求new[]配delete[]、new配delete交叉使用是未定义行为。// 适用于 C17void array_usage() {int* a new int[10]; // 默认初始化int 的值不确定int* b new int[10](); // 值初始化全部为 0delete[] a;delete[] b;}new int[10]与new int[10]()的差别经常被忽略前者对内置类型不做初始化读取未写过的元素是未定义行为后者进行值初始化元素为零。对类类型两者都会调用默认构造函数差别体现在「有没有用户提供的默认构造函数」上。对齐是另一个维度。C17 起new表达式会自动处理过对齐类型over-aligned type底层调用带std::align_val_t参数的分配函数。手写operator new或使用malloc时必须自己保证对齐满足alignof(T)否则访问对象是未定义行为。用std::aligned_allocC11/C17或平台提供的对齐分配函数时大小通常还必须是 alignment 的整数倍这些细节要查对应文档。placement new 用在数组上时问题更复杂它不会返回元素个数析构时必须自己记录并逐个调用。这也是标准库容器把「分配」和「构造」分开管理allocator 的allocate/construct的原因。四、现代替代容器、智能指针与 RAII裸new/delete的问题不在于语法而在于所有权不明确一块内存由谁释放、在异常路径上是否释放、提前返回时是否泄漏全靠人工保证。RAIIresource acquisition is initialization把释放动作绑定到对象析构让编译器替你执行。场景推荐写法引入版本单个对象std::make_uniqueT(args...)C14动态数组std::make_uniqueT[](n)C14共享所有权std::make_sharedT(args...)C11可变长序列std::vectorTC98字符串std::stringC98std::unique_ptr不可拷贝、可移动表达独占所有权std::unique_ptrT[]会正确调用delete[]。std::make_shared把对象和控制块合并分配减少一次分配但也意味着即使还有weak_ptr存活对象所占的内存也要等控制块一起释放。二者没有绝对优劣按生命周期需求选择。// 适用于 C17#include memorystruct Point {int x;int y;Point(int a, int b) : x(a), y(b) {}};void modern_usage() {auto p std::make_uniquePoint(1, 2);auto arr std::make_uniqueint[](10);auto shared std::make_sharedPoint(3, 4);std::unique_ptrPoint moved std::move(p); // 所有权转移p 变为空}如果确实要写底层容器至少把「分配」封装进一个不可拷贝的 RAII 类型让析构函数负责释放。下面是一个完整的动态缓冲区实现它同时演示了异常安全的扩容// 适用于 C17#include cstddef#include memory#include utilityclass Buffer {public:explicit Buffer(std::size_t n): size_(n), data_(std::make_uniqueint[](n)) {}Buffer(const Buffer) delete;Buffer operator(const Buffer) delete;Buffer(Buffer) noexcept default;Buffer operator(Buffer) noexcept default;void resize(std::size_t n) {auto next std::make_uniqueint[](n); // 先分配失败则原对象不变const std::size_t keep n size_ ? n : size_;for (std::size_t i 0; i keep; i) {next[i] data_[i];}data_ std::move(next); // 提交释放旧缓冲区size_ n;}int operator[](std::size_t i) { return data_[i]; }const int operator[](std::size_t i) const { return data_[i]; }std::size_t size() const { return size_; }private:std::size_t size_;std::unique_ptrint[] data_;};resize的顺序很重要先分配新缓冲区并拷贝全部成功后才替换成员。如果make_unique抛bad_allocdata_和size_都没有被修改Buffer仍是原状态满足强异常安全保证。反过来先delete旧数据再分配一旦分配失败对象就处于半损坏状态。注意Buffer显式删除了拷贝默认了移动。unique_ptr成员本身已经让拷贝被隐式删除这里写出来是为了让意图更明确移动构造函数标noexcept容器在扩容时才敢用它。常见坑点❌int* p new int[10]; delete p;✅new[]必须配delete[]交叉使用是未定义行为。❌int* p (int*)malloc(sizeof(int) * 10); delete[] p;✅malloc/free与new/delete不能混用分配与释放必须来自同一套机制。❌ 认为delete p;之后p变成nullptr。✅delete不会修改指针置空需要手动写p nullptr;或直接用智能指针。❌ 同一指针delete两次或delete后继续解引用。✅ 双重释放和释放后使用都是未定义行为用unique_ptr从根本上避免。❌ 以为new int[10]会把元素初始化为 0。✅ 内置类型默认初始化不赋值需要清零写new int[10]()或使用std::vectorint(10)。❌ 以为new (std::nothrow)连构造函数异常也不抛。✅ 它只处理分配失败构造函数抛出的异常照常传播内存仍会被释放。❌ 用 placement new 构造对象后直接operator delete不调用析构函数。✅ 先显式调用p-~T()再释放原始存储数组还要记录元素个数逐个析构。❌ 对void*调用delete或用memset/memcpy操作非平凡类型。✅delete void*是未定义行为且不会调用析构非平凡类型必须用构造/赋值/析构。总结操作是否构造/析构失败行为配对释放new T/delete是抛std::bad_allocdeletenew T[n]/delete[]是逐个抛std::bad_allocdelete[]::operator new/::operator delete否抛std::bad_alloc::operator deleteplacement new只构造不分配无分配失败手动析构malloc/free否返回nullptrfree动态内存的核心不是「怎么分配」而是「谁负责释放、异常时怎么办」。裸new把这两个问题留给了人容器和智能指针把它们交给了类型系统。维护旧代码时先确认每一对分配与释放是否匹配写新代码时默认使用make_unique、make_shared和标准容器只在实现底层设施时才直接接触operator new。