第39章:常见错误与调试技巧——段错误、内存泄漏、野指针的排查,使用GDB调试

说实话,C语言程序员最怕什么?不是算法难,不是逻辑复杂,而是那些莫名其妙就崩掉的运行时错误。段错误、内存泄漏、野指针——这三个词,我敢说每个写过C的人看到都会心头一紧。

我自己刚入行那会儿,有一次项目上线前夜,程序跑着跑着就Segmentation Fault了。我盯着屏幕看了三个小时,最后发现是一个野指针在作祟。从那以后,我就养成了几个调试习惯,今天全部分享给你。

本章核心:掌握三大常见错误的本质原因,学会用GDB高效定位问题。

39.1 段错误(Segmentation Fault)

段错误说白了就是程序访问了不该访问的内存。操作系统有个内存保护机制,你越界了,它就给你发个SIGSEGV信号,程序直接崩掉。

39.1.1 常见触发场景

  • 解引用空指针——最常见,没有之一
  • 数组越界——尤其是写操作,踩到别人的地盘
  • 栈溢出——递归太深或者局部变量太大
  • 访问已释放的内存——use-after-free
// 典型段错误示例
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = NULL;
    *p = 42;  // 段错误!解引用空指针

    int arr[10];
    arr[100] = 5;  // 越界写入,可能段错误

    int *q = malloc(100);
    free(q);
    *q = 10;  // use-after-free,段错误

    return 0;
}

注意:数组越界不一定会立即段错误,它可能悄悄覆盖了别的变量。这种bug最难查,因为症状和原因不在同一个地方。

39.2 内存泄漏

内存泄漏不会让程序立刻崩,但它像慢性病。跑一天没事,跑一周就开始卡,跑一个月直接OOM被系统杀掉。

我在项目中遇到过最离谱的一次:一个后台服务跑了28天,内存从50MB涨到了8GB。查到最后,是一个循环里每次malloc都没free。

39.2.1 泄漏的典型模式

  • 忘记free——分配了堆内存,但没释放
  • 丢失指针——重新赋值前没释放旧内存
  • 异常路径未释放——函数中间return了,free没执行到
// 内存泄漏示例
void leak_example() {
    char *buf = malloc(1024);
    // 用buf做点事...
    // 忘记free(buf) — 泄漏了

    // 另一种:指针被覆盖
    int *data = malloc(100 * sizeof(int));
    data = malloc(200 * sizeof(int));  // 第一次分配的内存丢了
    free(data);  // 只释放了第二次的
}

// 正确做法
void good_example() {
    char *buf = malloc(1024);
    if (!buf) return;
    // 使用buf...
    free(buf);  // 成对出现
}

个人习惯:我写malloc的时候,会立刻写对应的free。哪怕中间有100行代码,free先占个位置。这叫「配对编程法」,能减少90%的泄漏。

39.3 野指针

野指针就是指向「不确定」内存的指针。它比空指针更危险,因为空指针至少会段错误,野指针可能静默地破坏数据。

39.3.1 野指针的三大来源

来源 说明 示例
未初始化 局部指针变量没赋初值 int *p; *p = 1;
释放后未置空 free之后指针还在,指向已释放内存 free(p); // p还是原来的地址
返回局部变量地址 函数返回了栈上变量的地址 return &local_var;
// 野指针的典型
int* get_wild() {
    int local = 42;
    return &local;  // 危险!函数返回后local就没了
}

int main() {
    int *p;  // 未初始化,野指针
    *p = 10; // 不知道写到哪里去了

    int *q = malloc(4);
    free(q);
    // q现在是野指针
    *q = 100; // 可能崩,也可能不崩,看运气

    int *r = get_wild();
    printf("%d\n", *r); // 未定义行为
}

我曾经被一个野指针折磨了两天。现象是程序偶尔崩溃,但大部分时间正常。最后发现是free之后没置NULL,另一个线程又去读了这个指针。嗯,从那以后我free完必加一句p = NULL;

39.4 使用GDB调试

GDB是C程序员的瑞士军刀。说实话,没有GDB我可能早就转行了。它让你能「钻进」程序内部,看变量、看堆栈、看内存。

