☰
new/delete 与 malloc/free:为什么绝对不能混用
2026/9/30 2:37:41 网站建设 项目流程

很多 C++ 新手把new/delete当成「C++ 版的 malloc/free」来用,结果在析构不执行、资源泄漏、甚至程序崩溃里打转。本质区别在于:new不只是申请内存,它还调用构造函数;delete不只是释放内存,它还调用析构函数。而malloc/free只认字节、不认对象。一旦把两边混着用,等于让「管生命周期的」和「只管内存的」两套机制互相越权——后果是未定义行为(undefined behavior)。本文用实跑代码把这条红线画清楚。

1. 引子:一个看似无害的替换

下面这段「只是想把内存申请换成 C 风格」的代码,藏着大坑:

// 反例,不要这么写:用 free 释放 new 出来的对象(违反 R.10 资源管理应交给 RAII)Resource*p=newResource();// new 调用了构造函数std::free(p);// free 只释放内存,不调用析构函数 -> 资源泄漏,且混用分配器属 UB

Resource的析构函数里哪怕只是std::fclose一个文件、或delete一个成员指针,这一行std::free都会让它们永远不被执行。更糟的是,new和malloc可能来自不同的内存分配器,跨分配器释放本身就是未定义行为。先记住结论:new配delete、malloc配free,严格成对,绝不交叉。

2. 核心区别:new 比 malloc 多做了一件事

new T干了两件事:向系统要sizeof(T)字节、然后在这块内存上构造一个T。delete p反过来:先析构*p、再归还内存。malloc/free完全不碰构造/析构,它连「这是什么类型」都不知道,返回的是void*。

#include<iostream>structResource{Resource(){std::cout<<"Resource 构造:分配资源\n";}~Resource(){std::cout<<"Resource 析构:释放资源\n";}};intmain(){std::cout<<"进入作用域\n";Resource*p=newResource();// 分配内存 + 调用构造deletep;// 调用析构 + 释放内存std::cout<<"离开作用域\n";}
进入作用域 Resource 构造:分配资源 Resource 析构:释放资源 离开作用域

官方文档:new 表达式 · delete 表达式

再看malloc那条路——它只给内存,构造和析构都得你手动补上:

#include<cstdlib>#include<iostream>#include<new>structResource{Resource(){std::cout<<"Resource 构造:分配资源\n";}~Resource(){std::cout<<"Resource 析构:释放资源\n";}};intmain(){std::cout<<"malloc 只拿内存,不调构造\n";void*raw=std::malloc(sizeof(Resource));// 只分配,不构造Resource*p=static_cast<Resource*>(raw);// 必须转型,malloc 返回 void*new(p)Resource();// 必须显式调构造(placement new)p->~Resource();// 必须显式调析构std::free(raw);// 再释放内存std::cout<<"free 只释放内存,不调析构\n";}
malloc 只拿内存,不调构造 Resource 构造:分配资源 Resource 析构:释放资源 free 只释放内存,不调析构

注意这里两个「必须手动」:①std::malloc返回void*,要static_cast才能当Resource*用(类型不安全,C++ 里应尽量避免 C 风格转换);② 构造和析构都得自己写。而new/delete把这两步打包好了。

把六个维度摆在一起对比:

维度new/deletemalloc/free
是否调构造/析构是(对象语义)否(只搬字节)
返回类型具体类型指针(类型安全)void*(需手动转型)
大小计算编译器按sizeof(T)算手动sizeof,极易算错
失败行为抛std::bad_alloc异常返回NULL(需判空)
数组形式new[]/delete[]无,需手算N * sizeof(T)
可否重载可(operator new/operator delete)不可

new失败抛异常、而非返回空,是个关键差异:用new后不必像malloc那样每次判空,但要用try/catch接异常(按引用捕)。

3. 混用的灾难:为什么是未定义行为

把两套机制交叉,编译器不会拦你,运行时的结果却无法预测。本质原因:new申请的内存归 C++ 分配器管,malloc申请的内存归 C 库分配器管,二者的元数据/对齐约定未必一致;且delete会去调析构、free不会,职责错位就出事。

错误写法后果
std::free(new T)析构不执行→资源泄漏;且跨分配器释放,未定义行为
delete(指向malloc内存的指针)对未构造对象调析构→未定义行为
delete(指向new[]的指针)数组 cookie 错位→未定义行为
delete[](指向new的指针)多读一次不存在的 cookie→未定义行为
// 反例,不要这么写:用 delete 释放 malloc 出来的内存(违反 ES.48 不用 C 风格转换也更不该混用)void*raw=std::malloc(sizeof(Resource));// 没有构造任何对象Resource*p=static_cast<Resource*>(raw);deletep;// 对一块从未构造的内存调析构函数 -> 未定义行为

