c++如何实现简单的内存泄漏排查_c++ 重载new与delete运算符记录【指南】

重载 new 和 delete 是排查内存泄漏最直接的手段,因其可在每次分配/释放时插入日志、堆栈捕获或计数逻辑,无需修改业务代码且不依赖外部工具;但须同时覆盖全局及数组版本,并妥善处理 size 获取、线程安全、递归调用与 STL 绕过等问题。

为什么重载 newdelete 是排查内存泄漏最直接的手段

因为 C++ 标准分配器不记录谁在什么位置申请了多大块内存,而重载全局 new/delete 能在每次分配/释放时插入日志、堆栈捕获或计数逻辑——不需要修改业务代码,也不依赖外部工具(如 Valgrind 或 ASan),适合嵌入式、游戏引擎或无法用调试器的环境。

但要注意:仅重载全局版本(::operator new / ::operator delete)不够,必须同时覆盖数组版本(operator new[] / operator delete[]),否则 new int[10] 类操作会绕过你的监控。

如何安全地重载全局 newdelete 并记录调用点

核心是用 __FILE____LINE__backtrace()(Linux)或 CaptureStackBackTrace(Windows)获取上下文。为避免递归调用(比如日志本身 malloc),所有记录逻辑必须使用栈内存或预分配缓冲区。

  • 重载函数必须声明在全局作用域,且不能在头文件中重复定义(加 #pragma onceinline 修饰符易出错,建议单独实现于一个 .cpp 文件)
  • 不要在重载函数里调用 std::coutmallocnew 或任何可能间接触发分配的函数
  • 用静态数组缓存调用栈地址,再用 dladdr(Linux)或 SymFromAddr(Windows)做符号解析;若不想依赖符号表,至少保留 __FILE____LINE__
  • 用原子计数器(std::atomic_size_t)统计当前未释放字节数,避免多线程竞争
void* operator new(size_t size) {
    void* ptr = malloc(size);
    if (ptr) {
        static std::atomic_size_t total_allocated{0};
        total_allocated += size;
        fprintf(stderr, "[ALLOC] %p %zu bytes at %s:%d\n", ptr, size, __FILE__, __LINE__);
    }
    return ptr;
}

void operator delete(void* ptr) noexcept { if (ptr) { static std::atomic_size_t total_allocated{0}; // 这里无法知道 size —— 实际需配合 malloc_usable_size 或自建映射表 fprintf(stderr, "[FREE] %p\n", ptr); free(ptr); } }

如何补全 size 信息并支持匹配检查

标准 operator delete 不带 size 参数,所以单纯打印 __FILE__/__LINE__ 无法判断哪次 new 没被配对释放。解决方法是维护一张哈希表:以指针为 key,存 size + 分配位置 + 时间戳。

  • 哈希表本身必须用 mmap 分配(避免用 new 初始化自己),或用固定大小环形缓冲区 + 线性查找(牺牲精度换安全性)
  • Linux 下可用 malloc_usable_size(ptr) 近似还原 size(仅对 malloc 系分配有效,不适用于自定义对齐或 operator new(std::align_val_t)
  • 启用 C++17 的 operator new(size_t, std::align_val_t) 重载时,必须同步实现对应 delete 版本,否则对齐分配会 fallback 到默认 new,漏监控
  • 程序退出前遍历未释放项,按 size 排序输出 top N 泄漏点,比单纯计数更易定位问题模块

实际使用时最容易忽略的三个坑

很多实现跑起来没报错,但根本抓不到泄漏——不是逻辑错,而是被编译器或运行时绕过了。

  • std::vectorstd::string 等 STL 容器默认使用 std::allocator,它内部可能直接调用 malloc 而非 operator new;要彻底监控,得传入自定义 allocator 或用链接期替换(如 LD_PRELOAD)
  • 静态对象构造期间的 new 可能发生在你的重载函数初始化之前,导致首几笔分配丢失;把记录结构体定义为 static 全局变量而非局部 static,可确保优先构造
  • Release 模式下编译器可能内联或优化掉 __FILE__/__LINE__,或把 backtrace 调用整个删掉;务必在 Release 构建中保留 debug info(-g)并禁用相关优化(如 -fno-omit-frame-pointer

真正有效的内存泄漏定位,从来不是靠“有没有重载”,而是看是否覆盖了所有分配路径、能否还原真实调用上下文、以及是否能在目标环境下稳定复现——其余都是细节。