重载全局 new 和 delete 是最直接的泄漏检测入口,因为所有 new/delete 堆内存均经此路径,埋点可捕获地址、大小、调用栈和时间戳,无需侵入代码或编译器插桩,但须覆盖 operator new/delete 各标准变体。

为什么重载全局 new 和 delete 是最直接的泄漏检测入口
因为所有通过 new 分配、delete 释放的堆内存,都会经过这两个运算符。只要在它们内部埋点,就能捕获每次分配的地址、大小、调用栈和时间戳。不需要侵入业务代码,也不依赖编译器插桩(如 AddressSanitizer),适合嵌入式或受限环境下的轻量调试。
但要注意:C++ 标准允许重载 operator new 的多个变体(带 noexcept、带 std::align_val_t、placement 版本等),漏掉任意一个都可能让部分内存逃逸监控。实际项目中至少要覆盖:
void* operator new(std::size_t)void* operator new(std::size_t, const std::nothrow_t&) noexceptvoid operator delete(void*) noexceptvoid operator delete(void*, const std::nothrow_t&) noexcept- (C++17 起)
void* operator new(std::size_t, std::align_val_t)及对应delete
如何用 backtrace + backtrace_symbols 快速抓调用栈
Linux/macOS 下最省事的方式是调用 execinfo.h 提供的 backtrace。它不依赖调试符号,能拿到函数返回地址数组;再用 backtrace_symbols 转成可读字符串(注意:该函数不是异步信号安全的,仅用于调试模式,不可在信号处理函数中调用)。
实操建议:
立即学习“C++免费学习笔记(深入)”;
- 分配时最多记录前 16 层栈帧,避免性能暴跌 ——
backtrace(buf, 16) - 用
std::unordered_map<void leakinfo></void>存活块,键为分配地址,值含大小、栈符号、分配线程 ID(可选) - 别直接存
backtrace_symbols返回的字符串指针 —— 它指向内部静态缓冲区,下次调用就失效;必须strdup或转成std::string - Windows 下换用
CaptureStackBackTrace+SymFromAddr,需初始化 dbghelp.dll 和符号路径
为什么 atexit 不可靠,而应主动调用泄漏报告函数
atexit 注册的函数在 main 返回后执行,看似完美,但存在两个致命问题:
- 若程序因
abort()、std::terminate()或信号(如 SIGSEGV)崩溃,atexit函数根本不会运行 - 多线程环境下,若某个线程正在
new中加锁,而主线程已进入atexit回调并尝试遍历 map,极易死锁
更稳妥的做法是:在关键退出点(比如 main 结束前、单元测试 tearDown 中)显式调用 dump_leaks()。也可封装成 RAII 类,在其析构中触发检查,但要确保该对象生命周期覆盖整个程序运行期。
哪些内存必然逃逸你的检测器
重载 new/delete 只能管住 C++ 堆分配。以下几类完全不在监控范围内:
-
malloc/calloc/realloc—— 需单独 hook libc 符号(如 LD_PRELOAD) -
mmap/VirtualAlloc等直接系统调用 - 第三方库内部使用的自定义分配器(如 Boost.Pool、tcmalloc 的
malloc替代) - 静态/全局对象的构造函数中隐式调用的
new(若检测器自身尚未初始化)
真正实用的泄漏检测器,往往得组合多种机制:全局 new/delete 拦截 + malloc hook + 构造/析构日志 + 运行时 dump 接口。单靠重载 new 只能解决“你写的那部分”。









