4. 动态内存分配:malloc/calloc/realloc/free 的正确用法与内存分配失败处理

动态内存分配,说白了就是程序运行时,跟操作系统“要一块地”来用。这块地有多大、什么时候还回去,都得你自己说了算。C 语言里,malloc、calloc、realloc 和 free 就是干这个活的四个兄弟。

我刚开始学的时候,觉得这玩意儿挺简单——要内存就调函数,用完就释放。结果呢?项目一复杂,各种野指针、内存泄漏、double free 全来了。嗯,今天咱们就把这四个兄弟的脾气摸透。

4.1 malloc:最基础的“要地”方式

malloc 的全称是 memory allocation。它从堆上申请一块连续的内存,返回一个 void* 指针。注意,它不会初始化这块内存——里面是啥垃圾数据,完全看运气。

#include <stdlib.h>

int *p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
    // 处理分配失败
    fprintf(stderr, "内存分配失败\n");
    exit(1);
}

我个人习惯,每次 malloc 之后立刻检查返回值。为什么?因为一旦 p 是 NULL,你后面直接写 p[0] = 42,那就是对空指针解引用——程序直接崩给你看。

警告:malloc 返回的指针,在使用前一定要判空。这不是“多此一举”,这是保命符。

4.2 calloc:带初始化的“要地”方式

calloc 和 malloc 的区别就两点:第一,它会自动把申请的内存全部清零;第二,它的参数是两个——元素个数和每个元素的大小。

int *arr = (int*)calloc(10, sizeof(int));
// 等价于 malloc(10 * sizeof(int)) 然后 memset(arr, 0, 10 * sizeof(int))

我在项目中遇到过一个问题:用 malloc 申请了一个结构体数组,结果某个字段没初始化,导致后续逻辑判断出错。排查了半天,最后发现是垃圾值惹的祸。从那以后,如果申请的内存需要初始化为零,我直接上 calloc,省心。

小提示:calloc 的清零操作会消耗一点性能。如果你不需要零初始化,用 malloc 更快。

4.3 realloc:动态调整内存大小

有时候,你一开始申请的内存不够用了,或者太多了想缩一缩。realloc 就是干这个的。它可以在原有内存块的基础上调整大小,尽量不移动数据。

int *p = (int*)malloc(5 * sizeof(int));
// ... 用着用着发现不够了
int *new_p = (int*)realloc(p, 10 * sizeof(int));
if (new_p == NULL) {
    // realloc 失败,原内存块 p 仍然有效
    fprintf(stderr, "realloc 失败\n");
    free(p);
    exit(1);
}
p = new_p;

这里有个坑,我曾经踩过:realloc 的第一个参数可以是 NULL。如果传 NULL,它的行为就和 malloc 一样。但千万别传一个已经被 free 掉的指针——那是未定义行为,后果自负。

关键点:realloc 返回的新指针,可能和原来的指针不同。一定要用返回值更新你的指针变量,不要继续用旧指针。

4.4 free:用完了一定要还

申请了内存,用完了就得还回去。不还?那就是内存泄漏。程序跑得越久,泄漏越多,最后系统内存耗尽,程序被干掉。

int *p = (int*)malloc(100 * sizeof(int));
// 使用 p
free(p);
p = NULL;  // 好习惯:释放后置空

我见过最典型的错误:free 之后没有把指针置 NULL,然后后面不小心又 free 了一次——double free,程序直接 abort。所以,我每次 free 之后,立刻把指针赋为 NULL。这样即使不小心再 free,free(NULL) 是安全的。

警告:不要 free 同一个指针两次。不要 free 栈上的地址。不要 free 一个已经被 free 的指针。这些都会导致未定义行为。

4.5 内存分配失败处理

动态内存分配不是百分百成功的。系统内存不足、堆空间碎片化、申请大小超过限制——都可能导致分配失败。这时候,malloc、calloc、realloc 都会返回 NULL。

怎么处理?我一般分两种情况:

  • 小型分配失败:直接报错退出,或者尝试释放一些缓存再重试。
  • 大型分配失败:记录日志,优雅地释放已有资源,然后退出。
void *safe_malloc(size_t size) {
    void *ptr = malloc(size);
    if (ptr == NULL) {
        fprintf(stderr, "错误:无法分配 %zu 字节内存\n", size);
        // 可以在这里尝试释放缓存
        // 或者直接退出
        exit(EXIT_FAILURE);
    }
    return ptr;
}

我个人习惯写一个 safe_malloc 这样的包装函数,把判空逻辑封装起来。这样主代码里就不用到处写 if 判空了,清爽很多。

4.6 知识体系总览

下面这张图,把四个函数的关系和核心要点串起来了。你一看就明白:

动态内存分配核心函数关系图 malloc 申请内存,不初始化 calloc 申请内存,自动清零 realloc 调整已有内存大小 使用内存 读写操作,注意不要越界 free 释放内存,置空指针 分配失败处理 判空 → 报错/重试/退出

4.7 常见错误与避坑指南

说一千道一万,不如看看实际中容易犯的错。我整理了几个高频问题:

错误类型 表现 解决方法
内存泄漏 程序内存持续增长,最终 OOM 每次 malloc 对应一个 free,用工具如 Valgrind 检测
野指针 访问已释放的内存,数据错乱或崩溃 free 后立即置 NULL,避免悬空指针
越界访问 写入了申请范围之外的内存,破坏堆结构 严格检查索引范围,用 sizeof 计算大小
double free 程序 abort,core dump free 后置 NULL,避免重复释放
忘记判空 对 NULL 指针解引用,段错误 每次分配后立即检查返回值
我的经验:写代码时,每写一个 malloc,就立刻写上对应的 free。这叫“成对编程”,能有效减少泄漏。

嗯,动态内存分配这块,说白了就是“借要还,借多少记清楚,还了就别再碰”。你只要记住这三点,大部分坑都能避开。至于那些更隐蔽的问题——比如堆内存碎片化、realloc 的移动开销——等你项目做深了,自然会遇到。到时候再回来翻翻这节课,应该会有新的体会。

可可精品资料库