ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

new/delete 与 malloc/free:为什么绝对不能混用

new/delete 与 malloc/free:为什么绝对不能混用 很多 C 新手把new/delete当成「C 版的 malloc/free」来用结果在析构不执行、资源泄漏、甚至程序崩溃里打转。本质区别在于new不只是申请内存它还调用构造函数delete不只是释放内存它还调用析构函数。而malloc/free只认字节、不认对象。一旦把两边混着用等于让「管生命周期的」和「只管内存的」两套机制互相越权——后果是未定义行为undefined behavior。本文用实跑代码把这条红线画清楚。1. 引子一个看似无害的替换下面这段「只是想把内存申请换成 C 风格」的代码藏着大坑// 反例不要这么写用 free 释放 new 出来的对象违反 R.10 资源管理应交给 RAIIResource*pnewResource();// new 调用了构造函数std::free(p);// free 只释放内存不调用析构函数 - 资源泄漏且混用分配器属 UBResource的析构函数里哪怕只是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*。#includeiostreamstructResource{Resource(){std::coutResource 构造分配资源\n;}~Resource(){std::coutResource 析构释放资源\n;}};intmain(){std::cout进入作用域\n;Resource*pnewResource();// 分配内存 调用构造deletep;// 调用析构 释放内存std::cout离开作用域\n;}进入作用域 Resource 构造分配资源 Resource 析构释放资源 离开作用域官方文档new 表达式 · delete 表达式再看malloc那条路——它只给内存构造和析构都得你手动补上#includecstdlib#includeiostream#includenewstructResource{Resource(){std::coutResource 构造分配资源\n;}~Resource(){std::coutResource 析构释放资源\n;}};intmain(){std::coutmalloc 只拿内存不调构造\n;void*rawstd::malloc(sizeof(Resource));// 只分配不构造Resource*pstatic_castResource*(raw);// 必须转型malloc 返回 void*new(p)Resource();// 必须显式调构造placement newp-~Resource();// 必须显式调析构std::free(raw);// 再释放内存std::coutfree 只释放内存不调析构\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*rawstd::malloc(sizeof(Resource));// 没有构造任何对象Resource*pstatic_castResource*(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[]会精确调三次且逆序#includeiostreamstructItem{intid;Item(inti):id(i){std::coutItem#id 构造\n;}~Item(){std::coutItem#id 析构\n;}};intmain(){Item*arrnewItem[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)#includeiostream#includenewintmain(){int*pnew(std::nothrow)int(42);if(p){std::coutnothrow 分配成功值*p\n;deletep;}else{std::cout分配失败返回 nullptr不抛异常\n;}}nothrow 分配成功值42operator 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::nothrow6. 现代写法把「手动配对」交给 RAII讲这么多混用危害最根本的解法是根本不手写new/delete。C Core Guidelines 的基调就是 RAII 优先让资源的生命周期绑在对象上出作用域自动释放自然杜绝「忘了 delete」「用错 delete」「混用 free」这一整类错误。过去的写法现代写法好处T* p new T;…delete p;auto p std::make_uniqueT();出作用域自动析构不可能泄漏T* a new T[n];…delete[] a;std::vectorT a(n);连数组 cookie 都不用操心malloc/free管理缓冲std::vectorstd::byte/std::string类型安全、自动扩容7. 完整示例裸写法 vs 智能指针把「手动配对」和「RAII 自动配对」放在一个程序里对照效果完全一致但后者无需手写delete#includeiostream#includememorystructResource{Resource(){std::coutResource 构造\n;}~Resource(){std::coutResource 析构\n;}};intmain(){std::cout[裸 new/delete需手动配对]\n;Resource*pnewResource();deletep;// 忘了 delete - 泄漏用 free - 灾难std::cout[std::make_unique出作用域自动析构]\n;autoupstd::make_uniqueResource();// 同样调构造/析构但无需手写 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 managementnew/delete与malloc/free混用的官方警告。9. 一句话总结new/delete是「内存 构造/析构」一体而malloc/free只搬字节、不认对象两者严格成对、绝不交叉否则就是未定义行为——现代 C 的正确做法是把这对操作交给std::make_unique/std::vector等 RAII 容器从根上避免手动配对的全部坑。
返回列表