Skip to content

malloc / new / delete / free 全面对比与底层

一、malloc vs new

维度malloc/freenew/delete
语言C 库函数C++ 运算符/关键字(不可重载调用形式上的全局但可替换)
类型返回 void*,需强转返回指定类型指针,类型安全
大小需手动给字节数由编译器按类型推导 + 自动算数组大小
构造/析构只分配/释放内存,不调用构造与析构分配后调用构造函数,释放前调用析构函数 ← 核心
失败返回 NULL默认抛 std::bad_alloc(或走 new_handler)
数组手动算 n*sizeofnew T[n] / delete[],长度由内部记录
释放free 无需知对象类型需 delete(要求类型已知以调析构)
可重载可重载全局/类内 operator new/delete

一句话malloc 只给"一块内存",new 给"一个已构造好的对象";free 只是回收,delete 先析构再回收。

new 的内部:void* p = operator new(sizeof(T)); // 分配p = new(p) T(args...); // placement new 调用构造 失败时 operator new 抛 bad_alloc。 delete p; 内部:p->~T(); operator delete(p);

二、new 的三种形式

形式作用
new T(...)分配 + 构造
new (place) T(...) placement new在给定地址构造,不分配内存
new (nothrow) T(...)分配失败不抛异常,返回 NULL

placement new 常用于内存池、容器手写(::operator new + placement new)。

三、delete 与 delete[] 的区别(经典必考)

cpp
T* a = new T[10];
delete a;    // ❌ 错误:只把首元素当单对象析构,数组其它元素不析构 → UB/泄漏
delete[] a;  // ✅ 正确:逐个析构

为什么 delete[] 知道析构 n 个? 实现常在数组分配时、在返回首地址前存一个"元素个数"(如头部 cookie / 或借助 size 信息)。delete[] 读该计数逐个调用析构。

  • 内置类型(int 等)无析构时,delete 与 delete[] 常常"碰巧能跑",但仍是未定义行为,面试要强调:凡是 new[] 就必须 delete[],凡是 new 就必须 delete
  • 手写 String、容器类若持有数组,析构函数里必须 delete[]

四、free 与 delete / malloc 失败

  • malloc 失败返回 NULL(需判空)。
  • new 失败:默认 throw std::bad_alloc(多数现代平台基本不失败,因过量分配 overcommit)。想不抛:new(std::nothrow)
  • 自定义失败处理:std::set_new_handler 设置回调(循环释放内存重试)。

五、malloc 底层(brk / mmap / 内存池)

经典 ptmalloc(glibc)实现:

  1. 大块(> MMAP_THRESHOLD,默认 128KB) → 直接用 mmap 系统调用分配一段匿名内存(归还时 munmap,易回收、避免堆碎片)。
  2. 小块 → 通过 brk/sbrk 扩展数据段(堆)顶部,交由用户态分配器管理。
  3. 用户态分配器维护 free lists(空闲块链表,按大小分类 bins:fastbins/smallbins/largebins/unsorted),malloc 在 bins 里找合适空闲块;free 把块放回 bins(可合并相邻空闲块减少外碎片)。
  4. 首次分配 / 空间不足时才调 brk/mmap 扩堆。分配时返回给用户的块往往比请求多一小块(头部存 size/状态)。

malloc(1KB) vs malloc(1MB)

  • 1KB → 走堆 brk + 用户态 bins 缓冲,快、可复用。
  • 1MB> 阈值 → 大部分实现直接走 mmap(单独映射,free 时 munmap 归还内核)。两者都可;面试强调 <<阈值走堆、大块走 mmap>>。

内存碎片

  • 内部碎片:分配块比请求大一点(头部 cookie/对齐)。
  • 外部碎片:已释放零散小块无法凑成大块连续空间。
  • 解决:内存池 / jemalloc/tcmalloc / 对象池,及大块走 mmap。

六、手写/示意 operator new 替换

cpp
// 全局替换(演示思路,需在链接层控制)
void* operator new(std::size_t sz) {
    if (void* p = std::malloc(sz)) return p;
    throw std::bad_alloc();
}
void operator delete(void* p) noexcept { std::free(p); }

可延伸到「拦截/统计分配」实现内存泄漏检测(覆盖 new/delete/free 计数分配与释放对账)。这也正是手写「new/delete 计数分配器 + 报告未释放」的常用方法。

七、经典追问

  1. new[] 额外存长度会不会导致返回的指针与我们以为的起始不同?—— 是,可能把 cookie 放前面;这就是为何同一指针必须配 delete[],不能混用 delete/delete[]/free。
  2. placement new 什么情况用?—— 内存池里预分配好内存再就地构造;容器实现里的 ::operator new + new(ptr)T
  3. malloc 释放时怎么知道块大小?—— free 从传入指针找前面头部的 size 字段。
  4. 为什么说 malloc 不一定真的调用系统调用?—— 首次分配到够用则复用 bins 内空闲块,只有不足才扩展。

C++ 面试八股 · VitePress 版