20、数组与指针的常见错误:数组名不能自增、指针越界、野指针、内存重复释放

说实话,指针和数组这块,很多同学学完理论觉得懂了,一写代码就崩。我见过太多类似的bug了——不是段错误,就是内存泄漏,或者程序跑着跑着突然挂掉。今天咱们就把这几个最常见的坑,一个一个说清楚。

20.1 数组名不能自增

先看一个经典错误:

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;

// 正确:指针可以自增
p++;  // p指向arr[1]

// 错误:数组名不能自增
arr++;  // 编译错误!

为什么会这样?

数组名 arr 是一个常量指针,它指向数组的第一个元素。常量意味着它的值不能被修改。你想想看,如果允许 arr++,那 arr 就不再指向数组开头了,整个数组就丢了。

关键区别:

  • arr 是常量指针,不能自增
  • p = arr 是指针变量,可以自增

我在项目中遇到过有人这样写:

void process_array(int arr[]) {
    while (*arr != 0) {
        // 处理数据
        arr++;  // 编译通过?不一定!
    }
}

注意了,函数参数中的 int arr[] 其实已经被编译器退化成了指针 int *arr,所以这里 arr++ 是合法的。但如果你在函数外面定义了一个真正的数组,那就不能自增了。这个细节,很多人栽过跟头。

20.2 指针越界

指针越界,说白了就是访问了不属于你的内存。这是C语言里最隐蔽、最危险的错误之一。

int arr[3] = {10, 20, 30};
int *p = arr;

// 越界访问
p[3] = 40;   // 危险!arr只有3个元素
*(p + 5) = 50; // 更危险!

这段代码编译时不会报错,运行时也不一定立刻崩溃。但它在悄悄破坏别人的内存——可能是另一个变量的值,可能是函数返回地址,甚至可能是系统关键数据。

越界的后果:

  • 覆盖相邻变量的值
  • 破坏堆栈结构,导致函数返回异常
  • 产生难以复现的随机bug
  • 严重时直接段错误(Segmentation Fault)

我曾经调试过一个嵌入式设备,程序运行几小时后随机死机。查了三天,最后发现是一个数组越界写,把任务控制块给覆盖了。嗯,这种bug最难查,因为出问题的地方和犯错的地方往往不在同一行代码。

避坑指南:

我曾经在代码里加了一个边界检查宏,每次访问数组前都判断索引是否合法。虽然多了一点点性能开销,但换来了安心。

#define SAFE_ACCESS(arr, idx, size) \
    ((idx) >= 0 && (idx) < (size) ? (arr)[idx] : 0)

20.3 野指针

野指针就是指向「不知道哪里」的指针。它比越界更可怕,因为越界至少还有个目标,野指针完全随机。

int *p;  // 未初始化,野指针!
*p = 10; // 危险!不知道写到哪里去了

int *q = malloc(sizeof(int));
free(q);
*q = 20; // 危险!q已经被释放,变成野指针

野指针的三种常见来源:

  1. 未初始化:局部指针变量默认值是随机的
  2. 释放后未置空:free之后指针还在,但指向的内存已归还
  3. 指向栈变量:函数返回后,栈上的局部变量被销毁
int* get_value() {
    int a = 100;
    return &a;  // 危险!返回栈地址
}

int *p = get_value();
*p = 200;  // 栈帧已销毁,行为未定义

我个人习惯是:每次定义指针时都初始化为NULL,每次free之后也立即置为NULL。这样至少能保证野指针不会乱飞,访问NULL指针会立刻崩溃,比随机崩溃好查得多。

好习惯:

int *p = NULL;  // 初始化
p = malloc(100);
if (p != NULL) {
    // 使用p
    free(p);
    p = NULL;  // 释放后置空
}

20.4 内存重复释放

重复释放(double free)是动态内存管理中的常见灾难。你释放了一块内存,然后又释放一次——这会导致堆管理器的元数据被破坏,轻则程序崩溃,重则产生安全漏洞。

int *p = malloc(sizeof(int) * 10);
// 使用p...
free(p);
// 又用了一次
free(p);  // double free!未定义行为

更隐蔽的情况是多个指针指向同一块内存:

int *p = malloc(100);
int *q = p;  // q和p指向同一块内存

free(p);
p = NULL;

free(q);  // 重复释放!q没有置空

重复释放的后果:

  • 堆管理器的链表被破坏
  • 后续的malloc/free行为异常
  • 可能被利用进行堆溢出攻击
  • 程序直接崩溃(glibc会检测到并abort)

我记得有一次,团队里一个同事在错误处理路径上连续调了两次free,结果线上服务每隔几小时就挂一次。查日志发现每次挂掉前都有一次double free的报错。从那以后,我们定了个规矩:谁分配谁释放,释放后立即置空

我的建议:

  • 每个malloc配一个对应的free,成对出现
  • 释放后立即将指针置为NULL
  • 使用智能指针或封装好的内存管理函数
  • 代码审查时重点检查free的调用次数

20.5 知识体系总览

下面这张图把本章的四个核心错误串起来了,方便你整体把握:

数组与指针常见错误 数组名不能自增 arr是常量指针 arr++ 编译错误 函数参数退化为指针后可自增 本质:常量 vs 变量 指针越界 访问超出数组范围的内存 编译不报错,运行时随机崩溃 破坏相邻变量或堆栈 对策:边界检查 野指针 指向未知内存的指针 未初始化 / 释放后未置空 返回栈变量地址 对策:初始化+置空 内存重复释放 同一块内存被free两次 多个指针指向同一内存 破坏堆管理器元数据 对策:谁分配谁释放 核心原则:初始化、不越界、不重复、释放置空

这四个错误,说白了都是内存管理不规范导致的。C语言给了你极大的自由,但自由越大,责任越大。你想想看,Java、Python这些语言为什么没有野指针?因为它们替你管了。但C语言把管理权交给你,你就得自己上心。

最后送大家一句话:指针用得好,程序跑得稳;指针用不好,debug到天亮。希望今天的内容能帮你少踩几个坑。


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