一、什么是手动记录法(Manual Memory Tracking)
手动记录法 = 自己实现一个最小版 LeakSanitizer
核心思想只有一句话:
拦截所有内存分配 / 释放,并在程序退出时统计"仍然存活的块"
二、手动记录法的三种层级
| 层级 | 能力 | 成本 | 适用 |
|---|---|---|---|
| Level 1 | 记录指针 + 大小 | 极低 | 快速定位 |
| Level 2 | 记录调用栈 | 中 | 工程级 |
| Level 3 | 作用域 / 模块标记 | 高 | SLAM / Server |
下面逐级进行讲解展示。
三、Level 1:最小可用版
3.1 原理
- 重载
operator new / delete - 用
std::unordered_map<void*, size_t>记录 - 程序退出时打印未释放项
3.2 最简实现代码
头文件:mem_tracker.h
cpp
#pragma once
#include <unordered_map>
#include <mutex>
#include <cstdlib>
#include <iostream>
namespace memtrack {
inline std::unordered_map<void*, size_t>& alloc_map() {
static std::unordered_map<void*, size_t> map;
return map;
}
inline std::mutex& mutex() {
static std::mutex m;
return m;
}
inline void record_alloc(void* ptr, size_t size) {
std::lock_guard<std::mutex> lock(mutex());
alloc_map()[ptr] = size;
}
inline void record_free(void* ptr) {
std::lock_guard<std::mutex> lock(mutex());
alloc_map().erase(ptr);
}
inline void report_leaks() {
std::lock_guard<std::mutex> lock(mutex());
if (alloc_map().empty()) {
std::cout << "[memtrack] No leaks detected.\n";
return;
}
std::cout << "[memtrack] Memory leaks detected:\n";
for (auto& [ptr, size] : alloc_map()) {
std::cout << " Leak at " << ptr << ", size = "
<< size << " bytes\n";
}
}
} // namespace memtrack
重载 new/delete:mem_tracker.cpp
cpp
#include "mem_tracker.h"
void* operator new(size_t size) {
void* ptr = std::malloc(size);
if (!ptr) throw std::bad_alloc();
memtrack::record_alloc(ptr, size);
return ptr;
}
void operator delete(void* ptr) noexcept {
memtrack::record_free(ptr);
std::free(ptr);
}
程序退出自动检测
cpp
struct LeakReporter {
~LeakReporter() {
memtrack::report_leaks();
}
};
static LeakReporter g_leak_reporter;
3.3 使用示例
cpp
int main() {
int* a = new int(5);
double* b = new double[10];
delete a;
// delete[] b; // 故意泄漏
}
输出
[memtrack] Memory leaks detected:
Leak at 0x12345678, size = 80 bytes
四、Level 2:记录调用栈
4.1 为什么要调用栈?
你不只想知道 "漏了"
你想知道 "谁分配的"
4.2 Linux / macOS:backtrace
cpp
#include <execinfo.h>
struct AllocInfo {
size_t size;
void* stack[16];
int stack_size;
};
记录分配点
cpp
AllocInfo info;
info.size = size;
info.stack_size = backtrace(info.stack, 16);
alloc_map()[ptr] = info;
打印调用栈
cpp
char** symbols = backtrace_symbols(info.stack, info.stack_size);
for (int i = 0; i < info.stack_size; ++i)
std::cout << symbols[i] << "\n";
free(symbols);
4.2.1、示例整体思路
每次分配时记录:大小 + 调用栈
释放时删除记录
程序退出时,剩余记录 = 内存泄漏
核心结构:
text
operator new
└── backtrace() ← 记录分配位置
└── alloc_map[ptr] = AllocInfo
operator delete
└── alloc_map.erase(ptr)
程序结束
└── 遍历 alloc_map → 打印 backtrace
4.2.2、完整示例代码(Linux / macOS)
仅用于调试 / 学习,不要直接上生产。
1.头文件与数据结构
cpp
#include <execinfo.h>
#include <unordered_map>
#include <iostream>
#include <mutex>
#include <cstdlib>
struct AllocInfo {
size_t size;
void* stack[16];
int stack_size;
};
2.全局分配记录表(线程安全)
cpp
std::unordered_map<void*, AllocInfo>& alloc_map() {
static std::unordered_map<void*, AllocInfo> map;
return map;
}
std::mutex& alloc_mutex() {
static std::mutex m;
return m;
}
3.重载 operator new(核心)
cpp
void* operator new(size_t size) {
void* ptr = std::malloc(size);
if (!ptr) throw std::bad_alloc();
AllocInfo info;
info.size = size;
info.stack_size = backtrace(info.stack, 16);
{
std::lock_guard<std::mutex> lock(alloc_mutex());
alloc_map()[ptr] = info;
}
return ptr;
}
关键点解释
backtrace():抓取当前线程调用栈- 栈深度
16:经验值,足够定位 - 必须在 malloc 之后记录
- 加锁防止多线程踩踏
4.重载 operator delete
cpp
void operator delete(void* ptr) noexcept {
if (!ptr) return;
{
std::lock_guard<std::mutex> lock(alloc_mutex());
alloc_map().erase(ptr);
}
std::free(ptr);
}
5.泄漏打印函数(你给的代码完整版)
cpp
void dump_leaks() {
std::lock_guard<std::mutex> lock(alloc_mutex());
if (alloc_map().empty()) {
std::cout << "No memory leaks detected.\n";
return;
}
std::cout << "Memory leaks detected:\n";
for (const auto& [ptr, info] : alloc_map()) {
std::cout << "Leak at " << ptr
<< ", size = " << info.size << "\n";
char** symbols = backtrace_symbols(info.stack, info.stack_size);
for (int i = 0; i < info.stack_size; ++i) {
std::cout << " " << symbols[i] << "\n";
}
std::free(symbols);
std::cout << "---------------------------\n";
}
}
6.程序结束自动检查(关键工程技巧)
cpp
struct LeakChecker {
~LeakChecker() {
dump_leaks();
}
};
static LeakChecker leak_checker;
利用全局对象析构在 main 之后执行
7.测试代码(制造泄漏)
cpp
struct Foo {
int x;
};
int main() {
Foo* a = new Foo;
Foo* b = new Foo;
delete a;
// b 泄漏
return 0;
}
4.2.3、运行效果示例
text
Memory leaks detected:
Leak at 0x55d3a8c4eeb0, size = 4
./a.out(_Znwm+0x3a)
./a.out(main+0x1e)
/lib/x86_64-linux-gnu/libc.so.6(__libc_start_main+0xf3)
---------------------------
这正是 LeakSanitizer / Valgrind 做的事,只是它们更完善
4.3 Windows:CaptureStackBackTrace
cpp
#include <windows.h>
USHORT frames = CaptureStackBackTrace(0, 16, stack, NULL);
4.3.1 Windows:CaptureStackBackTrace(完整示例)
适用平台:Windows
头文件:
<windows.h>链接库:
Dbghelp.lib(用于符号解析)
1.头文件与数据结构
cpp
#include <windows.h>
#include <dbghelp.h>
#include <unordered_map>
#include <mutex>
#include <iostream>
#include <cstdlib>
#pragma comment(lib, "dbghelp.lib")
struct AllocInfo {
size_t size;
void* stack[16];
USHORT stack_size;
};
2.全局分配记录表
cpp
std::unordered_map<void*, AllocInfo>& alloc_map() {
static std::unordered_map<void*, AllocInfo> map;
return map;
}
std::mutex& alloc_mutex() {
static std::mutex m;
return m;
}
3.重载 operator new(Windows 版本)
cpp
void* operator new(size_t size) {
void* ptr = std::malloc(size);
if (!ptr) throw std::bad_alloc();
AllocInfo info;
info.size = size;
info.stack_size = CaptureStackBackTrace(
0, // skip frames
16, // max depth
info.stack,
NULL // hash (optional)
);
{
std::lock_guard<std::mutex> lock(alloc_mutex());
alloc_map()[ptr] = info;
}
return ptr;
}
关键点解释
| 参数 | 含义 |
|---|---|
0 |
不跳过当前栈帧(可设为 1 跳过 operator new) |
16 |
最大调用栈深度 |
stack |
返回的 PC 地址数组 |
hash |
可选,用于栈去重 |
4.重载 operator delete
cpp
void operator delete(void* ptr) noexcept {
if (!ptr) return;
{
std::lock_guard<std::mutex> lock(alloc_mutex());
alloc_map().erase(ptr);
}
std::free(ptr);
}
5.初始化符号系统(非常重要)
Windows 默认 只有地址,没有符号名,必须初始化:
cpp
void init_symbols() {
static bool inited = false;
if (!inited) {
SymInitialize(GetCurrentProcess(), NULL, TRUE);
inited = true;
}
}
建议在 main 或 dump_leaks 前调用一次
6.打印调用栈
cpp
void dump_stack(void* addr) {
HANDLE process = GetCurrentProcess();
char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME];
SYMBOL_INFO* symbol = (SYMBOL_INFO*)buffer;
symbol->SizeOfStruct = sizeof(SYMBOL_INFO);
symbol->MaxNameLen = MAX_SYM_NAME;
DWORD64 displacement = 0;
if (SymFromAddr(process, (DWORD64)addr, &displacement, symbol)) {
std::cout << " " << symbol->Name
<< " + 0x" << std::hex << displacement << std::dec << "\n";
} else {
std::cout << " [0x" << addr << "]\n";
}
}
7.泄漏打印函数
cpp
void dump_leaks() {
init_symbols();
std::lock_guard<std::mutex> lock(alloc_mutex());
if (alloc_map().empty()) {
std::cout << "No memory leaks detected.\n";
return;
}
std::cout << "Memory leaks detected:\n";
for (const auto& [ptr, info] : alloc_map()) {
std::cout << "Leak at " << ptr
<< ", size = " << info.size << "\n";
for (USHORT i = 0; i < info.stack_size; ++i) {
dump_stack(info.stack[i]);
}
std::cout << "---------------------------\n";
}
}
8.程序退出自动检测
cpp
struct LeakChecker {
~LeakChecker() {
dump_leaks();
}
};
static LeakChecker leak_checker;
9.测试代码
cpp
struct Foo {
int x;
};
int main() {
Foo* a = new Foo;
Foo* b = new Foo;
delete a;
// b 泄漏
return 0;
}
4.3.2、运行效果示例(Windows)
text
Memory leaks detected:
Leak at 0000021A7C1F5EB0, size = 4
operator new + 0x2A
main + 0x1E
__scrt_common_main_seh + 0x106
---------------------------
4.3.3、Windows vs Linux 对比
| 项目 | Linux/macOS | Windows |
|---|---|---|
| 栈捕获 | backtrace |
CaptureStackBackTrace |
| 符号解析 | backtrace_symbols |
DbgHelp / SymFromAddr |
| 默认符号 | 有 | 需初始化 |
| 性能 | 中 | 快 |
| 工业工具 | Valgrind / LSan | UMDH / VS Diagnostic |
五、Level 3:模块 / 作用域标记
5.1 典型问题
- 哪个模块在涨内存?
- 建图?优化?回环?
5.2 作用域标签设计
cpp
thread_local const char* g_scope = "global";
struct ScopeTag {
ScopeTag(const char* tag) { g_scope = tag; }
~ScopeTag() { g_scope = "global"; }
};
使用
cpp
{
ScopeTag tag("Mapping");
auto* p = new MapPoint;
}
泄漏报告
Leak 256 bytes at 0x...
Scope: Mapping
六、工程级注意事项(非常重要)
6.1 不要递归 new
cpp
// 错误示例
std::unordered_map // 会再次触发 new
// 解决方法
- 使用 malloc
- 或 static 初始化后禁用记录
解决方案:
cpp
thread_local bool g_tracking = true;
if (g_tracking) {
g_tracking = false;
record_alloc(...)
g_tracking = true;
}
6.2 new[] / delete[] 也要重载
cpp
void* operator new[](size_t size);
void operator delete[](void* ptr) noexcept;
6.3 与 STL / Eigen / OpenCV
- Eigen 默认对齐 new
- OpenCV 内部有自己的 allocator
可能需要宏开关
七、什么时候该用手动记录法
| 场景 | 是否推荐 |
|---|---|
| 无法用 ASan | ✅ |
| 嵌入式系统 | ✅ |
| 第三方库泄漏 | ✅ |
| 实时系统 | ✅ |
| 新手项目 | ❌ |
八、和 ASan 的关系
手动记录法 不是 ASan 的替代
它是:
- 无工具环境的兜底方案
- 模块级内存画像工具