从 malloc/free 到 new/delete,彻底理解 C/C++ 动态内存管理

动态内存管理几乎是每个 C/C++ 学习者都会反复碰到的话题。

刚开始学的时候,最容易记住的是几个表面事实:C 里常用 malloc/free,C++ 里常用 new/delete。但如果只停在这一层,后面一旦碰到对象构造、析构、异常安全、堆分配器、虚拟内存、缺页中断这些概念,就会越来越乱。

这篇文章想做的事情,就是把这些知识点串起来,从最基础的身份差异开始,一路讲到 malloc 和操作系统之间的关系,再回头看 new/delete 为什么是 C++ 面向对象语义下更自然的选择。

malloc/free 和 new/delete 的身份到底有什么不同

先从最根本的地方说起。

mallocfreeC 语言标准库函数。你只要包含头文件,就可以像使用 printfstrlen 一样使用它们。它们做的事情非常纯粹,就是申请和释放一段原始内存。

1
2
void* p = malloc(100);  // 申请 100 个字节
free(p); // 释放这块内存

这里最关键的一点是,malloc 返回的是 void*,也就是“无类型指针”。它只知道自己给了你一块内存,但不知道你打算在里面放什么数据,更不知道你是否要把它当成一个对象来使用。

newdelete 不一样,它们是 C++ 语言级别的运算符。这意味着它们不只是“申请一块内存”这么简单,而是和对象模型直接绑定的。

1
2
int* p = new int(42);
delete p;

这时你拿到的不是一个模糊的 void*,而是一个已经完成初始化的、类型安全的 int*

所以,二者的核心区别可以先记成一句话:

malloc/free 管的是字节,new/delete 管的是对象。

这是后面一切差异的起点。

为什么说 new/delete 管的是对象

C 语言的世界里,内存本质上就是一片连续的字节区域。至于这块字节里存的是整数、数组还是结构体,更多要靠程序员自己解释。

但 C++ 不只是“带类的 C”。C++ 引入了构造函数、析构函数、封装、继承、多态,也就是说,一个对象在“被创建”时往往需要初始化,在“被销毁”时往往需要清理资源。

例如一个对象内部可能管理着:

  • 文件句柄
  • 互斥锁
  • socket 连接
  • 动态申请的缓冲区
  • 其他资源句柄

如果只是单纯拿到一块内存,这个对象其实并没有真正“活起来”。

new 的意义就在这里:它不只是拿到一块足够大的空间,还会在这块空间上调用构造函数,让对象进入可用状态。

delete 则在释放内存之前,先调用析构函数,让对象完成资源清理。

所以从语言语义上说:

  • malloc 只负责“给空间”
  • new 负责“给空间,并构造对象”
  • free 只负责“还空间”
  • delete 负责“析构对象,并释放空间”

这就是为什么在 C++ 中,new/deletemalloc/free 更符合语言设计。

new 的内部到底做了什么

虽然 new T() 看起来只是一行代码,但编译器背后其实帮你做了三件事。

第一步,分配内存

编译器会先调用 operator new(sizeof(T)),申请一块足够容纳对象 T 的原始内存。

这一步的目标和 malloc 很像,都是“先拿空间”。默认实现中,operator new 底层通常也会依赖堆分配器,很多实现最终会走到类似 malloc 的分配路径。

第二步,调用构造函数

拿到原始内存之后,编译器会在这块地址上调用 T 的构造函数。到这一步,对象才真正被建立起来。

如果构造函数里完成了成员初始化、资源申请、状态设置,那么对象此时才算处于“可以安全使用”的状态。

第三步,返回类型安全指针

构造成功后,这块内存的地址会被解释为 T* 返回给你。于是你拿到的是一个可以直接访问成员和方法的对象指针。

换句话说,new expression 可以简单理解成:

分配内存 + 构造对象 + 返回指针

delete 的顺序正好相反:

调用析构函数 + 释放内存

