第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 很像,但有两个区别:
- 它会将申请的内存全部初始化为0
- 它接受两个参数:元素个数和每个元素的大小
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。
避免内存泄漏的几个原则:
- 谁申请,谁释放:哪个函数malloc的,就由哪个函数free。
- 成对出现:写malloc的时候,就写好对应的free。
- 使用工具检测:Valgrind是Linux下检测内存泄漏的神器。
- 封装成模块:把动态内存操作封装成函数,统一管理。
常见内存泄漏场景:
- 函数中malloc了内存,但提前return了,没来得及free
- 结构体中有指针成员,释放结构体时忘了释放指针指向的内存
- 循环中不断malloc,但只在循环外free了一次
1.8 知识体系总览
下面这张图总结了本章的核心内容:
1.9 总结
动态内存管理是C语言的精髓之一。malloc、calloc、realloc、free这四个函数,用好了能让程序灵活高效,用不好就会埋下各种坑。我个人觉得,掌握动态内存的关键就三点:
- 申请时检查返回值
- 释放后置空指针
- 保持申请和释放的成对性
做到这三点,大部分内存问题都能避免。至于动态数组的实现,它是个很好的练习项目,建议你亲手写一遍,体会一下realloc的妙用。