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

动态内存分配,说白了就是程序在运行的时候,跟操作系统“要”一块内存来用。这跟咱们平时定义变量不一样——变量的大小是编译时就定死的,而动态分配的内存,大小可以在运行时决定。我刚开始学C语言那会儿,觉得这玩意儿可有可无,直到有一次做嵌入式项目,需要处理一个大小不确定的数据包……嗯,从那以后,我再也不敢小看动态内存了。

8.1 为什么需要动态内存分配?

你想想看,写程序的时候,有些数据的大小是未知的。比如你要从文件里读一串字符串,长度多少?不知道。或者你要创建一个链表,节点个数?也不确定。这时候,静态数组就不好使了。

静态数组有个毛病:你开大了,浪费内存;开小了,程序可能崩溃。我见过不少新手,上来就定义一个 char buf[1024],觉得1024够用了。结果用户输入了1025个字符……嗯,缓冲区溢出,程序直接挂了。

动态内存分配,就是解决这个问题的。它让你在程序运行时,按需申请内存。用完了,再还回去。说白了,就是“按需分配,用完即还”。

8.2 malloc:最基础的内存分配器

malloc 是动态内存分配的核心函数。它的原型长这样:

#include <stdlib.h>

void *malloc(size_t size);

它接收一个参数:你要申请的字节数。返回一个 void* 指针,指向分配到的内存起始地址。如果分配失败,返回 NULL

我个人习惯,每次用 malloc 都会检查返回值。为什么?因为嵌入式设备内存有限,分配失败是常有的事。我曾经在一个项目中,因为忘了检查 malloc 返回值,导致程序在内存不足时直接访问了 NULL 指针……结果就是系统复位,用户数据丢失。那教训,够我记一辈子。

核心要点:malloc 分配的内存是未初始化的。里面存的是随机值,别指望它是0。

看个例子:

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

int main() {
    int *p = (int*)malloc(10 * sizeof(int));
    if (p == NULL) {
        fprintf(stderr, "内存分配失败!\n");
        return -1;
    }

    // 使用内存
    for (int i = 0; i < 10; i++) {
        p[i] = i * 2;
    }

    // 打印
    for (int i = 0; i < 10; i++) {
        printf("%d ", p[i]);
    }

    free(p);
    return 0;
}

这里有个细节:malloc 返回的是 void*,所以需要强制类型转换。在C语言里,其实可以不转,但C++不行。我个人建议转一下,代码更清晰。

8.3 calloc:带初始化的分配

callocmalloc 很像,但有两个区别:

  • 它接收两个参数:元素个数和每个元素的大小
  • 它会自动把分配的内存清零

原型:

void *calloc(size_t nmemb, size_t size);

说白了,calloc(n, size) 等价于 malloc(n * size) 然后 memset(..., 0, n * size)。但 calloc 可能更高效,因为操作系统可以直接给你返回已经清零的物理页。

我在项目中遇到过一个问题:用 malloc 分配了一个结构体数组,结果忘了初始化,里面的指针成员全是野指针。后来改成 calloc,所有字段自动变成0,指针变成 NULL,至少不会直接崩溃。嗯,省心不少。

小技巧:如果你需要分配的内存要清零,直接用 calloc。别先 malloc 再 memset,多此一举。

8.4 realloc:调整已分配内存的大小

有时候,你一开始分配的内存不够用了,想扩大一点。或者分配多了,想缩小一点。这时候 realloc 就派上用场了。

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

它接收两个参数:原来的指针和新的字节数。返回调整后的内存地址。注意,返回的地址可能跟原来不一样——因为系统可能需要在别处找一块更大的连续空间。

使用 realloc 有个坑:如果它返回 NULL,原来的内存块不会被释放。所以你不能直接这么写:

// 错误写法
p = realloc(p, new_size);  // 如果失败,p 变成 NULL,原来的内存就泄漏了

正确的做法是:

// 正确写法
int *new_p = (int*)realloc(p, new_size);
if (new_p == NULL) {
    // 处理失败,但 p 仍然有效
    fprintf(stderr, "realloc 失败!\n");
} else {
    p = new_p;  // 只有成功才更新指针
}

我曾经在代码里犯过这个错误,结果内存泄漏查了好久。后来养成了习惯:永远用一个临时指针接收 realloc 的返回值。

警告:realloc 的第一个参数可以是 NULL。如果 ptr 是 NULL,它的行为就跟 malloc 一样。但我不建议这么用,代码可读性差。

8.5 free:把内存还回去

有借有还,再借不难。动态分配的内存,用完了必须用 free 释放。否则就会造成内存泄漏。

void free(void *ptr);

释放之后,指针就变成了野指针。我建议立即把指针置为 NULL

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

为什么?因为如果你不小心再次 free(p),程序会崩溃。而 free(NULL) 是安全的,什么都不会发生。

还有一点:不要重复释放同一块内存。这叫 double free,是未定义行为。我见过一个同事,在错误处理路径里忘了把指针置 NULL,结果正常路径和错误路径各 free 了一次……嗯,调试了一下午。

8.6 内存分配失败的处理

动态内存分配不是100%成功的。在嵌入式系统里,内存资源尤其紧张。分配失败的原因通常有:

  • 内存碎片太多,没有足够大的连续空间
  • 系统总内存不足
  • 堆空间被耗尽

处理分配失败,有几种常见策略:

策略 说明 适用场景
直接退出 打印错误信息,调用 exit() 关键系统,无法降级运行
返回错误码 让调用者决定如何处理 库函数,上层有容错机制
重试 等待一段时间后重新分配 临时资源紧张,可能恢复
使用备用内存池 预先分配一块备用内存 嵌入式系统,实时性要求高

我个人习惯,在嵌入式项目里,会预先分配一个小的“应急内存池”。当 malloc 失败时,从应急池里分配。这样至少能保证系统可以输出错误信息,而不是直接死机。

重要原则:每次调用 malloc/calloc/realloc 后,必须检查返回值是否为 NULL。这不是可选项,是必须项。

8.7 知识体系图

下面这张图,把动态内存分配的核心逻辑串起来了。你看一眼,就能明白整个流程:

动态内存分配核心流程 需要动态内存 选择分配函数 malloc(size) 未初始化,随机值 calloc(n, size) 自动清零 realloc(ptr, size) 调整已有内存大小 检查返回值是否为 NULL? 处理失败(退出/重试/备用) 使用内存 → 最后 free()

8.8 避坑指南

最后,我总结几个常见的坑,都是我曾经踩过的:

  • 忘记 free:内存泄漏。程序跑久了,内存被吃光。嵌入式设备尤其明显。
  • 重复 free:double free,程序崩溃。解决办法:free 后立即置 NULL。
  • 使用已 free 的内存:use-after-free,未定义行为。可能当时没事,但随时会崩。
  • 越界访问:分配了10个int,却写了第11个。会破坏堆结构,导致后续分配失败。
  • 忘记检查 NULL:直接访问空指针,段错误。这是最常见的错误之一。

嗯,动态内存分配就是这么回事。说白了,就是“借、用、还”三个步骤。但每一步都有细节,稍不注意就会出问题。我建议你写代码的时候,把 mallocfree 成对写,就像括号一样。这样不容易漏。


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