C++内存泄漏检测之手动记录法(Manual Memory Tracking)

一、什么是手动记录法(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 的替代

它是:

  • 无工具环境的兜底方案
  • 模块级内存画像工具

相关推荐
好评1242 小时前
【C++】二叉搜索树(BST):从原理到实现
数据结构·c++·二叉树·二叉搜索树
zylyehuo2 小时前
error: no matching function for call to ‘ros::NodeHandle::param(const char [11], std::string&, const char [34])’
c++·ros1
码上成长2 小时前
JavaScript 数组合并性能优化:扩展运算符 vs concat vs 循环 push
开发语言·javascript·ecmascript
打工的小王2 小时前
java并发编程(三)CAS
java·开发语言
油丶酸萝卜别吃2 小时前
Mapbox GL JS 表达式 (expression) 条件样式设置 完全指南
开发语言·javascript·ecmascript
爱吃大芒果2 小时前
Flutter for OpenHarmony前置知识:Dart 语法核心知识点总结(下)
开发语言·flutter·dart
Ulyanov2 小时前
从桌面到云端:构建Web三维战场指挥系统
开发语言·前端·python·tkinter·pyvista·gui开发
星火开发设计2 小时前
C++ 函数定义与调用:程序模块化的第一步
java·开发语言·c++·学习·函数·知识
cypking2 小时前
二、前端Java后端对比指南
java·开发语言·前端