C 语言给了你直接向操作系统要内存的权力,但也要求你像个负责任的成年人一样:借了东西,必须记得还。
思维导图




一、 栈与堆的区别
内存主要分为两个区域:栈 和堆。理解它们的区别是防止内存错误的根本。
1.1 对比表
| 特性 | 栈 | 堆 |
|---|---|---|
| 管理方式 | 自动分配,自动释放 | 手动申请,手动释放 |
| 生命周期 | 函数返回即销毁 | 直到 free() 或程序结束 |
| 空间大小 | 很小 (通常几 MB) | 很大 (取决于物理内存) |
| 使用场景 | 局部变量、函数参数 | 大数组、动态数据结构 (链表) |
1.2 代码对比
c
void stack_vs_heap() {
// 1. 栈分配:快,但空间有限,函数结束即亡
int stack_arr[100];
// 2. 堆分配:慢,但空间大,生命周期由你控制
// 向系统申请 400 字节,返回首地址
int *heap_arr = (int*)malloc(100 * sizeof(int));
if (heap_arr != NULL) {
heap_arr[0] = 99; // 像数组一样使用
free(heap_arr); // 用完必须还!
}
}
二、 内存分配三剑客:malloc, calloc, realloc
2.1 malloc:最基础的分配
原型: void* malloc(size_t size);
注意: 它申请的内存里是垃圾值 (未初始化)。
规范: 永远要检查返回值是否为 NULL(防止内存耗尽)。
代码模板:
c
int *p = (int*)malloc(10 * sizeof(int)); // 推荐写法:sizeof(类型)
if (p == NULL) {
fprintf(stderr, "内存申请失败!\n");
return -1;
}
2.2 calloc:自带清洁功能的分配
原型: void* calloc(size_t num, size_t size);
特点: 它会自动把内存全部初始化为 0 。
代价: 稍微慢一点点。
代码示例:
c
// 申请 10 个 int,且全部清零
int *p = (int*)calloc(10, sizeof(int));
// 此时 p[0] 到 p[9] 都是 0
2.3 free:归还内存
原型: void free(void* ptr);
规则:
1.只能 free 堆内存(malloc/calloc/realloc 出来的)
2.不能重复 free
3.
free(NULL)是安全的(什么都不做)
最佳实践:
c
free(p);
p = NULL; // 养成好习惯,防止悬空指针
三、 realloc 的正确用法
当你发现申请的数组不够用了,realloc 可以帮你扩容。
3.1 致命陷阱:直接赋回原指针
错误写法:
c
// 如果 realloc 失败返回 NULL,原有的 p 就丢了!内存泄漏!
p = realloc(p, new_size);
3.2 正确写法:使用临时指针
c
// 1. 初始分配
int *arr = malloc(5 * sizeof(int));
// 2. 扩容
int new_size = 10 * sizeof(int);
// 先用临时指针接住返回值
int *temp = realloc(arr, new_size);
if (temp == NULL) {
// 扩容失败,arr 里的旧数据还在,可以决定怎么处理
printf("扩容失败\n");
} else {
// 扩容成功,更新 arr
arr = temp;
}
四、 常见内存错误专题
4.1 内存泄漏
现象: 借了不还。程序运行时间越长,占用内存越多,最后系统崩溃。
复现:
c
void leak() {
int *p = malloc(100);
return; // 忘了 free(p)!这 100 字节这就没人能用了
}
4.2 释放后使用
现象: 已经还回去了,还想去拿东西。
复现:
c
free(p);
// p 变成了悬空指针
*p = 10; // 危险!这块内存可能已经分配给别人了,你这是在改别人的数据!
4.3 重复释放
现象: 同一张支票兑换了两次。
复现:
c
free(p);
free(p); // 崩溃!
解法: free(p); p = NULL; 第二次 free(NULL) 是安全的。
4.4 堆越界
复现:
c
char *s = malloc(5); // 申请 5 字节
strcpy(s, "Hello"); // 写入 "Hello\0" (6 字节) -> 越界!破坏堆元数据
五、 练习题
题目 1: malloc(0) 会返回什么?是 NULL 吗?
题目 2: 下面代码有内存泄漏吗?
c
void func() {
int *p = malloc(10);
p = malloc(20);
free(p);
}
题目 3: 为什么 realloc(ptr, 0) 等价于 free(ptr)(在某些标准下)?
题目 4: calloc 分配的内存一定全是 0 吗?
题目 5: 下面代码有什么严重问题?
c
int *arr = malloc(10 * sizeof(int));
arr = realloc(arr, 20 * sizeof(int));
if (!arr) return;
题目 6: 可以在函数里 malloc,在函数外 free 吗?
题目 7: 栈溢出 (Stack Overflow) 和 堆溢出 (Out of Memory) 有什么区别?
题目 8: free(p) 之后,p 的值会变成 NULL 吗?
题目 9: 结构体包含指针成员时,如何正确释放?
c
struct Node { int *data; };
struct Node *p = malloc(sizeof(struct Node));
p->data = malloc(100);
题目 10: 为什么不推荐用 void *p = malloc(100); 然后直接 p[0]?
题目 11: 什么是内存碎片?
题目 12: 编写一个函数 create_array(n),动态创建一个长度为 n 的 int 数组并初始化为 0-n。
题目 13: alloca 函数是在哪里分配内存?
题目 14: 为什么说 strcpy 到 malloc 的空间时特别容易出 Bug?
题目 15: 怎么检测内存泄漏?(面试常考)
六、 解析
题 1 解析
答案: 标准规定是"实现定义"。可能返回 NULL,也可能返回一个唯一的指针(可以被 free)。
详解:
尽量避免写
malloc(0)。
题 2 解析
答案: 有。
详解:
p指向了第一次申请的 10 字节。紧接着p又指向了新的 20 字节。那这前 10 字节的地址就丢失了,再也无法释放。
题 3 解析
答案: 是的。
详解:
如果 size 为 0,
realloc会释放旧内存并返回 NULL(虽然 C23 标准可能修改此行为,但老代码常利用此特性)。
题 4 解析
答案: 是的。
详解:
calloc保证所有位都是 0。这对于整数来说就是 0,对指针来说就是 NULL,对浮点数通常是 0.0。
题 5 解析
答案: 内存泄漏风险。
详解:
如果
realloc失败返回 NULL,赋给arr后,原来的内存地址就丢了,变成了无人认领的孤魂野鬼。
题 6 解析
答案: 可以,但这需要良好的文档说明。
详解:
谁申请谁释放是原则。如果函数里申请,必须明确告诉调用者:"这个指针的所有权交给你了,记得 free。"
题 7 解析
答案:
栈溢出:通常是因为递归过深或局部数组太大(如
int a[1000000])。堆溢出:malloc 申请了超过物理内存限制的空间。
题 8 解析
答案: 不会。
详解:
free只是告诉操作系统收回这块地。p依然存着那个地址值(变成了悬空指针)。所以手动p = NULL很重要。
题 9 解析
答案: 先释放成员,再释放结构体。
详解:
c
free(p->data); // 先释放里面
free(p); // 再释放外面
顺序反了会导致
p->data无法访问。
题 10 解析
答案: void* 不能进行指针算术或解引用。
详解:
必须强转为具体类型(如
int*)才能访问。
题 11 解析
答案: 堆内存中即使有总空闲空间,但都不是连续的,导致无法申请大块连续内存。
题 12 解析
答案:
c
int* create_array(int n) {
int *arr = malloc(n * sizeof(int));
if (arr) {
for(int i=0; i<n; i++) arr[i] = i;
}
return arr;
}
题 13 解析
答案: 栈上。
详解:
alloca是非标准函数,它在栈上动态分配,函数结束自动释放。优点是快,缺点是容易爆栈。
题 14 解析
答案: 忘了算 \0。
详解:
strlen("abc")是 3。malloc(3)放不下,必须malloc(strlen(s) + 1)。
题 15 解析
答案:
- 静态分析工具:Cppcheck。
- 动态分析工具:Valgrind (Linux 神器),AddressSanitizer (编译器自带
-fsanitize=address)。

日期:2025年2月13日
专栏:C语言