c++oding="utf-8" ?>
直接重载全局 operator new 会出问题,因其是线程不安全入口点,替换后所有代码(含标准库、stl)均调用该实现,易因未处理 nothrow 版本、忽略对齐、异常返回导致死循环或初始化期 crash。

为什么直接重载全局 operator new 会出问题
C++ 全局 operator new 是线程不安全的入口点,一旦被替换,所有代码(包括标准库、第三方库、甚至 STL 容器内部)都会走你的实现。常见踩坑:没处理 std::nothrow 版本、忽略对齐要求(alignof(std::max_align_t))、未返回 nullptr 而是抛异常导致死循环。更隐蔽的是:某些编译器(如 MSVC)在初始化静态对象时,可能在你的内存池还没 ready 之前就调用 new,直接 crash。
- 必须同时重载
operator new、operator new[]、operator new(std::nothrow_t)、operator new[](std::nothrow_t) - 对齐不能硬写 8 或 16 —— 必须用
std::align_val_t{align}参数(C++17 起),并转发给底层分配器 - 首次调用前要确保内存池已构造完成,建议用函数内 static 变量延迟初始化
如何设计一个线程安全但无锁的固定块内存池
固定大小块(fixed-size slab)是最易控、最常用的选择,适合高频小对象(如节点、事件结构体)。关键不是“快”,而是“确定性”:避免碎片、避免系统调用、避免锁争用。
- 每个池只管理一种 size(比如 64 字节),按需创建多个池实例;size > 128 字节建议直接 fallback 到
malloc - 用单向 free-list(链表头存于首字节) + 原子指针(
std::atomic<void></void>)实现无锁分配;释放时仅 CAS 插入头部 - 为防 ABA 问题,不依赖指针值本身做比较,而用
std::atomic<uintptr_t></uintptr_t>存“地址+版本号”(简单场景可暂不加版本) - 初始化时预分配一大块内存(如 64KB),切成等长块,用
std::aligned_alloc保证对齐
示例核心分配逻辑:
void* FixedPool::alloc() {
void* expected = head_.load();
do {
if (expected == nullptr) return nullptr;
void* next = *static_cast<void>(expected);
if (head_.compare_exchange_weak(expected, next)) {
return expected;
}
} while (true);
}
</void>
怎样安全地替换全局 new 而不破坏 std::string / std::vector
STL 容器默认使用 std::allocator,它最终调用全局 operator new。但部分容器(尤其 debug 模式下的 libc++/MSVC)会在构造时做额外检查或预分配,若内存池未预留足够空间或对齐不符,就会触发断言失败或访问违规。
- 务必在
main()之前完成内存池初始化(例如用static FixedPool pool(64);全局变量) - 在
operator new中区分 size:size 走池,否则调用 <code>::operator new(size, align)(注意 C++17 的对齐参数) - 禁止在内存池分配路径中调用任何可能间接触发
new的操作(如std::cout、throw 异常、调用 STL 算法) - 调试阶段开启
-D_GLIBCXX_DEBUG或_HAS_ITERATOR_DEBUGGING=1,验证容器行为是否正常
为什么 delete 不一定要配对回收到内存池
全局 operator delete 接收原始地址和 size(C++14 起),但你的内存池通常不知道某块内存属于哪个池、对应什么 size。强行根据地址反查池实例,会引入哈希查找或页表映射开销,反而拖慢性能。
- 推荐策略:只重载
operator new,让operator delete直接调用::operator delete(ptr, align)(即原生释放) - 如果真要回收,必须在
operator new分配时把 size/池 ID 写入内存前缀(如前 8 字节),并在operator delete中读取 —— 但要注意这会破坏 ABI 兼容性,且影响 cache 局部性 - 更稳妥的做法:用 RAII 封装池分配器(如
pool_allocator<t></t>),显式传给容器,而非全局劫持
真正麻烦的从来不是怎么写内存池,而是判断哪些对象该进池、哪些不该进——比如 std::function 内部捕获的小闭包、std::shared_ptr 的控制块,它们的生命周期和 size 高度不确定。全局替换本质是高风险操作,上线前必须跑满压力测试 + ASan + UBSan。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











