16、常见内存错误:越界访问、重复释放、释放非堆内存

内存管理这事儿,说难不难,说简单也真不简单。我见过太多人在堆内存上栽跟头了,包括我自己刚入行那会儿。今天咱们就把三个最要命的错误掰开揉碎了讲清楚。

16.1 越界访问:你以为你写的是自己的地盘?

越界访问,说白了就是你申请了10个字节,结果你写了第11个字节。C语言不会拦着你,它觉得你是个成年人,应该为自己的行为负责。但运行时,可能就炸了。

典型场景:数组下标越界、字符串拷贝时忘记算'\0'、memcpy长度算错。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main() {
    char *p = (char*)malloc(5);  // 只申请了5个字节
    if (!p) return -1;

    // 危险!这里拷贝了11个字符(含'\0')
    strcpy(p, "Hello World");  // 越界访问!

    printf("%s\n", p);
    free(p);
    return 0;
}

这段代码能编译通过,甚至可能正常运行几次。但别高兴太早——它只是还没踩到地雷而已。我在项目中遇到过类似情况,程序在客户现场跑了三天才崩溃,查得我头都大了。

后果:

  • 覆盖了相邻内存块的管理信息(free时崩溃)
  • 覆盖了其他变量的值(逻辑错误,极难排查)
  • 触发段错误(Segmentation Fault)

我的习惯:每次malloc后,我都会在纸上算一遍实际需要的字节数。尤其是字符串,别忘了末尾的'\0'。你想想看,strlen("abc")返回3,但你需要4个字节。

16.2 重复释放:同一个地址,你free了两次

重复释放,也叫double free。这错误看起来很低级,但实际中特别容易发生。尤其是在函数调用链中,你传了个指针出去,结果调用方和接收方都以为该自己负责释放。

#include <stdlib.h>

void bad_free(char *ptr) {
    free(ptr);  // 第一次释放
}

int main() {
    char *p = (char*)malloc(10);
    if (!p) return -1;

    bad_free(p);  // 这里释放了一次
    free(p);      // 这里又释放了一次 —— 重复释放!

    return 0;
}

为什么会这样?因为free之后,指针p的值并没有变成NULL。它仍然指向那块已经被回收的内存。第二次free时,堆管理器就懵了——这地址不在我的管理列表里啊!

我曾经踩过的坑:写一个链表销毁函数时,我在循环里free了节点,又在循环外free了一次头节点。结果头节点已经被当作普通节点释放过了。那次调试花了我整整一个下午。

避坑指南:

  • free之后立即将指针置为NULL
  • 明确谁申请谁释放的原则
  • 使用工具如Valgrind检测

16.3 释放非堆内存:你free了不该free的东西

这个错误更隐蔽。你只能free掉malloc、calloc、realloc返回的地址。栈上的变量、全局变量、字符串常量,这些都不能free。

#include <stdlib.h>
#include <stdio.h>

int global_var = 42;

int main() {
    int stack_var = 10;           // 栈上变量
    char *str = "hello";          // 字符串常量(只读区)
    int *heap = (int*)malloc(4);  // 堆上变量

    free(&stack_var);  // 错误!释放栈内存
    free(str);         // 错误!释放只读区
    free(heap);        // 正确

    return 0;
}

嗯,这里要注意:free(&stack_var)和free(str)都会导致未定义行为。轻则程序崩溃,重则破坏堆管理结构,影响后续所有内存操作。

如何判断一个指针能否被free?

指针来源 能否free 说明
malloc/calloc/realloc ✅ 可以 标准用法
&局部变量 ❌ 不可以 栈上分配
字符串常量 ❌ 不可以 只读数据段
全局变量地址 ❌ 不可以 数据段或BSS段
函数指针 ❌ 不可以 代码段

16.4 知识体系总览

下面这张图把三种常见内存错误的关系和后果梳理清楚了。我建议你多看几遍,形成肌肉记忆。

常见内存错误知识体系 越界访问 重复释放 释放非堆内存 后果 覆盖相邻内存 段错误 / 逻辑错误 后果 堆管理结构损坏 程序崩溃 后果 未定义行为 后续malloc失败 预防措施 free后置NULL | 明确谁申请谁释放 | 使用Valgrind/AddressSanitizer 计算好缓冲区大小 | 不要free栈/全局/常量地址

16.5 我的调试心得

说实话,排查内存错误是C程序员的基本功,也是最磨人的。我个人的经验是:

  • 先怀疑自己——90%的内存错误都是自己代码的问题,别一开始就怪编译器或操作系统
  • 用工具说话——Valgrind、AddressSanitizer、GDB,这些工具能帮你省下大量时间
  • 写防御性代码——free后置NULL、检查malloc返回值、用assert验证指针合法性

一个小技巧:如果你不确定一个指针能不能free,可以在free之前加个打印,看看指针的值。如果它指向的是0x7f开头的地址(栈),或者0x4开头的地址(代码段),那多半不能free。堆地址通常是程序运行时动态分配的,没有固定规律,但你可以通过上下文判断。

好了,这三种错误你记住了吗?越界访问、重复释放、释放非堆内存。每一个我都亲手踩过,每一个都让我印象深刻。希望你读完这一章,能少走一些弯路。


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