源码在
1 | C:\Program Files (x86)\Windows Kits\10\Source\10.0.26100.0\ucrt\heap |
malloc.cpp文件非常简洁:
1 | // |
可以看到就是调试debug模式与其他。
先看看release模式:进入到_malloc_base
mallocbase也很简洁:
1 | // |
感觉注释已经讲得很清楚了
分解一下看看,第一行代码是
1 | _VALIDATE_RETURN_NOEXC(_HEAP_MAXREQ >= size, ENOMEM, nullptr); |
这是一个宏,用来做参数检查。宏展开后逻辑大致类似:
1 | if (size > _HEAP_MAXREQ) |
也就是说,CRT 会先检查用户请求的大小是不是超过了运行时允许的最大分配尺寸。这个最大值 _HEAP_MAXREQ 通常接近 SIZE_MAX,但略小一点,因为堆管理器还需要在块前面放 header。如果请求太大,直接失败并设置 errno 为 ENOMEM。从内存角度看,这一步只是防止一个明显不可能完成的请求继续往下执行。
接下来是一个很小但很重要的处理:
1 | size_t const actual_size = size == 0 ? 1 : size; |
也就是说,如果用户调用
1 | malloc(0); |
CRT 实际会把请求改成至少分配 1 个字节。原因是 C 标准允许 malloc(0) 返回 nullptr 或返回一个可释放的指针,而 Windows 的实现选择第二种策略。这样做有两个好处:第一,调用者仍然可以把这个指针交给 free;第二,分配器的内部逻辑不需要处理“0 字节块”这种特殊情况。
到这里为止,函数只是把请求做了合法化处理。真正的分配发生在下面的循环里:
1 | for (;;) |
这里第一次出现了真正重要的函数:HeapAlloc。这就是 Windows 的用户态堆分配 API。__acrt_heap 是 CRT 在初始化时获取的进程默认堆句柄,通常来自 GetProcessHeap()。因此这行代码的真实含义其实是:到当前进程的堆里,找一块大小为 actual_size 的空闲内存块。
然后还检查了一次:
1 | // If the new handler fails, just return nullptr: |
这里出现了 C++ 运行时和 C 运行时之间的一个桥接机制。_query_new_mode() 用来查询当前 CRT 是否启用了new-handler 模式。在 C++ 里,如果 new 分配失败,可以触发一个用户注册的函数(通过 std::set_new_handler)。CRT提供了一种兼容机制:当 malloc 失败时,也可以尝试调用这个 handler。
简单总结一下就是它首先检查请求是否合法,然后把 0 字节请求转换为最小分配单位,接着调用 Windows 堆分配函数 HeapAlloc。如果堆分配失败,它会尝试通过 C++ 的 new-handler 机制释放一些内存,再重复尝试;只有在 handler 也无法解决问题时,才真正返回 nullptr 并设置 errno 为 ENOMEM。
HeapAlloc又在哪呢?