39.4.1 编译时准备

要用GDB调试,编译时必须加-g选项,保留符号信息。

gcc -g -o myapp myapp.c   # 加-g,保留调试信息
gcc -O0 -g -o myapp myapp.c  # -O0关闭优化,调试更准确

39.4.2 基本调试流程

# 启动GDB
gdb ./myapp

# 常用命令(我每天用这些)
(gdb) run          # 运行程序
(gdb) break main   # 在main函数设断点
(gdb) break 42     # 在第42行设断点
(gdb) next         # 单步执行(不进入函数)
(gdb) step         # 单步执行(进入函数)
(gdb) print x      # 打印变量x的值
(gdb) backtrace    # 查看调用栈
(gdb) list         # 查看源代码
(gdb) quit         # 退出

39.4.3 调试段错误

程序崩了之后,用GDB看它「死在哪里」是最快的。

# 方法一:直接运行,等它崩
gdb ./myapp
(gdb) run
...程序崩溃...
(gdb) backtrace   # 看调用栈,定位到哪一行崩的
(gdb) print p     # 看那个指针的值

# 方法二:用core dump
ulimit -c unlimited  # 允许生成core文件
./myapp              # 运行,崩溃后生成core
gdb ./myapp core     # 用core文件调试
(gdb) backtrace      # 直接看到崩溃位置

我建议:开发环境一定要开core dump。有时候bug复现概率低,core文件就是破案的关键证据。

39.4.4 检查内存泄漏

GDB本身不直接检测泄漏,但配合Valgrind就无敌了。

# 用Valgrind检测内存泄漏
valgrind --leak-check=full ./myapp

# 输出示例:
# ==12345== 100 bytes in 1 blocks are definitely lost
# ==12345==    at 0x4C2B800: malloc (vg_replace_malloc.c:299)
# ==12345==    by 0x4005E4: leak_example (test.c:8)
# ==12345==    by 0x4005F8: main (test.c:15)

# 看到没?直接告诉你哪一行泄漏了

39.4.5 实战:用GDB定位野指针

// buggy.c
#include <stdio.h>
#include <stdlib.h>

int main() {
    int *p = malloc(4);
    *p = 42;
    free(p);
    // 忘记置NULL
    *p = 100;  // 野指针写入
    printf("Done\n");
    return 0;
}

// GDB调试过程
// $ gcc -g -o buggy buggy.c
// $ gdb ./buggy
// (gdb) run
// Program received signal SIGSEGV, Segmentation fault.
// 0x0000000000400527 in main () at buggy.c:9
// 9           *p = 100;  // 野指针写入
// (gdb) print p
// $1 = (int *) 0x0   // 或者某个无效地址
// (gdb) info locals
// p = 0x0
// 真相大白:free之后p没置NULL,但内存已经被回收了

39.5 调试心法总结

调试不是瞎猜,是有章法的。我总结了一套流程,你试试看:

  1. 复现——稳定复现是第一步,不能复现的bug等于不存在
  2. 二分法——注释掉一半代码,看问题还在不在,缩小范围
  3. 加日志——关键位置加printf,看变量值的变化
  4. 用工具——GDB看堆栈,Valgrind查内存,AddressSanitizer抓越界
  5. 修一个测一个——别一次改一堆,改完立刻验证

记住:段错误、内存泄漏、野指针,本质上都是「内存管理」的问题。C语言给了你自由,但也给了你责任。每次malloc都要问自己:谁来free?每次用指针都要问自己:它指向的内存还活着吗?

C语言常见错误与调试流程 段错误 内存泄漏 野指针 常见原因 • 解引用空指针 • 数组越界 • 栈溢出 / use-after-free 常见原因 • 忘记free • 指针被覆盖 • 异常路径未释放 常见原因 • 未初始化 • free后未置NULL • 返回局部变量地址 调试工具:GDB + Valgrind backtrace看堆栈 print看变量值 break设断点 Valgrind查泄漏

调试这件事,说白了就是「大胆假设,小心求证」。别怕bug,每个bug都是你成长的机会。我到现在还记得第一次用GDB定位到段错误时的那种成就感——嗯,比写新代码还爽。

专注资料整理