从 malloc/free 到 new/delete,彻底理解 C/C++ 动态内存管理
动态内存管理几乎是每个 C/C++ 学习者都会反复碰到的话题。
刚开始学的时候,最容易记住的是几个表面事实:C 里常用 malloc/free,C++ 里常用 new/delete。但如果只停在这一层,后面一旦碰到对象构造、析构、异常安全、堆分配器、虚拟内存、缺页中断这些概念,就会越来越乱。
这篇文章想做的事情,就是把这些知识点串起来,从最基础的身份差异开始,一路讲到 malloc 和操作系统之间的关系,再回头看 new/delete 为什么是 C++ 面向对象语义下更自然的选择。
malloc/free 和 new/delete 的身份到底有什么不同
先从最根本的地方说起。
malloc 和 free 是 C 语言标准库函数。你只要包含头文件,就可以像使用 printf、strlen 一样使用它们。它们做的事情非常纯粹,就是申请和释放一段原始内存。
1 | |
这里最关键的一点是,malloc 返回的是 void*,也就是“无类型指针”。它只知道自己给了你一块内存,但不知道你打算在里面放什么数据,更不知道你是否要把它当成一个对象来使用。
而 new 和 delete 不一样,它们是 C++ 语言级别的运算符。这意味着它们不只是“申请一块内存”这么简单,而是和对象模型直接绑定的。
1 | |
这时你拿到的不是一个模糊的 void*,而是一个已经完成初始化的、类型安全的 int*。
所以,二者的核心区别可以先记成一句话:
malloc/free管的是字节,new/delete管的是对象。
这是后面一切差异的起点。
为什么说 new/delete 管的是对象
C 语言的世界里,内存本质上就是一片连续的字节区域。至于这块字节里存的是整数、数组还是结构体,更多要靠程序员自己解释。
但 C++ 不只是“带类的 C”。C++ 引入了构造函数、析构函数、封装、继承、多态,也就是说,一个对象在“被创建”时往往需要初始化,在“被销毁”时往往需要清理资源。
例如一个对象内部可能管理着:
- 文件句柄
- 互斥锁
- socket 连接
- 动态申请的缓冲区
- 其他资源句柄
如果只是单纯拿到一块内存,这个对象其实并没有真正“活起来”。
new 的意义就在这里:它不只是拿到一块足够大的空间,还会在这块空间上调用构造函数,让对象进入可用状态。
delete 则在释放内存之前,先调用析构函数,让对象完成资源清理。
所以从语言语义上说:
malloc只负责“给空间”new负责“给空间,并构造对象”free只负责“还空间”delete负责“析构对象,并释放空间”
这就是为什么在 C++ 中,new/delete 比 malloc/free 更符合语言设计。
new 的内部到底做了什么
虽然 new T() 看起来只是一行代码,但编译器背后其实帮你做了三件事。
第一步,分配内存
编译器会先调用 operator new(sizeof(T)),申请一块足够容纳对象 T 的原始内存。
这一步的目标和 malloc 很像,都是“先拿空间”。默认实现中,operator new 底层通常也会依赖堆分配器,很多实现最终会走到类似 malloc 的分配路径。
第二步,调用构造函数
拿到原始内存之后,编译器会在这块地址上调用 T 的构造函数。到这一步,对象才真正被建立起来。
如果构造函数里完成了成员初始化、资源申请、状态设置,那么对象此时才算处于“可以安全使用”的状态。
第三步,返回类型安全指针
构造成功后,这块内存的地址会被解释为 T* 返回给你。于是你拿到的是一个可以直接访问成员和方法的对象指针。
换句话说,new expression 可以简单理解成:
分配内存 + 构造对象 + 返回指针
而 delete 的顺序正好相反:
调用析构函数 + 释放内存
这也是为什么在 C++ 里千万不要混用:
malloc配deletenew配free
因为一旦混用,就会破坏这套“对象生命周期 + 内存释放”的完整语义,轻则资源泄漏,重则直接崩溃。
new/delete 比 malloc/free 多出来的价值是什么
很多时候,初学者会觉得:
“既然 new 底层最终也可能会走到堆分配器,那我是不是直接用 malloc 也差不多?”
从“拿空间”这件事上看,二者确实有相通之处。但从语言保证和工程安全的角度看,差别很大。
1. 类型安全
new 返回的是具体类型指针,malloc 返回的是 void*。
2. 自动调用构造与析构
new/delete 负责对象生命周期,malloc/free 完全不知道类和对象的存在。
3. 更符合异常安全模型
如果构造函数抛出异常,C++ 编译器会自动处理已经申请的那块内存,避免出现“构造失败但内存泄漏”的情况。
这也是 new 在 C++ 中被设计出来的根本原因之一,它不是为了“语法更好看”,而是为了让对象模型和内存管理真正结合起来。
malloc 为什么不可能每次都直接找操作系统
理解完 new/delete 之后,再回来看 malloc,事情就会清楚很多。
很多人第一次接触 malloc 时,会直觉地以为:
- 我调用一次
malloc - 操作系统就立刻分一块内存给我
- 我拿到地址开始使用
但真实情况不是这样。
如果每次申请几十字节、几百字节都进入一次系统调用,代价会非常高。系统调用意味着用户态和内核态切换,开销远比普通函数调用大得多。因此,标准库实现里通常会有一层 用户态内存分配器,先在用户空间管理一部分可复用内存,再按需切分给程序。
你可以把它理解成一个“内存池 + 分配器”的组合。
- 小块申请,优先从内存池中找
- 找到了,就直接返回
- 找不到,才向操作系统申请更大的区域
你给的第一张图,其实就很适合说明这个流程:

