【数据结构与算法】第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调试LinuxGDBGNU 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:9bt命令直接告诉你第5行*p 10出问题了调用链是 main → causeSegfault。2.3 使用printf调试快速定位当没有调试器时在可疑位置插入printf是最原始有效的方法cprintf(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;三、内存泄漏排查Valgrind3.1 安装Valgrindbash# Ubuntu/Debian sudo apt install valgrind # CentOS/RHEL sudo yum install valgrind # macOS brew install valgrind3.2 基本使用bashvalgrind --leak-checkfull ./programValgrind会运行你的程序并在结束时报告内存泄漏情况。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-checkfull ./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-checkfull详细泄漏报告--show-reachableyes显示仍可访问的泄漏--track-originsyes追踪未初始化变量的来源--log-fileval.log输出到文件四、使用VS调试器Windows4.1 设置断点在代码行号左侧单击出现红点即断点。4.2 常用快捷键快捷键作用F5开始调试/继续F10单步执行不进入函数F11单步执行进入函数ShiftF5停止调试CtrlF10运行到光标处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 内存分配后检查cint *p (int*)malloc(n * sizeof(int)); if (p NULL) { fprintf(stderr, 内存分配失败\n); exit(1); }六、调试技巧总结问题排查方法工具段错误查看调用栈GDBbt内存泄漏运行泄漏检测Valgrind数组越界添加边界检查AddressSanitizer未初始化变量追踪来源Valgrind--track-originsyes死循环打印日志printf / 断点逻辑错误单步调试GDB / VS七、AddressSanitizer更现代的选择GCC和Clang内置了AddressSanitizer比Valgrind更快适合大型程序。bash# 编译时加上 -fsanitizeaddress gcc -g -fsanitizeaddress -o program program.c # 直接运行错误时会输出详细报告 ./program八、小结这一篇我们学习了调试和内存错误排查工具用途核心命令GDB调试段错误、逻辑错误gdb ./prog,run,btValgrind检测内存泄漏valgrind --leak-checkfullAddressSanitizer检测内存错误-fsanitizeaddressprintf快速定位打印关键位置预防胜于治疗指针初始化并置NULLfree后立即置NULL数组操作检查边界分配内存后检查返回值下一篇我们讲专栏总结与面试高频考点。九、思考题段错误一定是程序有bug吗还有哪些情况可能导致段错误Valgrind报告definitely lost和possibly lost有什么区别如何用GDB在程序崩溃时自动打印调用栈为什么free(p)后要把p设为NULL欢迎在评论区讨论你的答案。