这也是为什么在 C++ 里千万不要混用:

  • mallocdelete
  • newfree

因为一旦混用,就会破坏这套“对象生命周期 + 内存释放”的完整语义,轻则资源泄漏,重则直接崩溃。

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 小块分配与操作系统交互流程图

malloc 底层如何向操作系统申请内存

当分配器发现自己手头的内存不够时,才会进一步向操作系统申请。

在 Linux 环境下,常见路径通常有两类:

1. 通过 brk/sbrk 扩展堆

对于较小、较常规的堆内存申请,分配器可能会扩展进程的 heap 区域。

2. 通过 mmap 映射独立区域

对于较大的内存申请,分配器往往更倾向于使用 mmap,单独映射一块虚拟地址区间。

这里要注意,文章里常说“malloc 底层会用 brk 或 mmap”,这是对整体机制的概括,不是说你每写一行 malloc,就会在源代码级别直接对应一次 brk()mmap()

真正中间还隔着一层用户态堆分配器,它会负责:

  • 空闲块管理
  • 内存切分与合并
  • 小块复用
  • 大块直接映射
  • 多线程场景下的并发分配策略

所以更准确的表达应该是:

malloc 先与用户态分配器打交道,分配器在必要时再通过 brk/mmap 等机制向内核要更多地址空间。

malloc 成功返回了,为什么物理内存可能还没真正分配

这就引出了一个非常重要的概念:惰性分配

在 Linux 的虚拟内存模型里,程序看到的是虚拟地址,不是物理地址。

也就是说,malloc 返回给你的其实首先是一段“可以被使用的虚拟地址区间”。这时候内核可能只是修改了进程的虚拟内存区域描述,并没有马上给这块地址分配真实的物理页。

只有当你第一次真正去访问这块内存时,CPU 才会通过 MMU 做地址翻译。如果发现对应页表项还没有建立有效映射,就会触发 缺页异常。此时内核接管处理流程,分配物理页,建立映射,刷新 TLB,然后程序继续运行。

所以从时间线上看,更准确的流程是:

  1. malloc 返回一个可用的虚拟地址
  2. 程序第一次访问这块地址
  3. CPU 发现当前页没有有效映射
  4. 触发缺页异常
  5. 内核分配物理页并更新页表
  6. 程序恢复执行

你给的第二张图,非常适合拿来解释这个过程:

首次访问内存时的缺页处理时序图

再回头看 malloc/free 与 new/delete

如果把全文串起来,你会发现这几组概念其实并不是互相割裂的。

在 C 的世界里

  • malloc/free 负责原始内存申请与释放
  • 标准库分配器负责提高效率
  • 操作系统负责虚拟内存与物理页管理

在 C++ 的世界里

  • operator new 负责拿到原始内存
  • 构造函数负责让对象真正建立起来
  • 析构函数负责对象资源清理
  • operator delete 负责释放底层内存

所以二者并不是“谁更高级谁更低级”的关系,而是:

  • malloc/free 更接近“字节级内存管理”
  • new/delete 更接近“对象级生命周期管理”

总结

把这篇文章压缩成几个最关键的结论,大概就是下面这些:

  1. malloc/free 是 C 标准库函数,管理的是原始字节内存
  2. new/delete 是 C++ 运算符,管理的是对象的创建和销毁
  3. new 不只是分配内存,还会调用构造函数
  4. delete 不只是释放内存,还会调用析构函数
  5. malloc 为了高效,通常不会每次都直接进入内核,而是先经过用户态分配器
  6. malloc 返回成功,不代表物理内存已经立刻分配完成
  7. 真正首次访问页面时,可能会通过缺页异常完成物理页映射

从 malloc/free 到 new/delete,彻底理解 C/C++ 动态内存管理
https://breaker505.github.io/2026/03/04/new-delete-and-malloc/
作者
爱发呆的鱼
发布于
2026年3月4日
许可协议