3. 动态内存分配
大家好,我是老张。今天咱们聊聊动态内存分配。
说实话,指针越界里最让人头疼的,就是动态内存这块。我见过太多项目,上线跑了两周突然崩溃,查到最后都是堆内存的问题。你想想看,栈上的变量出了作用域就自动销毁,但堆上的内存你得亲手释放。忘了释放?内存泄漏。释放早了?悬空指针。释放两次?double free,直接崩。
嗯,咱们一个一个说。
3.1 堆与栈的区别
很多新手搞不清堆和栈的区别。我简单说:栈是系统管的,堆是你自己管的。
| 对比项 | 栈(Stack) | 堆(Heap) |
|---|---|---|
| 分配方式 | 编译器自动分配释放 | 程序员手动分配释放 |
| 大小限制 | 较小(通常几MB) | 较大(取决于系统内存) |
| 分配效率 | 高(直接移动栈顶指针) | 低(需要查找空闲块) |
| 生命周期 | 函数返回后自动销毁 | 直到手动释放或程序结束 |
| 典型问题 | 栈溢出 | 内存泄漏、野指针 |
我个人习惯:能用栈就别用堆。栈上分配快,不用操心释放,出作用域自动清理。但有些场景没办法——比如你需要在函数返回后继续使用那块内存,或者数据量太大栈装不下,这时候就得请出堆了。
3.2 malloc / calloc / realloc / free
这四个函数是 C 语言动态内存管理的核心。咱们一个个看。
3.2.1 malloc —— 最常用的分配器
void* malloc(size_t size);
malloc 分配一块连续内存,返回 void* 指针。注意:它不初始化内存。里面的数据是随机的,可能是上次用过的残留值。
int* p = (int*)malloc(10 * sizeof(int));
if (p == NULL) {
// 处理分配失败
return -1;
}
// 使用 p[0] 到 p[9]
free(p);
这里有个坑:一定要检查返回值。我在项目中遇到过,嵌入式设备内存紧张,malloc 返回 NULL 的情况并不罕见。不检查就直接用,解引用空指针,程序直接崩。
3.2.2 calloc —— 带清零的分配
void* calloc(size_t num, size_t size);
calloc 和 malloc 的区别就两点:
- 它会把分配的内存全部清零
- 它接受两个参数:元素个数和每个元素大小
我什么时候用 calloc?当我要分配一个结构体数组,并且希望所有字段初始为 0 的时候。比如:
struct Student* students = (struct Student*)calloc(50, sizeof(struct Student));
// 所有学生的 name 为 NULL,age 为 0,score 为 0.0
但注意:calloc 比 malloc 慢,因为它多了一步清零操作。如果你马上要覆盖所有数据,用 malloc 更合适。
3.2.3 realloc —— 调整已分配内存的大小
void* realloc(void* ptr, size_t new_size);
realloc 可以扩大或缩小之前分配的内存。我经常用它来实现动态数组:
int* arr = (int*)malloc(5 * sizeof(int));
// 用着用着发现不够了
int* new_arr = (int*)realloc(arr, 10 * sizeof(int));
if (new_arr == NULL) {
// realloc 失败,原内存还在
free(arr);
return -1;
}
arr = new_arr;
arr = (int*)realloc(arr, 20 * sizeof(int)); // 如果失败,arr 变成 NULL,原内存泄漏
正确做法:先用临时变量接收返回值,检查成功后再赋值。
3.2.4 free —— 释放内存
void free(void* ptr);
free 把内存归还给系统。但有个关键点:free 之后,指针的值还在。它指向的那块内存已经不属于你了,但指针变量本身没有被置 NULL。
我曾经犯过一个错:free 之后忘了把指针置 NULL,后来又在别的地方判断 if (p != NULL) 就以为内存还在,直接往里写数据。结果呢?那块内存可能已经被分配给别的变量了,数据被覆盖得一塌糊涂。查了两天才找到原因。
free(p);
p = NULL;
这样即使后面误用了,至少不会解引用野指针,顶多是个空指针,好查得多。
3.3 野指针与悬空指针
这两个概念经常被混用,但其实是两码事。
3.3.1 野指针(Wild Pointer)
野指针是未初始化的指针。它指向哪里?不知道。可能是任何地址。
int* p; // 野指针!没有初始化
*p = 10; // 危险!不知道写到了哪里
解决办法很简单:声明指针时立即初始化。如果暂时没有目标,就置为 NULL。
int* p = NULL; // 好习惯
3.3.2 悬空指针(Dangling Pointer)
悬空指针是指向已释放内存的指针。它曾经有效,但现在那块内存已经不属于你了。
int* p = (int*)malloc(sizeof(int));
free(p);
// 此时 p 是悬空指针
*p = 42; // 未定义行为!可能崩溃,可能静默覆盖其他数据
悬空指针比野指针更隐蔽,因为 p 曾经有效,你可能会误以为它还能用。
- 野指针:从来就没指向过有效内存
- 悬空指针:曾经指向有效内存,但被释放了
3.4 实战避坑指南
说了这么多理论,来点实际的。这些都是我踩过的坑:
- 忘记 free —— 内存泄漏。程序跑久了,内存一点点被吃掉,最后 OOM 被杀。我建议每次 malloc 之后,立刻写对应的 free,就像写大括号一样形成肌肉记忆。
- 重复 free —— double free。第一次 free 没问题,第二次 free 时内存管理器的元数据已经被破坏了,直接 crash。解决办法:free 后置 NULL,再 free(NULL) 是安全的。
- 越界访问堆内存 —— 比如 malloc 了 10 个 int,却写了第 11 个。堆内存的元数据通常紧挨着用户数据,你越界写一个字节,可能就把管理信息破坏了。下次 free 的时候,程序就炸了。
- 在栈上返回局部变量的地址 —— 这个其实不属于动态内存,但太常见了,我提一嘴:
int* bad_func() {
int a = 10;
return &a; // 危险!a 是局部变量,函数返回后销毁
}
解决办法:要么用 static,要么用 malloc 在堆上分配。
3.5 知识体系图
下面这张图总结了本章的核心逻辑,我手绘的 SVG,你凑合看:
嗯,这张图把今天的内容串起来了。你仔细看看,左边是堆栈对比,中间是四个分配函数,右边是两种危险指针。底部那句话是我多年经验总结——谁分配,谁释放;释放后置 NULL。记住这十个字,能避开 80% 的动态内存问题。
本章小结:
- 栈自动管理,堆手动管理——能用栈就别用堆
- malloc 不初始化,calloc 清零,realloc 可调整大小
- free 后指针要置 NULL,防止悬空指针
- 野指针未初始化,悬空指针指向已释放内存——两者都要避免
好了,今天就到这儿。动态内存这块水很深,但掌握了这些基础,至少不会犯低级错误。下次咱们聊数组越界——那个更隐蔽,也更致命。
可可精品资料库