陷阱一:悬空指针——指向已逝之物的指针

悬空指针,说白了就是指针指向的内存已经被释放了,但指针变量还保留着原来的地址值。你拿着这个地址去访问,鬼知道会读到什么。

我刚开始写C语言时,就栽过这个跟头。有一次调试一个通信协议栈,程序跑着跑着就随机崩溃。查了两天,最后发现是一个悬空指针在作祟。嗯,从那以后我对指针的生命周期格外敏感。

悬空指针是怎么产生的?

最常见的情况有三种:

  • 局部变量地址返回——函数返回后,栈空间被回收
  • free/delete后未置空——内存释放了,指针还在
  • 指针指向的变量超出作用域——比如指向了块作用域内的变量
// 典型错误:返回局部变量的地址
int* get_value() {
    int x = 42;
    return &x;  // x 在函数返回后就不存在了
}

// 正确做法:用静态变量或动态分配
int* get_value_safe() {
    static int x = 42;
    return &x;
}

// 或者用 malloc
int* get_value_dynamic() {
    int* p = malloc(sizeof(int));
    if (p) *p = 42;
    return p;
}
⚠️ 注意:悬空指针不会立刻报错。它可能在你访问时恰好指向一块未被使用的内存,程序照常运行。但一旦那块内存被重新分配,你的程序就会莫名其妙地崩溃。这种bug最难排查。
💡 我的习惯:每次 free 指针后,立刻将其置为 NULL。这样后续如果误用,至少能快速崩溃,而不是产生难以追踪的随机行为。

陷阱二:缓冲区溢出——写过了头

缓冲区溢出,就是往数组里写数据时,超出了它原本分配的大小。C语言不检查边界,所以编译器不会拦你,但运行时可能直接崩掉。

我记得有一次在嵌入式项目中,一个同事用 sprintf 拼接字符串,没注意目标缓冲区只有32字节。结果数据一多,直接覆盖了栈上的返回地址。程序跳转到未知地址,整个系统复位。嗯,从那以后我们团队规定:所有字符串操作必须指定长度

// 危险写法
char buf[16];
strcpy(buf, "这是一个非常长的字符串,肯定超过16字节了");

// 安全写法
char buf[16];
strncpy(buf, "这是一个非常长的字符串,肯定超过16字节了", sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';  // 确保以 '\0' 结尾

缓冲区溢出的后果:

  • 覆盖相邻变量——数据被篡改
  • 覆盖返回地址——程序崩溃或执行恶意代码
  • 堆内存损坏——后续 malloc/free 行为异常

陷阱三:指针类型不匹配——编译器不总是帮你

指针类型不匹配,说白了就是指针的类型和它指向的数据类型不一致。C语言允许强制类型转换,但后果自负。

你想想看,一个 int* 指向一个 char 变量,然后你通过这个指针去读写4个字节。这4个字节里,有3个是别人的地盘。我见过一个案例,有人把 float* 强转成 int* 去比较数值大小,结果因为浮点数的内存表示和整数完全不同,比较结果完全错误。

// 类型不匹配的典型例子
int value = 0x12345678;
char* p = (char*)&value;  // 合法,但要注意字节序

// 更危险的情况:对齐问题
char buffer[4];
int* p_int = (int*)buffer;  // buffer 可能未对齐到4字节边界
*p_int = 0xDEADBEEF;        // 某些架构上会触发总线错误
指针类型 解引用读取的字节数 对齐要求
char* 1 字节
short* 2 字节 2 字节对齐
int* 4 字节 4 字节对齐
double* 8 字节 8 字节对齐
⚠️ 注意:在 ARM、MIPS 等嵌入式处理器上,未对齐访问会触发硬件异常。x86 虽然能容忍,但性能会下降。所以不要依赖编译器的"宽容"。

陷阱四:数组越界访问——C语言不拦你,但系统会

数组越界,就是访问了索引范围之外的元素。C语言不会像 Java 那样抛异常,它直接去读写那块内存。运气好,读到的是垃圾数据;运气不好,写坏了关键数据。

我曾经调试过一个电机控制程序,电机偶尔会突然反转。查了三天,最后发现是一个数组索引写成了 i <= N 而不是 i < N。多出来的那次访问,恰好写到了控制寄存器的映射地址上。嗯,从那以后我写循环条件时都会多看一眼。

int arr[10];
int i;

// 越界写入
for (i = 0; i <= 10; i++) {  // 注意:应该是 i < 10
    arr[i] = i * 2;  // 当 i=10 时,arr[10] 越界
}

// 越界读取
int value = arr[15];  // 读到的是栈上的其他数据

数组越界的常见场景:

  • 循环条件写错(<= 写成 < 或反之)
  • 字符串未以 '\0' 结尾,导致 strlen 读到越界
  • 多维数组的索引计算错误
  • 缓冲区大小和实际数据量不匹配

知识体系总览

下面这张图,我把这四种陷阱的核心要点和关联关系画出来了。你可以把它当作一张"避坑地图"。

指针常见陷阱 悬空指针 缓冲区溢出 类型不匹配 数组越界 访问已释放内存 写入超出分配空间 错误解释内存数据 访问索引范围之外 核心教训 1. 指针的生命周期管理:谁分配,谁释放,释放后置空 2. 边界检查:循环条件、字符串长度、数组索引 3. 类型安全:避免随意强制转换,注意对齐要求 4. 工具辅助:使用 AddressSanitizer、Valgrind 等工具

避坑指南:我的实战经验

说了这么多理论,最后分享几条我自己的实战经验:

  • 我曾经在调试一个网络协议栈时,发现数据包偶尔被截断。查到最后,是 memcpy 的第三个参数写成了 sizeof(指针) 而不是 sizeof(数组)。嗯,sizeof 对指针和数组的行为不同,这个坑太经典了。
  • 我曾经在嵌入式裸机环境下,用 printf 打印调试信息,结果程序跑飞。后来发现是 printf 内部用了栈缓冲区,而栈空间只剩不到64字节。从那以后,我在资源受限的平台上只用自己写的简易打印函数。
  • 我个人习惯在定义数组时,用宏定义常量而不是直接写数字。比如 #define BUF_SIZE 64,这样所有用到这个大小的地方都统一,改起来也方便。

💡 最后的建议:写指针代码时,脑子里要时刻有一张"内存地图"。知道每个指针指向哪里,那块内存是谁的,生命周期有多长。如果你觉得这张地图模糊了,那就停下来,画张图,或者加个断言。


公众号:蓝海资料掘金营,微信deep3321