delete释放new[]分配的内存会导致仅调用首个对象析构函数,其余对象析构被跳过,若类中含动态资源(如new出的指针、文件句柄等)则必然泄漏;对内置类型虽无析构,但属未定义行为,换环境易崩溃。
![c++delete和delete[]为什么不能混用](https://img.php.cn/upload/article/001/431/639/179160227063547.png)
delete 释放 new[] 分配的内存会漏掉析构函数
new[] 分配对象数组时,编译器会在实际数据前悄悄存一个数组长度(比如 4 字节或 8 字节),然后让返回的指针指向数据起始处。delete[] 就靠这个长度信息,循环调用每个元素的析构函数,再释放整块内存。
而 delete 完全不看这个隐藏长度——它只当那是单个对象,仅调用第一个元素的析构函数,接着把整个内存块交还给堆管理器。其余 9 个 MyClass 对象的析构函数根本不会执行。
如果类里有 new 出来的资源(如 int*、文件句柄、锁等),这些就彻底泄漏了。现象通常是:程序跑着跑着变慢、内存占用持续上涨、Valgrind 报 mismatched free() / delete / delete[]。
- 对
int、double等内置类型,没析构函数,可能“看起来”没事——但这仍是未定义行为,换编译器或优化等级就可能崩 - 对自定义类,只要析构函数里有资源清理逻辑,混用基本等于埋雷
- 现代编译器(如 GCC 12+、Clang 14+)在 Debug 模式下有时会检测并 abort,但 Release 下通常静默失败
delete[] 释放 new 分配的单个对象会读错内存地址
delete[] 一上来就会尝试从指针地址往前偏移几个字节,去读那个“本该存在”的数组长度。但 new 分配的单个对象前面根本没有这玩意儿——它读到的可能是栈变量、其他堆块的元数据,甚至越界内容。
立即学习“C++免费学习笔记(深入)”;
结果就是:用一个垃圾数字当作数组长度,疯狂调用析构函数(比如调 0xCAFEBABE 次),最后把一个错误地址传给底层 _ZdaPv 去释放。几乎必然触发段错误(SIGSEGV)或堆损坏(heap corruption)。
- 这种错误往往当场崩溃,比漏析构更易发现,但也更难调试——堆栈可能已破坏,gdb 显示无效帧
- 即使没崩,也可能悄悄改写相邻内存,导致后续某个完全无关的模块出错,排查成本极高
- ASan(AddressSanitizer)能捕获这类越界读,建议开发期默认开启
汇编层面它们调用的是完全不同的 operator 函数
你写的 delete ptr 和 delete[] ptr,最终展开为不同符号的库函数调用:
-
delete→ 调用_ZdlPv(operator delete(void*)) -
delete[]→ 调用_ZdaPv(operator delete[](void*))
这两个函数实现逻辑不同:_ZdaPv 会尝试回溯找数组头,_ZdlPv 则直接释放传入地址。链接器不会警告你传错了参数,因为签名都是 void* ——错误只在运行时暴露。
这也是为什么编译器从不报错:语法合法,只是语义错配。C++ 把责任完全交给了程序员。
std::vector 和 std::unique_ptr 是更安全的替代方案
手写 new[]/delete[] 的唯一合理场景,是极少数性能敏感且必须绕过容器开销的底层代码。其余情况,应该用 RAII 容器接管生命周期:
- 动态数组优先用
std::vector<T>:自动管理内存、支持移动、异常安全 - 需要裸指针语义时,用
std::unique_ptr<T[]>:构造时用new T[N],析构自动调用delete[],不会漏也不会错 - 绝对不要用
std::unique_ptr<T>去管理new T[N]—— 它内部调用的是delete,不是delete[]
真正容易被忽略的点是:即使你记得配对,也很难保证团队里所有人、所有代码路径(尤其是异常分支)都严格遵守。RAII 不依赖人工记忆,它靠类型系统强制约束。


















