【数据结构与算法】第49篇:代码调试技巧与常见内存错误排查
一、常见内存错误类型
| 错误类型 | 表现 | 常见原因 |
|---|---|---|
| 段错误 | 程序崩溃,Segmentation fault | 访问空指针、越界访问、栈溢出 |
| 内存泄漏 | 程序内存持续增长,最终耗尽 | malloc后忘记free |
| 重复释放 | 程序崩溃或行为异常 | 对同一指针多次free |
| 野指针 | 随机崩溃 | 使用已释放的指针 |
| 缓冲区溢出 | 数据被覆盖,逻辑错误 | 数组越界写入 |
二、段错误(Segmentation Fault)排查
2.1 什么是段错误
程序试图访问不属于它的内存区域时,操作系统会发送SIGSEGV信号,导致程序崩溃。
常见触发场景:
解引用空指针:
int *p = NULL; *p = 10;数组越界:
int arr[5]; arr[5] = 10;使用已释放的内存
栈溢出:递归过深或局部变量过大
2.2 使用GDB调试(Linux)
GDB(GNU Debugger)是Linux下最常用的调试工具。
bash
# 编译时加 -g 选项保留调试信息 gcc -g -o program program.c # 启动GDB gdb ./program # 常用命令 (gdb) run # 运行程序 (gdb) bt # 查看调用栈(backtrace) (gdb) list # 查看源代码 (gdb) print var # 打印变量值 (gdb) break 行号 # 设置断点 (gdb) continue # 继续运行 (gdb) quit # 退出
实战示例:调试段错误
c
#include <stdio.h> #include <stdlib.h> void causeSegfault() { int *p = NULL; *p = 10; // 这里会段错误 } int main() { causeSegfault(); return 0; }bash
$ gcc -g -o test test.c $ gdb ./test (gdb) run Program received signal SIGSEGV, Segmentation fault. 0x0000000000401136 in causeSegfault () at test.c:5 5 *p = 10; (gdb) bt #0 causeSegfault () at test.c:5 #1 main () at test.c:9
bt命令直接告诉你:第5行*p = 10出问题了,调用链是 main → causeSegfault。
2.3 使用printf调试(快速定位)
当没有调试器时,在可疑位置插入printf是最原始有效的方法:
c
printf("debug: before line %d\n", __LINE__); // 可疑代码 printf("debug: after line %d\n", __LINE__);如果"after"没打印出来,说明问题在这两行之间。
2.4 使用断言(assert)
断言可以帮助你在开发阶段提前发现问题:
c
#include <assert.h> int *p = (int*)malloc(sizeof(int)); assert(p != NULL); // 如果p为NULL,程序停止并输出错误位置 *p = 10;
三、内存泄漏排查(Valgrind)
3.1 安装Valgrind
bash
# Ubuntu/Debian sudo apt install valgrind # CentOS/RHEL sudo yum install valgrind # macOS brew install valgrind
3.2 基本使用
bash
valgrind --leak-check=full ./program
Valgrind会运行你的程序,并在结束时报告内存泄漏情况。
3.3 实战示例
c
#include <stdio.h> #include <stdlib.h> void memoryLeak() { int *p = (int*)malloc(10 * sizeof(int)); // 忘记 free(p) } int main() { memoryLeak(); return 0; }bash
$ gcc -g -o test test.c $ valgrind --leak-check=full ./test ==12345== HEAP SUMMARY: ==12345== in use at exit: 40 bytes in 1 blocks ==12345== total heap usage: 1 allocs, 0 frees, 40 bytes allocated ==12345== ==12345== 40 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==12345== at 0x4848899: malloc (in /usr/lib/valgrind/...) ==12345== by 0x10916B: memoryLeak (test.c:5) ==12345== by 0x10917B: main (test.c:10)
关键信息:
40 bytes in 1 blocks are definitely lost:确认泄漏
直接告诉你泄漏发生在
test.c:5的malloc调用
3.4 Valgrind常用选项
| 选项 | 作用 |
|---|---|
--leak-check=full | 详细泄漏报告 |
--show-reachable=yes | 显示仍可访问的泄漏 |
--track-origins=yes | 追踪未初始化变量的来源 |
--log-file=val.log | 输出到文件 |
四、使用VS调试器(Windows)
4.1 设置断点
在代码行号左侧单击,出现红点即断点。
4.2 常用快捷键
| 快捷键 | 作用 |
|---|---|
| F5 | 开始调试/继续 |
| F10 | 单步执行(不进入函数) |
| F11 | 单步执行(进入函数) |
| Shift+F5 | 停止调试 |
| Ctrl+F10 | 运行到光标处 |
4.3 查看内存
调试时打开“内存窗口”(调试 → 窗口 → 内存),输入地址如&arr即可查看数组内容。
五、常见内存错误的预防
5.1 指针使用规范
c
// 1. 初始化指针为NULL int *p = NULL; // 2. 使用前检查 if (p == NULL) { // 处理错误 } // 3. free后置NULL free(p); p = NULL; // 4. 不要返回局部变量的地址 int* badFunc() { int local = 10; return &local; // 错误!局部变量在函数返回后销毁 }5.2 数组边界检查
c
// 不安全 void unsafeCopy(char *dest, char *src) { while (*src) { *dest++ = *src++; // 不知道dest有多大 } } // 安全:传入长度 void safeCopy(char *dest, size_t destSize, char *src) { size_t i = 0; while (src[i] != '\0' && i < destSize - 1) { dest[i] = src[i]; i++; } dest[i] = '\0'; }5.3 内存分配后检查
c
int *p = (int*)malloc(n * sizeof(int)); if (p == NULL) { fprintf(stderr, "内存分配失败\n"); exit(1); }六、调试技巧总结
| 问题 | 排查方法 | 工具 |
|---|---|---|
| 段错误 | 查看调用栈 | GDBbt |
| 内存泄漏 | 运行泄漏检测 | Valgrind |
| 数组越界 | 添加边界检查 | AddressSanitizer |
| 未初始化变量 | 追踪来源 | Valgrind--track-origins=yes |
| 死循环 | 打印日志 | printf / 断点 |
| 逻辑错误 | 单步调试 | GDB / VS |
七、AddressSanitizer(更现代的选择)
GCC和Clang内置了AddressSanitizer,比Valgrind更快,适合大型程序。
bash
# 编译时加上 -fsanitize=address gcc -g -fsanitize=address -o program program.c # 直接运行,错误时会输出详细报告 ./program
八、小结
这一篇我们学习了调试和内存错误排查:
| 工具 | 用途 | 核心命令 |
|---|---|---|
| GDB | 调试段错误、逻辑错误 | gdb ./prog,run,bt |
| Valgrind | 检测内存泄漏 | valgrind --leak-check=full |
| AddressSanitizer | 检测内存错误 | -fsanitize=address |
| printf | 快速定位 | 打印关键位置 |
预防胜于治疗:
指针初始化并置NULL
free后立即置NULL
数组操作检查边界
分配内存后检查返回值
下一篇我们讲专栏总结与面试高频考点。
九、思考题
段错误一定是程序有bug吗?还有哪些情况可能导致段错误?
Valgrind报告"definitely lost"和"possibly lost"有什么区别?
如何用GDB在程序崩溃时自动打印调用栈?
为什么
free(p)后要把p设为NULL?
欢迎在评论区讨论你的答案。
