跳转至

C++ 内存问题排查(Valgrind)

越界、泄漏、未初始化、双重释放——C++ 内存错误的共同特点是症状远离病因(崩在别处、值莫名不对、跑得越久越慢)。Valgrind 用二进制插桩在运行期把每一笔内存操作都检查一遍,不需要重新编译。全部示例在 WSL valgrind 3.18 实测,报告原文如下。

基本用法

sudo apt install valgrind
valgrind --leak-check=full ./app            # 最常用组合:完整泄漏报告
valgrind --leak-check=full --show-leak-kinds=all ./app
valgrind --track-origins=yes ./app          # 未初始化值排查必加(慢一点,但给出来源)

程序照常运行(慢 10~50 倍),结束时输出报告。适合小型测试程序和单测跑;大型程序先缩小输入规模。

示例一:内存泄漏

// leak.cpp —— 用完不还
#include <cstdio>

int main() {
    int* p = new int[100];
    p[0] = 42;
    std::printf("%d\n", p[0]);
    // 忘了 delete[] p;
    return 0;
}
g++ -g leak.cpp -o leak        # -g 带调试符号,报告才有行号
valgrind --leak-check=full ./leak
HEAP SUMMARY:
    in use at exit: 400 bytes in 1 blocks
...
400 bytes in 1 blocks are definitely lost in loss record 1 of 1
   at 0x4841F9B: operator new[](unsigned long)
   by 0x40116F: main (leak.cpp:5)        ← 精确到行

LEAK SUMMARY:
   definitely lost: 400 bytes in 1 blocks

definitely lost 就是真正的泄漏:指针丢了,内存再也还不了。分类速查:

分类 含义 严重程度
definitely lost 指针丢失,无法释放 必须修
indirectly lost 随 definitely lost 一起丢的内部结构 修复前者即消失
possibly lost 剩下的指针指向块内部而非开头(常见于容器实现) 多数可解释,确认后处理
still reachable 程序结束时指针还在(如全局缓存,没释放而已) 一般可接受

示例二:越界读写

// oob.cpp —— 堆越界
#include <cstdio>

int main() {
    int* p = new int[10];
    p[10] = 7;                 // 合法下标是 0..9
    std::printf("%d\n", p[10]);
    delete[] p;
    return 0;
}
Invalid write of size 4
   at 0x40116F: main (oob.cpp:6)         ← 写越界,第 6 行
 Address 0x4a48068 is 0 bytes after a block of size 40

Invalid read / Invalid write / Invalid free / delete 与 new[] 混用都会被逐条报告。裸数组越界在 release 下多半静默,valgrind 让它当场现形——这也是先给程序带上 -g 再排查的原因。

示例三:未初始化读取

// uninit.cpp
#include <cstdio>

int main() {
    int x;
    if (x > 0)                 // 没初始化就判断
        std::printf("positive\n");
    return 0;
}
g++ -g uninit.cpp -o uninit && valgrind --track-origins=yes ./uninit
Conditional jump or move depends on uninitialised value(s)
   at 0x40113F: main (uninit.cpp:6)
 Uninitialised value was created by an allocation of 'x' in the stack frame

这类 bug 在 C++ 里的解法是习惯问题:声明即初始化(int x = 0;),成员用默认成员初始化器——与智能指针的 RAII 思路同源。

与 AddressSanitizer 怎么选

Valgrind ASan(-fsanitize=address)
重编译 不需要 需要(编译期插桩)
速度 慢 10~50 倍 慢约 2 倍
检出类型 上面全部 + 更多 类似,另含容器越界(libstdc++ 需加 -D_GLIBCXX_SANITIZE_VECTOR)
典型用法 第三方二进制、偶发问题、CI 兜底 日常开发常开

两者互补:日常开发挂 ASan,拿不到源码的第三方程序或需要零重编译的场景用 Valgrind。(本机 WSL2 的 ASan 存在环境兼容问题,Valgrind 是本站示例的实际验证工具。)

日常预防:让这类错误没有机会发生

  1. 能不用裸 new/delete 就不用:std::vector、std::string、智能指针覆盖 95% 场景,泄漏与双重释放自然消失;
  2. 越界的根源是手写下标——优先用迭代器、at()(越界抛异常)或 std::span;
  3. 声明即初始化;
  4. CI 里挂一套单测 + sanitizer/valgrind,回归问题当场拦截(测试框架见 GoogleTest)。

常见疑问

症状 说明
报告里全是 still reachable 通常是标准库/全局单例的"该还没还",多数环境可接受;要清零就显式释放或改用局部对象
第三方库报泄漏但无法改 用 suppression 文件屏蔽指定调用栈:--gen-suppressions=all 生成,--suppressions=file 复用
程序在 valgrind 下变慢/行为不同 插桩的正常代价;对时序敏感的多线程程序注意它可能掩盖竞争(竞争用 TSan,见并发笔记)
报告没有行号 编译时缺 -g;strip 过的 release 只能给地址

评论