C++ new
C++ 中 new 是如何工作的呢?
创建对象数组示例
以创建一个对象数组举例:
1 |
|
arr 拿到的地址是 0x000055555556b2b8,指向的是第一个对象。
内存布局

可以看到小端序存储的五个 value。这就是用户通过调用 new 拿到的指针,但这并不是 new 申请的全部内存。
Cookie 的原理
向前看有一个 8 字节的 cookie 值:0x5。
GCC 判断是否需要 Cookie
GCC 通过如下代码判断是否需要 cookie(gcc/cp/class.cc 第 6219-6266 行),如果有非平凡析构函数,就需要 cookie。
1 | static bool |
Cookie 的大小计算
cookie 的大小通过如下代码计算:
1 | // cookie_size = max(sizeof(size_t), alignof(type)) |
cookie 的值就是对象的数目,例子中就是 5。
汇编代码分析
例子中 Test *arr = new Test[5]{1, 2, 3, 4, 5}; 生成的汇编如下:
1 | mov edi,0x1c // 参数: 0x1c = 28 字节 (8 cookie + 5*4 数据) |
深入 operator new[]
这个流程基本清楚了,我们看看 operator new[] 里面到底做了什么。
operator new[] 源码
源码如下,直接调用 new,这里没有任何区别:
1 | // libstdc++-v3\libsupc++\new_opv.cc |
operator new 源码
new 源码如下:
1 | // libstdc++-v3\libsupc++\new_op.cc |
Malloc 的实现
到这里 new 就结束了,下一步是查看 malloc。
Windows vs Linux 的差异
glibc 的 malloc 和 ucrt 不一样,它是直接就进入到了内存分配的逻辑了,ucrt 是又包装了一层,然后调用的 HeapAlloc,在 HeapAlloc 中进行实际的内存分配,如下:
1 | extern "C" __declspec(noinline) _CRTRESTRICT void* __cdecl _malloc_base(size_t const size) |
Linux 堆管理器
说回 glibc 的 malloc,它的功能就和 HeapAlloc 基本一样了,就是一个堆管理器。
它们的逻辑大体都是根据不同的 size 阈值,去 tls 缓存、堆的前端、后端拿内存,堆不够就 sbrk 拓展,大内存就 mmap / RtlpHpLargeAlloc。正是因为有堆管理器,使得绝大部分的堆分配都不需要进内核,只需要后端批发内存,切分给前端,供调用者使用。
Linux 内存分配流程图
Windows 内存分配流程
Windows 上大概的流程是:LFH 低碎片堆 -> VS Allocator -> 后端 RtlpHpAllocateHeapBackend -> RtlpHpLargeAlloc 分配大页,降低 TLB 消耗。
然后进入内核,分配一个 VAD,插入这个进程的 EPROCESS。分配到这里就结束了,但是还没有构建有效 pte。真正的物理内存分配被推迟到了内存访问的时候,触发缺页异常,拿到异常信息后去 vad 中寻找该地址,如果已经注册,那么就去零页(堆 / VirtualAlloc 初始化的时候使用)/ 空闲页的 pfn 链表中拿一个页面,挂在这个虚拟地址上。
Malloc 的 Chunk 结构
我们再来看一下 glibc 中添加的头信息,它和 new[] 一样,也会加一个头,因为 free 的时候不传入大小,它肯定有地方保存申请的大小。
malloc 申请的 chunk 结构如下:
1 | struct malloc_chunk { |
Chunk 字段详解
这两个字段就是 header:
mchunk_prev_size (8 字节)
- 前一个
chunk空闲时:存储前一个chunk的大小,用于向前合并 - 前一个
chunk在用时:这块空间被前一个chunk当用户数据用
mchunk_size (8 字节)
- 高位:当前
chunk的总大小 - 低 3 位:三个标志位
标志位说明
三个标志位:
P(PREV_INUSE=0x1):前一个相邻chunk正在使用中P=1:前一个chunk在用,prev_size字段无效P=0:前一个chunk空闲,prev_size存储其大小,可用于合并
M(IS_MMAPPED=0x2):这个chunk是通过mmap()分配的M=1:mmap分配,释放时直接munmap()M=0:sbrk/heap分配
A(NON_MAIN_ARENA=0x4):这个chunk不属于主arenaA=1:来自非主arena(多线程场景)A=0:来自main_arena
后面的这些字段只在 chunk 空闲时使用,用于把空闲块串成链表,当这个 chunk 被使用的时候,这些字段会被用户数据覆盖:
fd/bk(所有空闲chunk都用)fd=forward→ 指向链表中下一个空闲chunkbk=backward→ 指向链表中上一个空闲chunk- 用于
smallbin、unsorted bin、large bin的双向循环链表。
fd_nextsize/bk_nextsize(仅large bin用)fd_nextsize→ 指向下一个不同大小的空闲chunkbk_nextsize→ 指向上一个不同大小的空闲chunk
完整内存布局示例
再看之前的例子:

根据上面的学习,就可以很清晰地分析出它的内存布局:
1 | chunk ──→ ┌─────────────────┐ +0x00 |
Delete[] 的配对问题
同样的 delete[],就会读取 cookie,调整指针,调用多次析构函数再调用 free,free 也会读取头信息,决定释放多大的内存。
不配对的后果
当 new[]、delete[] 不配对的时候,会出现如下情况:
new[]配delete:只调用一次析构函数,并且不调整指针(它多申请了cookie,调用free的时候要移动回去),会出现未定义行为(页面回收了就segment fault,没回收就堆损坏等)。new配delete[]:读取p-8位置的垃圾值当作元素个数n,尝试调用n次析构函数(可能是天文数字导致越界访问),然后调用free(p-8)传入错误地址,会出现未定义行为(段错误、堆损坏、无限循环调用析构等)。
为什么有时能正常工作
那么为什么有时候 new[]、delete[] 不配对,可以正常工作呢?
之前提到,类有非平凡析构函数时,才会生成 cookie,所以如果是 new int[10],或者使用默认析构函数的时候,即使使用 new[],也不会生成 cookie,这样 delete 也不会报错了。
但是即使能碰巧工作,在编译器层面属于未定义行为,必须避免出现。




