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;
}
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;
}
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 是本站示例的实际验证工具。)
日常预防:让这类错误没有机会发生
- 能不用裸
new/delete就不用:std::vector、std::string、智能指针覆盖 95% 场景,泄漏与双重释放自然消失; - 越界的根源是手写下标——优先用迭代器、
at()(越界抛异常)或std::span; - 声明即初始化;
- CI 里挂一套单测 + sanitizer/valgrind,回归问题当场拦截(测试框架见 GoogleTest)。
常见疑问
| 症状 | 说明 |
|---|---|
报告里全是 still reachable |
通常是标准库/全局单例的"该还没还",多数环境可接受;要清零就显式释放或改用局部对象 |
| 第三方库报泄漏但无法改 | 用 suppression 文件屏蔽指定调用栈:--gen-suppressions=all 生成,--suppressions=file 复用 |
| 程序在 valgrind 下变慢/行为不同 | 插桩的正常代价;对时序敏感的多线程程序注意它可能掩盖竞争(竞争用 TSan,见并发笔记) |
| 报告没有行号 | 编译时缺 -g;strip 过的 release 只能给地址 |