11、动态内存管理(stdlib.h):malloc、calloc、realloc、free。内存泄漏、野指针、内存碎片。

动态内存管理,说白了就是让程序在运行时「按需要内存」。

我刚开始学C语言时,觉得静态数组够用了。直到有一次写一个图像处理程序,图片大小是运行时才知道的——嗯,那时候我才意识到,没有动态内存,很多事根本做不了。

11.1 四个核心函数

stdlib.h 里提供了四个函数,它们构成了C语言动态内存的基石。我习惯把它们分成两组:分配组释放组

11.1.1 malloc —— 最常用的分配器

void *malloc(size_t size);

malloc 分配一块连续内存,返回首地址。注意:它不会初始化内存,里面是随机值。

我在项目中遇到过一个问题:用 malloc 分配了一个结构体数组,直接拿来用,结果里面有个指针成员是野的,程序跑起来就崩。排查了半天,才发现是没清零。

⚠️ 重要提醒: malloc 返回的指针一定要检查是否为 NULL。内存分配失败时,它会返回 NULL,不检查就直接用,程序会段错误。

11.1.2 calloc —— 带清零的分配器

void *calloc(size_t nmemb, size_t size);

calloc 和 malloc 的区别有两点:

  • 它会把分配的内存全部初始化为 0
  • 它接受两个参数:元素个数和每个元素的大小

我个人习惯:如果分配的内存要存字符串或结构体,我倾向于用 calloc。省得自己再写 memset。但如果你对性能有极致要求,而且马上会覆盖所有数据,那 malloc 更合适。

11.1.3 realloc —— 调整已有内存块

void *realloc(void *ptr, size_t new_size);

realloc 可以扩大或缩小之前分配的内存。我经常用它来实现动态数组——数组满了就翻倍扩容。

💡 一个小技巧: 用 realloc 时,不要直接 ptr = realloc(ptr, new_size)。如果 realloc 失败,它会返回 NULL,原来的 ptr 就丢了。正确做法是用一个临时指针接收返回值。
// 正确做法
void *new_ptr = realloc(old_ptr, new_size);
if (new_ptr == NULL) {
    // 处理错误,old_ptr 仍然有效
} else {
    old_ptr = new_ptr;
}

11.1.4 free —— 释放内存

void free(void *ptr);

free 把内存还给系统。但注意:free 之后,指针的值还在,它指向的内存已经无效了。这就是野指针的来源之一。

我曾经犯过一个错:free 了一个指针,后来又用这个指针去访问数据。程序偶尔正常,偶尔崩溃,查了两天才找到原因。从那以后,我每次 free 完都会把指针置为 NULL。

free(ptr);
ptr = NULL;  // 避免野指针

11.2 三大陷阱

动态内存管理之所以难,不是因为函数本身复杂,而是因为人容易犯错。我总结了三个最常见的坑。

11.2.1 内存泄漏

内存泄漏就是:你申请了内存,但忘了释放。程序跑得越久,占用的内存越多,最后系统扛不住。

我见过最夸张的一次:一个服务器程序跑了三天,内存从 50MB 涨到 8GB,最后 OOM 被内核杀了。查代码发现是一个循环里每次 malloc 都没 free。

🔍 如何避免:
  • 写代码时,先写 free,再写 malloc。养成「谁申请谁释放」的习惯
  • 使用工具检测:Valgrind 是 Linux 下的神器
  • 复杂项目中,可以考虑引用计数或垃圾回收库

11.2.2 野指针

野指针是指向「已释放内存」或「未分配内存」的指针。它比 NULL 指针更危险——NULL 指针至少会立即崩溃,野指针可能让你在错误的地方写数据,破坏其他变量。

野指针的常见来源:

  • free 之后没有置 NULL
  • 返回局部变量的地址
  • 指针越界访问
⚠️ 我曾经踩过的坑: 写了一个函数返回栈上数组的地址,调用方拿到指针后欢天喜地地用了,结果数据全是乱的。因为函数返回后,栈帧被回收了。

11.2.3 内存碎片

内存碎片是个更隐蔽的问题。频繁地 malloc 和 free 小块内存,会导致堆上出现大量不连续的小空闲块。虽然总空闲空间够,但找不到一块连续的大内存来满足分配请求。

我做过一个嵌入式项目,内存只有 64KB。程序跑着跑着,malloc 突然返回 NULL,但统计空闲总量还有 20KB。一查,碎片化严重,最大连续块只有 3KB。

缓解碎片的方法:

  • 尽量分配固定大小的内存块(对象池模式)
  • 避免频繁分配释放
  • 使用 slab 分配器或伙伴系统(在自定义内存管理时)

11.3 知识体系图

下面这张图总结了动态内存管理的核心脉络。我建议你把它记在脑子里,写代码时随时对照。

动态内存管理知识体系 核心函数 malloc 分配,不初始化 calloc 分配,清零 realloc 调整大小 free 释放内存 三大陷阱 内存泄漏 申请了没释放,内存越用越多 野指针 指向无效内存,崩溃或数据错乱 内存碎片 空闲块不连续,大分配失败 核心原则:谁申请谁释放,释放后置 NULL,分配后检查 NULL

11.4 避坑指南

最后,分享几条我这些年总结的经验:

  1. 分配后立即检查 NULL —— 不要偷懒,嵌入式环境尤其重要
  2. free 后立即置 NULL —— 我吃过亏,你也别重蹈覆辙
  3. 不要多次 free 同一个指针 —— 这是未定义行为,后果不可预测
  4. 谁申请谁释放 —— 模块间传递内存所有权时,一定要约定清楚
  5. 用 Valgrind 定期检查 —— 我每周至少跑一次,能发现很多隐藏问题
💡 我的个人习惯: 写一个简单的内存调试宏,在 malloc 和 free 时记录文件名和行号。出问题时,能快速定位到是哪一行申请的内存没释放。

动态内存管理是C语言的精髓,也是很多bug的根源。掌握好这四函数、三陷阱,你就能写出更健壮的程序。嗯,多练练,慢慢就有感觉了。


专注资料整理