malloc() 通常不会为每一次请求都进入内核。用户态分配器先在已经拥有的堆区、arena、线程缓存和空闲链表中寻找合适大小的块,只有库存不足或请求很大时才通过 brk、mmap 等向内核扩展虚拟内存。free(ptr) 之所以不需要用户再传大小,是因为分配器在块附近或独立元数据中记录了大小、状态和所属结构;这也解释了为什么越界写常常让程序在很久之后的无关 free() 中崩溃。
分配器要同时解决什么
它要快速满足小对象、支持不同大小、降低多线程争用、控制内部/外部碎片,并在适当时机归还或复用内存。常见策略包括 size class、线程本地缓存、多个 arena、空闲链表/bitmap 和对大块请求的独立 mmap。不同 libc 或专用 allocator 的实现细节不同,但“快路径复用、慢路径向系统要内存”是共同思路。
C++ 的对象语义不止是 malloc/free
new 负责分配并调用构造函数,delete 调用析构后释放;malloc/free 只管理原始字节。两套接口必须配对,数组 new[]/delete[] 也要配对。std::vector、std::string、unique_ptr 等容器/RAII 类型能把释放时机绑定到对象生命周期,通常比手写 malloc/free 更安全。
1 | auto p = std::make_unique<Widget>(); // 构造、独占所有权、作用域结束自动析构 |
高频分配成为热点时,优先复用 buffer、提前 reserve、批量处理或使用 std::pmr/对象池;不要因为“malloc 很慢”就立刻手写 allocator。现代 allocator 在无竞争小对象上可能很快,真正瓶颈也许是初始化、copy、cache miss 或对象生命周期设计。
内存错误要用工具抓,而不是猜
double free、use-after-free、越界写和错误配对都属于未定义行为,分配器只是在某个后来时刻发现元数据不一致。AddressSanitizer、Valgrind、glibc 检查与核心转储能将错误靠近发生点;内存泄漏还要区分“逻辑上不再需要但仍被引用”和“分配器尚未归还给 OS”。
实时路径应避免动态分配:提前建立固定容量对象池/环形缓冲,并让 new/日志/异常路径不落在控制周期内。分配器的目标是灵活性,而实时路径的目标是可预测性,两者需要用明确边界协作。