第1章:动态内存管理——malloc、calloc、realloc、free函数、动态数组的实现、内存泄漏的避免

动态内存管理,说白了就是让程序在运行时按需申请和释放内存。我刚开始学C语言时,总觉得静态数组够用了,直到有一次做嵌入式项目,需要处理不定长的传感器数据流……嗯,那次之后我才真正体会到动态内存的价值。

1.1 为什么需要动态内存?

你想想看,写程序时经常遇到这种情况:不知道用户会输入多少数据,也不知道文件有多大。如果提前声明一个很大的数组,比如 int arr[10000],那内存浪费得心疼。如果声明小了,又可能溢出。

动态内存正好解决这个矛盾。程序跑起来后,根据实际需要,随时申请内存,用完了再释放。这就是所谓的「按需分配」。

核心区别:

  • 静态分配:编译时确定大小,存在栈上,自动释放
  • 动态分配:运行时确定大小,存在堆上,手动释放

1.2 malloc——最常用的内存申请函数

malloc 的全称是 memory allocation。它的原型很简单:

void *malloc(size_t size);

它申请一块连续的内存空间,返回指向这块内存的指针。注意,它不会初始化内存——里面的数据是随机的。

我个人习惯这样用:

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

这里有个坑:一定要检查返回值是否为NULL。我在项目中遇到过几次,嵌入式设备内存紧张时,malloc真的会返回NULL。如果不检查,直接解引用空指针,程序立马崩溃。

避坑指南:

我曾经在一个数据采集项目中,因为没检查malloc返回值,导致设备运行三天后突然死机。排查了一整天,才发现是内存碎片导致malloc失败。从那以后,我每次malloc后面必跟NULL检查。

1.3 calloc——带初始化的内存申请

calloc 和 malloc 很像,但有两个区别:

  1. 它会将申请的内存全部初始化为0
  2. 它接受两个参数:元素个数和每个元素的大小
void *calloc(size_t nmemb, size_t size);

举个例子:

int *p = (int *)calloc(10, sizeof(int));
// 这10个int全部是0

什么时候用calloc?我个人觉得,如果你需要清零的内存块,直接用calloc比malloc+memset更省事。但要注意,calloc因为要做清零操作,速度会比malloc慢一点。

函数 初始化 参数 性能
malloc 不初始化(随机值) 1个参数:大小
calloc 初始化为0 2个参数:个数、大小 稍慢

1.4 realloc——调整已申请的内存大小

有时候,申请的内存不够用了,或者太多了,需要调整大小。realloc 就是干这个的:

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

它的行为有点意思:

  • 如果新大小比原来大,它会尝试扩展。如果后面有空间,直接扩展;如果没有,就重新找一块更大的空间,把旧数据拷贝过去,然后释放旧内存。
  • 如果新大小比原来小,它可能会缩小,也可能不动。
  • 如果ptr是NULL,realloc相当于malloc。
  • 如果new_size是0,realloc相当于free。

这里有个重要技巧:

// 错误用法
int *p = (int *)malloc(10 * sizeof(int));
p = (int *)realloc(p, 20 * sizeof(int)); // 如果realloc失败,返回NULL,p原来的地址就丢了!

// 正确用法
int *p = (int *)malloc(10 * sizeof(int));
int *tmp = (int *)realloc(p, 20 * sizeof(int));
if (tmp != NULL) {
    p = tmp;  // 只有成功才赋值
} else {
    // 处理失败,p仍然有效
}

我曾经在代码里犯过这个错误,realloc失败后原指针丢了,导致内存泄漏。嗯,这个教训挺深刻的。

1.5 free——释放内存,防止泄漏

有借有还,再借不难。动态申请的内存,用完后必须用 free 释放:

void free(void *ptr);

使用free时要注意:

  • 只能释放由malloc/calloc/realloc申请的内存
  • 不能重复释放同一块内存(double free)
  • 释放后,指针变成野指针,建议置为NULL
int *p = (int *)malloc(10 * sizeof(int));
// ... 使用p ...
free(p);
p = NULL;  // 好习惯,防止野指针

个人经验:

我习惯在free之后立即将指针置NULL。这样即使后面不小心再次free,free(NULL)是安全的。这个习惯帮我避免了好几次double free的bug。

1.6 动态数组的实现

动态数组是动态内存管理最经典的应用。说白了,就是一个可以自动扩容的数组。下面是一个简单的实现:

typedef struct {
    int *data;
    int size;     // 当前元素个数
    int capacity; // 当前容量
} DynamicArray;

// 初始化
DynamicArray* da_create(int init_capacity) {
    DynamicArray *da = (DynamicArray *)malloc(sizeof(DynamicArray));
    da->data = (int *)malloc(init_capacity * sizeof(int));
    da->size = 0;
    da->capacity = init_capacity;
    return da;
}

// 添加元素(自动扩容)
void da_append(DynamicArray *da, int value) {
    if (da->size >= da->capacity) {
        // 容量翻倍
        da->capacity *= 2;
        int *tmp = (int *)realloc(da->data, da->capacity * sizeof(int));
        if (tmp == NULL) {
            fprintf(stderr, "扩容失败!\n");
            exit(1);
        }
        da->data = tmp;
    }
    da->data[da->size++] = value;
}

// 释放
void da_free(DynamicArray *da) {
    free(da->data);
    free(da);
}

为什么扩容时翻倍而不是加固定大小?因为翻倍可以保证均摊时间复杂度为O(1)。你想想看,如果每次只加1个元素,那每次添加都要realloc,效率太低了。

1.7 内存泄漏的避免

内存泄漏是C语言程序中最头疼的问题之一。程序跑着跑着,内存越来越少,最后崩溃。我见过最夸张的一次,一个服务器程序运行一个月后,内存占用从50MB涨到了2GB。

避免内存泄漏的几个原则:

  1. 谁申请,谁释放:哪个函数malloc的,就由哪个函数free。
  2. 成对出现:写malloc的时候,就写好对应的free。
  3. 使用工具检测:Valgrind是Linux下检测内存泄漏的神器。
  4. 封装成模块:把动态内存操作封装成函数,统一管理。

常见内存泄漏场景:

  • 函数中malloc了内存,但提前return了,没来得及free
  • 结构体中有指针成员,释放结构体时忘了释放指针指向的内存
  • 循环中不断malloc,但只在循环外free了一次

1.8 知识体系总览

下面这张图总结了本章的核心内容:

动态内存管理知识体系 动态内存管理 malloc calloc realloc free 动态数组实现 内存泄漏避免 ⚠ 检查NULL返回值 ⚠ 释放后置NULL ⚠ 避免double free

1.9 总结

动态内存管理是C语言的精髓之一。malloc、calloc、realloc、free这四个函数,用好了能让程序灵活高效,用不好就会埋下各种坑。我个人觉得,掌握动态内存的关键就三点:

  • 申请时检查返回值
  • 释放后置空指针
  • 保持申请和释放的成对性

做到这三点,大部分内存问题都能避免。至于动态数组的实现,它是个很好的练习项目,建议你亲手写一遍,体会一下realloc的妙用。


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