19、指针(五):动态内存分配(malloc/calloc/realloc/free)、内存泄漏的检测与预防

动态内存分配,说白了就是让程序在运行时向操作系统“要内存”。

你写代码时定义变量,那是静态分配——大小在编译时就定死了。但很多时候,你根本不知道用户会输入多少数据。比如一个文本编辑器,用户可能只写一行,也可能写一万行。这时候就需要动态分配。

我个人习惯把动态内存分配比作“借东西”。你跟系统借一块内存,用完了得还回去。不还?那就是内存泄漏。还错了?那就是野指针、段错误。嗯,这里面的门道不少。

19.1 四大内存操作函数

C语言标准库提供了四个核心函数:malloccallocreallocfree。它们都声明在 <stdlib.h> 中。

19.1.1 malloc:申请一块未初始化的内存

void* malloc(size_t size);

malloc 分配 size 字节的内存,返回一个 void* 指针。注意:这块内存里的数据是随机的——也就是所谓的“垃圾值”。

我在项目中遇到过新手直接使用 malloc 返回的指针,结果读出来的数据莫名其妙。其实很简单,要么你自己用 memset 清零,要么直接用 calloc

重要:malloc 可能失败!当内存不足时,它返回 NULL。永远不要假设 malloc 一定成功。
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
    // 处理内存分配失败
    fprintf(stderr, "内存分配失败\n");
    exit(1);
}

19.1.2 calloc:申请一块已清零的内存

void* calloc(size_t num, size_t size);

calloc 分配 num 个元素,每个元素 size 字节,并且自动把这块内存全部初始化为 0。

我个人习惯在分配数组时优先用 calloc。为什么?因为清零能避免很多“未初始化”的坑。你想想看,如果 malloc 出来的内存里恰好有个非零值,你的逻辑可能就错了。

// 分配一个包含 100 个整数的数组,全部初始化为 0
int* arr = (int*)calloc(100, sizeof(int));
if (arr == NULL) {
    // 处理错误
}

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

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

realloc 用来改变之前分配的内存块的大小。它可能原地扩展,也可能搬到一个新地址。

我曾经踩过一个坑:直接用 p = realloc(p, new_size)。如果 realloc 失败,它会返回 NULL,但原来的内存块还在。这么写会把原来的指针弄丢,造成内存泄漏。

避坑指南:永远不要直接 p = realloc(p, ...)。应该用一个临时变量。
int* new_p = (int*)realloc(p, new_size * sizeof(int));
if (new_p == NULL) {
    // realloc 失败,原来的 p 仍然有效
    // 这里要处理错误,但不要 free(p),因为 p 还在
    fprintf(stderr, "realloc 失败\n");
} else {
    p = new_p;  // 只有成功时才更新指针
}

19.1.4 free:归还内存

void free(void* ptr);

free 把之前分配的内存还给系统。注意:只能 free 由 malloc/calloc/realloc 返回的指针。free 一个已经 free 过的指针,或者 free 一个栈上的指针,都是未定义行为。

好习惯:free 之后立即把指针置为 NULL。这样即使不小心再次 free,也不会出问题。
free(p);
p = NULL;  // 防止野指针

19.2 内存泄漏:看不见的杀手

内存泄漏是指程序分配了内存,但不再使用后没有释放。随着程序运行,泄漏的内存越来越多,最终可能导致系统内存耗尽。

你想想看,一个服务器程序跑上几个月,如果每天泄漏一点点,最后会怎样?嗯,崩溃。

19.2.1 常见的内存泄漏场景

  • 忘记 free:分配了内存,但函数返回前忘了释放。
  • 丢失指针:把指针重新赋值,原来的内存块再也找不到了。
  • 异常路径:函数中有多个 return 点,有些路径忘了 free。
// 典型的内存泄漏
void func() {
    int* p = (int*)malloc(100 * sizeof(int));
    if (some_condition) {
        return;  // 忘了 free(p)!
    }
    // 使用 p
    free(p);
}

19.2.2 如何检测内存泄漏

说实话,靠肉眼审查代码很难发现所有泄漏。我建议用工具。

工具 平台 说明
Valgrind Linux 最常用的内存检测工具,能报告泄漏位置
AddressSanitizer GCC/Clang 编译时加 -fsanitize=address,运行时检测
CRT 调试堆 Windows (MSVC) 使用 _CrtDumpMemoryLeaks() 检测
我的经验:在开发阶段就开启 AddressSanitizer。它不仅能检测泄漏,还能检测越界访问、use-after-free 等问题。说白了,这是性价比最高的做法。

19.3 内存泄漏的预防策略

预防永远比事后检测更省心。我总结了几条实用原则。

19.3.1 谁分配,谁释放

在函数设计时,明确约定:哪个函数分配了内存,就由哪个函数负责释放。如果函数返回一个动态分配的指针,文档里必须写清楚“调用者需要 free 返回值”。

19.3.2 使用 RAII 思想(C语言版)

C语言没有 RAII,但我们可以模仿。比如写一个结构体,把分配和释放封装成一对函数。

typedef struct {
    int* data;
    size_t size;
} DynamicArray;

DynamicArray* array_create(size_t size) {
    DynamicArray* arr = (DynamicArray*)malloc(sizeof(DynamicArray));
    if (arr == NULL) return NULL;
    arr->data = (int*)calloc(size, sizeof(int));
    if (arr->data == NULL) {
        free(arr);
        return NULL;
    }
    arr->size = size;
    return arr;
}

void array_destroy(DynamicArray* arr) {
    if (arr) {
        free(arr->data);
        free(arr);
    }
}

19.3.3 编码规范

  • 每个 malloc/calloc/realloc 都要有对应的 free。
  • free 后立即置 NULL。
  • 在函数开头就规划好所有退出路径,确保每条路径都释放了内存。
  • 使用 goto 统一处理错误清理(C语言中常见的模式)。
// 使用 goto 统一清理
int* p1 = NULL;
int* p2 = NULL;

p1 = (int*)malloc(100 * sizeof(int));
if (p1 == NULL) goto cleanup;

p2 = (int*)malloc(200 * sizeof(int));
if (p2 == NULL) goto cleanup;

// 正常使用
// ...

cleanup:
    free(p1);
    free(p2);
    return;

19.4 动态内存分配的核心流程图

下面这张图展示了动态内存分配与释放的完整生命周期,以及内存泄漏是如何发生的。

动态内存分配生命周期 malloc / calloc 使用内存 free 内存泄漏 忘记 free realloc 野指针 / 段错误 free 后未置 NULL 正常路径:malloc → 使用 → free 异常路径:忘记 free → 内存泄漏 异常路径:free 后继续使用 → 野指针 建议:每个 malloc 对应一个 free,free 后立即置 NULL,使用 Valgrind 或 AddressSanitizer 检测

19.5 总结

动态内存分配是C语言的精髓,也是最容易出bug的地方。说白了,就三条原则:

  • 申请了就要释放——每个 malloc/calloc/realloc 都要有对应的 free。
  • 释放了就别再用——free 后置 NULL,防止野指针。
  • 用工具辅助检查——Valgrind 和 AddressSanitizer 是你的好朋友。

我曾经在一个嵌入式项目里,因为一个内存泄漏导致设备运行三天后死机。查了两天才找到原因——一个函数里的 malloc 忘了 free。从那以后,我写代码时都会在函数开头就规划好所有退出路径,确保每条路径都释放了内存。

最后一个小技巧:写代码时,先写 free,再写 malloc。这样你就不会忘记释放了。

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