5、free 的使命:free函数原型、释放内存、避免野指针

好,咱们接着聊。上一章我们把 malloccalloc 怎么“申请”内存讲透了。那申请来的内存,用完了怎么办?

你得还回去。

不还?内存泄漏等着你。程序跑个几天,内存一点点被吃掉,最后系统扛不住,直接把你进程给杀了。我早期做嵌入式开发时就吃过这个亏——一个后台服务跑了三天,突然挂了。查了半天,发现是一个循环里 malloc 了没 free。嗯,从那以后,我每次 malloc 完,第一件事就是先把 free 写上去。

今天我们就来彻底搞懂 free 的使命。

5.1 free 函数原型

先看它的长相:

#include <stdlib.h>

void free(void *ptr);

参数只有一个:ptr —— 指向你要释放的那块内存的指针。

注意几个关键点:

  • ptr 必须是之前由 malloccallocrealloc 返回的地址。
  • 如果 ptrNULLfree 什么都不做。这是标准规定的,所以你可以放心传 NULL
  • 释放后,那块内存的内容变得“不确定”。别再去读它了。
我个人习惯:每次 free 之后,立刻把指针置为 NULL。这样就算不小心再 free 一次,也不会出问题。

5.2 释放内存:到底发生了什么?

你调用 free(ptr) 时,C 运行时库会做两件事:

  1. 标记那块内存为“可用”。它不会清空数据,只是把这块内存还给堆管理器。
  2. 可能合并相邻的空闲块,减少碎片。

说白了,就是告诉系统:“这块地我不种了,你拿去给别人用吧。” 但地里的庄稼(数据)还在,只是随时可能被新的“种植者”覆盖。

我举个例子:

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

int main() {
    char *p = (char*)malloc(10);
    if (p == NULL) {
        return 1;
    }
    strcpy(p, "hello");
    printf("释放前: %s\n", p);  // 输出 hello

    free(p);  // 释放内存

    // 危险!释放后继续使用 p
    printf("释放后: %s\n", p);  // 可能输出乱码,也可能还是 hello
    return 0;
}

运行这段代码,第二次 printf 可能还能打出 "hello",也可能打出乱码。为什么?因为 free 后那块内存已经被标记为“空闲”,但数据还没被覆盖。你读到的只是“残影”。

警告:千万不要依赖这种“残影”数据。这是典型的野指针访问,属于未定义行为。程序可能今天能跑,明天就崩。

5.3 避免野指针:free 后的正确姿势

什么是野指针?就是指向“已释放”或“无效”内存的指针。你拿着它去读写,就像拿着过期的地图找路,迟早掉坑里。

怎么避免?三个字:及时置空

free(p);
p = NULL;  // 斩断指针与已释放内存的联系

这样,后面如果误用了 p,程序会立即崩溃(空指针解引用),而不是偷偷摸摸地产生随机 bug。崩溃是好事,至少你能马上定位问题。

我曾经在一个网络库中见过这样的代码:

void process_data(char *buffer) {
    // ... 处理数据 ...
    free(buffer);
    // 忘记置 NULL
}

// 在另一个地方
char *buf = (char*)malloc(256);
process_data(buf);
if (buf != NULL) {  // 这个判断是无效的!
    strcpy(buf, "new data");  // 写入了已释放的内存
}

这个 bug 藏得很深。因为 buffree 后没有被置 NULL,所以后面的 if 判断认为它仍然有效。结果就是往堆管理器的空闲链表里写数据,把整个堆结构破坏了。程序过一会儿才崩溃,查起来非常痛苦。

核心原则:malloc,谁 freefree 完,立刻置 NULL

5.4 常见误区与避坑指南

我总结几个新手(甚至老手)容易犯的错:

误区 说明 正确做法
重复 free 对同一指针调用两次 free free 后立即置 NULL,第二次 free(NULL) 安全
释放栈变量 free(&local_var) 只释放堆内存,栈变量由系统自动管理
释放部分内存 对指针偏移后释放,如 free(p+1) 必须传入 malloc 返回的原始地址
忘记释放 内存泄漏,程序内存持续增长 养成配对习惯:malloc 后立即写 free
避坑指南:我曾经在写一个字符串处理函数时,为了省事,直接 free 了传入的指针。结果调用方后续还在用那个指针,导致崩溃。后来我改成:要么在文档里明确说明“本函数会释放传入的内存”,要么让调用方自己释放。接口约定要清晰。

5.5 知识体系:free 的核心逻辑

下面这张图帮你理清 free 在整个动态内存管理中的位置:

free 使命:动态内存管理闭环 malloc / calloc 使用内存 free 堆管理器(空闲链表) 内存块可被重新分配 野指针(危险!) 正确流程:malloc → 使用 → free → 堆管理器 → 可重用

从图中可以看到,free 是闭环的关键一步。它把内存还给堆管理器,让后续的 malloc 能复用。而虚线部分——野指针,就是 free 后没有置空导致的危险路径。

5.6 总结

free 的使命很简单:把借来的内存还回去。但“还”的方式决定了你的程序是健壮还是脆弱。

  • 记住原型:void free(void *ptr)
  • 释放后指针置 NULL,这是成本最低的防御性编程。
  • 不要重复释放,不要释放栈内存,不要释放偏移后的地址。
  • 配对检查:每个 malloc 都要有对应的 free

嗯,关于 free 就讲这么多。下一章我们聊聊 realloc——怎么在运行时调整内存大小。不过那是后话了,先把今天的内容消化掉。


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