官方文档:C++ Core Guidelines R.10:若必须申请内存,立即交给能管理它的对象(如智能指针)。

4. new[] 为什么要配 delete[]:数组 cookie

申请数组时,new T[N]不只要N * sizeof(T)字节,通常还会在前面多存一个cookie(元素个数),记下「要调几次析构」。delete[]读这个 cookie,按逆序对N个元素逐个调析构;若误用delete(不带[]),只会调一次析构、其余N-1个对象泄漏,且 cookie 读错位——又是未定义行为。

new T[3] 实际内存布局(概念图,cookie 来自实现细节,标准未规定但普遍存在) ┌──────────┬───────┬───────┬───────┐ │ cookie │ T[0] │ T[1] │ T[2] │ │ (元素个数)│ │ │ │ └────┬─────┴───────┴───────┴───────┘ │ delete[] 读 cookie,决定调几次析构 ▼ T[2].~T() -> T[1].~T() -> T[0].~T() (逆序,与构造相反)

实跑验证——三个带析构的对象,delete[]会精确调三次,且逆序:

#include<iostream>structItem{intid;Item(inti):id(i){std::cout<<"Item#"<<id<<" 构造\n";}~Item(){std::cout<<"Item#"<<id<<" 析构\n";}};intmain(){Item*arr=newItem[3]{1,2,3};// 调 3 次构造std::cout<<"--- 准备释放 ---\n";delete[]arr;// 调 3 次析构(逆序)}
Item#1 构造 Item#2 构造 Item#3 构造 --- 准备释放 --- Item#3 析构 Item#2 析构 Item#1 析构

官方文档:array new 与数组析构

5. nothrow 版本与 operator new 重载

new默认失败抛std::bad_alloc。如果你不想用异常,可以要一份「失败返回空」的版本——new (std::nothrow):

#include<iostream>#include<new>intmain(){int*p=new(std::nothrow)int(42);if(p){std::cout<<"nothrow 分配成功,值="<<*p<<'\n';deletep;}else{std::cout<<"分配失败(返回 nullptr,不抛异常)\n";}}
nothrow 分配成功,值=42

operator new/operator delete是可以重载的,常用于自定义内存池、统计分配次数、或接入调试分配器。new表达式最终会调用operator new(size),所以重载它就改变了「内存从哪来」:

// 片段(演示重载点):全局 operator new 可被替换,new 表达式会转发到它// void* operator new(std::size_t n) {// std::printf("申请 %zu 字节\n", n);// return std::malloc(n);// }// void operator delete(void* p) noexcept { std::free(p); }

官方文档:operator new · std::nothrow

6. 现代写法:把「手动配对」交给 RAII

讲这么多混用危害,最根本的解法是根本不手写new/delete。C++ Core Guidelines 的基调就是 RAII 优先:让资源的生命周期绑在对象上,出作用域自动释放,自然杜绝「忘了 delete」「用错 delete」「混用 free」这一整类错误。

过去的写法现代写法好处
T* p = new T;…delete p;auto p = std::make_unique<T>();出作用域自动析构,不可能泄漏
T* a = new T[n];…delete[] a;std::vector<T> a(n);连数组 cookie 都不用操心
malloc/free管理缓冲std::vector<std::byte>/std::string类型安全、自动扩容

7. 完整示例:裸写法 vs 智能指针

把「手动配对」和「RAII 自动配对」放在一个程序里对照,效果完全一致,但后者无需手写delete:

#include<iostream>#include<memory>structResource{Resource(){std::cout<<"Resource 构造\n";}~Resource(){std::cout<<"Resource 析构\n";}};intmain(){std::cout<<"[裸 new/delete,需手动配对]\n";Resource*p=newResource();deletep;// 忘了 delete -> 泄漏;用 free -> 灾难std::cout<<"[std::make_unique,出作用域自动析构]\n";autoup=std::make_unique<Resource>();// 同样调构造/析构,但无需手写 delete}// up 在此自动析构并释放
[裸 new/delete,需手动配对] Resource 构造 Resource 析构 [std::make_unique,出作用域自动析构] Resource 构造 Resource 析构

8. 延伸阅读

  • cppreference · new 表达式:最权威的new语义与数组形式说明。
  • cppreference · operator new:重载点、nothrow 版本、对齐分配。
  • C++ Core Guidelines · R.10/R.11:为什么资源要立刻交给管理对象。
  • isocpp.org FAQ · Freestore management:new/delete与malloc/free混用的官方警告。

9. 一句话总结

new/delete是「内存 + 构造/析构」一体,而malloc/free只搬字节、不认对象;两者严格成对、绝不交叉,否则就是未定义行为——现代 C++ 的正确做法是把这对操作交给std::make_unique/std::vector等 RAII 容器,从根上避免手动配对的全部坑。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询