动态内存管理:malloc/calloc/realloc/free 函数详解、内存分配失败处理

说到动态内存管理,这绝对是C语言里最容易出坑的地方之一。我刚开始做嵌入式开发那会儿,就因为在堆上分配内存没处理好,导致设备跑几天就死机。排查了整整一周,最后发现是内存泄漏。嗯,今天我们就来把这几个函数彻底讲透。

malloc:最基础的内存分配器

malloc 的全称是 memory allocation。它的作用就是从堆区申请一块连续的内存空间。

void *malloc(size_t size);

返回的是 void* 指针,所以需要强制类型转换。我个人习惯是这么写的:

int *p = (int *)malloc(10 * sizeof(int));
if (p == NULL) {
    // 处理分配失败
}

这里有个细节:malloc 分配的内存是未初始化的。里面存的是随机值,也就是所谓的「野数据」。你想想看,如果你直接读取这块内存,结果完全不可预测。

警告:malloc 分配的内存不会自动清零。如果你需要初始化为0,要么手动 memset,要么用 calloc。

calloc:带清零的分配器

calloc 和 malloc 最大的区别就是:它会把分配的内存全部初始化为0。

void *calloc(size_t nmemb, size_t size);

参数是两个:元素个数和每个元素的大小。比如:

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

我在项目中遇到过一个问题:用 malloc 分配了一个结构体数组,结果因为没初始化,里面的指针成员指向了随机地址。后来改成 calloc,问题就解决了。说白了,calloc 更安全,但速度会慢一点点。

realloc:动态调整内存大小

这个函数很有意思。它可以在不丢失原有数据的前提下,调整已分配内存的大小。

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

使用时要特别注意:

  1. 如果 ptr 是 NULL,realloc 的行为等同于 malloc
  2. 如果 new_size 是 0,行为等同于 free
  3. 如果扩展失败,原内存不会被释放

我曾经犯过一个经典错误:

// ❌ 错误写法
int *p = (int *)malloc(10 * sizeof(int));
p = (int *)realloc(p, 20 * sizeof(int)); // 如果realloc失败,p变成NULL,原内存泄漏!

// ✅ 正确写法
int *p = (int *)malloc(10 * sizeof(int));
int *tmp = (int *)realloc(p, 20 * sizeof(int));
if (tmp != NULL) {
    p = tmp;  // 只有成功才赋值
} else {
    // 处理失败,p仍然有效
}
技巧:realloc 可能会移动内存块。如果新大小比旧大小大很多,系统会在堆的其他位置找一块更大的空间,然后把旧数据拷贝过去。所以 realloc 后的指针值可能和原来不一样。

free:释放内存的必修课

有分配就必须有释放。这是铁律。

void free(void *ptr);

使用 free 时有几个铁律:

  • 只能释放由 malloc/calloc/realloc 分配的内存
  • 不能重复释放同一块内存(double free)
  • 释放后要将指针置为 NULL,避免野指针
int *p = (int *)malloc(10 * sizeof(int));
// ... 使用 p ...
free(p);
p = NULL;  // 好习惯!

为什么释放后要置 NULL?因为 free 只是把内存标记为可用,但指针仍然指向那块地址。如果不置 NULL,后面不小心又 free 一次,就是 double free,程序直接崩溃。

内存分配失败处理

嵌入式系统里,内存分配失败是家常便饭。堆空间有限,碎片化严重,随时可能分配失败。

处理策略一般有三种:

策略 说明 适用场景
检查返回值 每次 malloc 后都检查是否为 NULL 所有场景,必须做
错误恢复 分配失败时释放其他内存,重试 内存紧张的系统
断言退出 分配失败直接 assert 或 abort 开发调试阶段

我个人推荐的做法是:

void *safe_malloc(size_t size) {
    void *ptr = malloc(size);
    if (ptr == NULL) {
        // 记录日志
        fprintf(stderr, "malloc failed: size=%zu\n", size);
        // 尝试释放缓存
        release_cache();
        // 重试一次
        ptr = malloc(size);
        if (ptr == NULL) {
            // 实在不行就报错
            exit(EXIT_FAILURE);
        }
    }
    return ptr;
}
核心要点:动态内存管理的本质是「谁分配,谁释放」。每个 malloc 都要对应一个 free,否则就是内存泄漏。嵌入式系统里,内存泄漏是慢性毒药,不会立刻崩溃,但会慢慢耗尽系统资源。

知识体系图

动态内存管理 malloc calloc realloc free 未初始化 需手动清零 自动清零 适合数组/结构体 调整大小 保留原有数据 释放内存 必须配对使用 分配失败处理:检查返回值 → 错误恢复 → 断言退出

这张图把四个函数的关系和特性都串起来了。你想想看,malloc 和 calloc 是分配,realloc 是调整,free 是释放。每个函数都有自己的使用场景和注意事项。

我的建议:在嵌入式开发中,尽量少用动态内存分配。如果非用不可,一定要做好分配失败的预案。我曾经在一个项目中,因为 malloc 失败没有处理,导致系统在内存紧张时直接跑飞。从那以后,我每个 malloc 后面都会跟一个 NULL 检查。

好了,动态内存管理这块就讲到这里。记住核心原则:谁分配谁释放,每次分配都要检查失败,释放后指针置 NULL。做到这三点,大部分内存问题都能避免。

可可精品资料库