动态内存管理
动态内存管理让对象的创建时机不再受限于代码块作用域。它是构建容器、缓存和运行期可变数据结构的基础能力,同时也是 C 程序最常见的错误来源之一。
1. 四个核心接口
malloc 申请一段未初始化存储,calloc 申请并清零,realloc 调整已分配存储大小,free 释放已分配存储。它们都定义在 <stdlib.h> 中,返回值与失败语义必须显式检查。
c
#include <stdlib.h>
int *buf = malloc(16 * sizeof *buf);
if (buf == NULL) {
return;
}
free(buf);
buf = NULL;1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
把指针置为 NULL 不是语义必须,但它可以减少重复释放和悬垂访问风险。
2. realloc 的安全模式
realloc 失败时会返回 NULL,同时原指针仍然有效。因此不能直接覆盖原指针,应该先用临时指针接收结果。
c
#include <stdlib.h>
int *grow(int *old_buf, size_t new_count) {
int *new_buf = realloc(old_buf, new_count * sizeof *new_buf);
if (new_buf == NULL) {
return old_buf;
}
return new_buf;
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
这种写法可以确保失败时不丢失原有存储地址。
3. 常见错误与规避
最常见问题包括:忘记释放导致泄漏,释放后继续访问导致悬垂引用,重复释放导致运行时崩溃,越界写入破坏堆元数据。规避思路很直接:统一所有权、固定释放路径、在接口层写清责任边界,并使用工具链做持续检测(如 ASan、Valgrind)。
4. 工程建议
把“申请成功检查”和“失败回滚路径”当成模板代码,而不是临场补丁。动态内存本质是资源管理问题,不是语法问题;资源模型一旦清楚,代码复杂度会明显下降。
5. 大小计算溢出检查
申请数组对象时,除了检查返回值,还应先检查“元素个数 × 元素大小”是否溢出。若乘法先溢出再传给 malloc,得到的存储大小会小于预期,后续写入极易越界。
c
#include <stddef.h>
#include <stdlib.h>
int *alloc_n(size_t n) {
if (n > SIZE_MAX / sizeof(int)) {
return NULL;
}
return malloc(n * sizeof(int));
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
6. 所有权边界
动态存储最容易出错的点是“谁负责释放”。接口文档里若能明确“创建方释放”还是“调用方释放”,调用链会清楚很多,也更容易避免悬垂引用和重复释放。