malloc 底层如何向操作系统申请内存
当分配器发现自己手头的内存不够时,才会进一步向操作系统申请。
在 Linux 环境下,常见路径通常有两类:
1. 通过 brk/sbrk 扩展堆
对于较小、较常规的堆内存申请,分配器可能会扩展进程的 heap 区域。
2. 通过 mmap 映射独立区域
对于较大的内存申请,分配器往往更倾向于使用 mmap,单独映射一块虚拟地址区间。
这里要注意,文章里常说“malloc 底层会用 brk 或 mmap”,这是对整体机制的概括,不是说你每写一行 malloc,就会在源代码级别直接对应一次 brk() 或 mmap()。
真正中间还隔着一层用户态堆分配器,它会负责:
- 空闲块管理
- 内存切分与合并
- 小块复用
- 大块直接映射
- 多线程场景下的并发分配策略
所以更准确的表达应该是:
malloc先与用户态分配器打交道,分配器在必要时再通过brk/mmap等机制向内核要更多地址空间。
malloc 成功返回了,为什么物理内存可能还没真正分配
这就引出了一个非常重要的概念:惰性分配。
在 Linux 的虚拟内存模型里,程序看到的是虚拟地址,不是物理地址。
也就是说,malloc 返回给你的其实首先是一段“可以被使用的虚拟地址区间”。这时候内核可能只是修改了进程的虚拟内存区域描述,并没有马上给这块地址分配真实的物理页。
只有当你第一次真正去访问这块内存时,CPU 才会通过 MMU 做地址翻译。如果发现对应页表项还没有建立有效映射,就会触发 缺页异常。此时内核接管处理流程,分配物理页,建立映射,刷新 TLB,然后程序继续运行。
所以从时间线上看,更准确的流程是:
malloc返回一个可用的虚拟地址- 程序第一次访问这块地址
- CPU 发现当前页没有有效映射
- 触发缺页异常
- 内核分配物理页并更新页表
- 程序恢复执行
你给的第二张图,非常适合拿来解释这个过程:

再回头看 malloc/free 与 new/delete
如果把全文串起来,你会发现这几组概念其实并不是互相割裂的。
在 C 的世界里
malloc/free负责原始内存申请与释放- 标准库分配器负责提高效率
- 操作系统负责虚拟内存与物理页管理
在 C++ 的世界里
operator new负责拿到原始内存- 构造函数负责让对象真正建立起来
- 析构函数负责对象资源清理
operator delete负责释放底层内存
所以二者并不是“谁更高级谁更低级”的关系,而是:
malloc/free更接近“字节级内存管理”new/delete更接近“对象级生命周期管理”
总结
把这篇文章压缩成几个最关键的结论,大概就是下面这些:
malloc/free是 C 标准库函数,管理的是原始字节内存new/delete是 C++ 运算符,管理的是对象的创建和销毁new不只是分配内存,还会调用构造函数delete不只是释放内存,还会调用析构函数malloc为了高效,通常不会每次都直接进入内核,而是先经过用户态分配器malloc返回成功,不代表物理内存已经立刻分配完成- 真正首次访问页面时,可能会通过缺页异常完成物理页映射