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

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

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

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

cpp 复制代码
// 反例,不要这么写:用 free 释放 new 出来的对象(违反 R.10 资源管理应交给 RAII)
Resource* p = new Resource();   // 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*。

cpp 复制代码
#include <iostream>
struct Resource {
    Resource() { std::cout << "Resource 构造:分配资源\n"; }
    ~Resource() { std::cout << "Resource 析构:释放资源\n"; }
};
int main() {
    std::cout << "进入作用域\n";
    Resource* p = new Resource();   // 分配内存 + 调用构造
    delete p;                        // 调用析构 + 释放内存
    std::cout << "离开作用域\n";
}
text 复制代码
进入作用域
Resource 构造:分配资源
Resource 析构:释放资源
离开作用域

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

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

cpp 复制代码
#include <cstdlib>
#include <iostream>
#include <new>
struct Resource {
    Resource() { std::cout << "Resource 构造:分配资源\n"; }
    ~Resource() { std::cout << "Resource 析构:释放资源\n"; }
};
int main() {
    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";
}
text 复制代码
malloc 只拿内存,不调构造
Resource 构造:分配资源
Resource 析构:释放资源
free 只释放内存,不调析构

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

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

维度 new / delete malloc / 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→未定义行为
cpp 复制代码
// 反例,不要这么写:用 delete 释放 malloc 出来的内存(违反 ES.48 不用 C 风格转换也更不该混用)
void* raw = std::malloc(sizeof(Resource));   // 没有构造任何对象
Resource* p = static_cast<Resource*>(raw);
delete p;   // 对一块从未构造的内存调析构函数 -> 未定义行为

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

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

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

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

cpp 复制代码
#include <iostream>
struct Item {
    int id;
    Item(int i) : id(i) { std::cout << "Item#" << id << " 构造\n"; }
    ~Item() { std::cout << "Item#" << id << " 析构\n"; }
};
int main() {
    Item* arr = new Item[3]{1, 2, 3};   // 调 3 次构造
    std::cout << "--- 准备释放 ---\n";
    delete[] arr;                        // 调 3 次析构(逆序)
}
text 复制代码
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):

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

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

cpp 复制代码
// 片段(演示重载点):全局 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:

cpp 复制代码
#include <iostream>
#include <memory>
struct Resource {
    Resource() { std::cout << "Resource 构造\n"; }
    ~Resource() { std::cout << "Resource 析构\n"; }
};
int main() {
    std::cout << "[裸 new/delete,需手动配对]\n";
    Resource* p = new Resource();
    delete p;                        // 忘了 delete -> 泄漏;用 free -> 灾难

    std::cout << "[std::make_unique,出作用域自动析构]\n";
    auto up = std::make_unique<Resource>();   // 同样调构造/析构,但无需手写 delete
}  // up 在此自动析构并释放
text 复制代码
[裸 new/delete,需手动配对]
Resource 构造
Resource 析构
[std::make_unique,出作用域自动析构]
Resource 构造
Resource 析构

8. 延伸阅读

9. 一句话总结

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

相关推荐
weixin_3077791315 小时前
C++代码实现MATLAB中的fitrtree函数功能
开发语言·c++·算法·matlab
AI wins15 小时前
c++新增頭文件
c++
Shadow(⊙o⊙)16 小时前
仿muduo实现高并发服务器——事件就绪管理Channel.hpp
服务器·c++
by2099916 小时前
哈希表——快速找到数据:冲突、删除与扩容的实现
数据结构·c++·哈希算法·散列表
音草17 小时前
C++ 继承详解:从概念到菱形继承
c++
weixin_3077791317 小时前
C++代码实现MATLAB中的spectralcluster函数功能
开发语言·c++·算法·matlab
xiaoye-duck18 小时前
《C++ 项目:手写大模型接入 SDK》Trae IDE 开发环境搭建与依赖管理
c++·ide·语言模型
沫璃染墨18 小时前
《Qt从零入门系列(十四):QThread多线程详解——从线程创建到客户端阻塞处理》
开发语言·c++·笔记·qt·ui·交互·线程
草莓熊Lotso18 小时前
【C++ 基于微服务的即时通讯系统】Elasticsearch 从安装到 C++ 客户端二次封装全落地
linux·网络·数据库·c++·elasticsearch·微服务
CHPCWWHSU18 小时前
FreeCAD 源码解读:从 Property 到 Command,看一套 CAD 内核的完整闭环
c++·cad