Appearance
malloc / new / delete / free 全面对比与底层
一、malloc vs new
| 维度 | malloc/free | new/delete |
|---|---|---|
| 语言 | C 库函数 | C++ 运算符/关键字(不可重载调用形式上的全局但可替换) |
| 类型 | 返回 void*,需强转 | 返回指定类型指针,类型安全 |
| 大小 | 需手动给字节数 | 由编译器按类型推导 + 自动算数组大小 |
| 构造/析构 | 只分配/释放内存,不调用构造与析构 | 分配后调用构造函数,释放前调用析构函数 ← 核心 |
| 失败 | 返回 NULL | 默认抛 std::bad_alloc(或走 new_handler) |
| 数组 | 手动算 n*sizeof | new 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)实现:
- 大块(> MMAP_THRESHOLD,默认 128KB) → 直接用 mmap 系统调用分配一段匿名内存(归还时 munmap,易回收、避免堆碎片)。
- 小块 → 通过 brk/sbrk 扩展数据段(堆)顶部,交由用户态分配器管理。
- 用户态分配器维护 free lists(空闲块链表,按大小分类 bins:fastbins/smallbins/largebins/unsorted),malloc 在 bins 里找合适空闲块;free 把块放回 bins(可合并相邻空闲块减少外碎片)。
- 首次分配 / 空间不足时才调 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 计数分配器 + 报告未释放」的常用方法。
七、经典追问
new[]额外存长度会不会导致返回的指针与我们以为的起始不同?—— 是,可能把 cookie 放前面;这就是为何同一指针必须配 delete[],不能混用 delete/delete[]/free。- placement new 什么情况用?—— 内存池里预分配好内存再就地构造;容器实现里的
::operator new + new(ptr)T。 - malloc 释放时怎么知道块大小?—— free 从传入指针找前面头部的 size 字段。
- 为什么说 malloc 不一定真的调用系统调用?—— 首次分配到够用则复用 bins 内空闲块,只有不足才扩展。