从C语言标准揭秘C指针:第 10 章:指针与动态内存:堆区内存的生命周期管理
各位同学,上一章我们学习了多维数组的动态实现方案 —— 无论是多层指针(int***
)还是单指针 + 偏移,本质上都依赖堆区的动态内存分配。今天这一章,我们就聚焦 C 语言内存管理的核心:动态内存与指针的配合使用。
之前我们常用的局部变量(比如int a;
、int arr[10];
)都存在栈区,栈区内存有两个硬限制:一是大小在编译期就固定了,比如arr[10]
的长度 10 不能改;二是函数执行完会自动回收,生命周期短。而堆区内存正好能突破这些限制 —— 运行时能根据需求灵活分配大小,还能手动控制生命周期。接下来,我们会结合 C 标准,详细拆解malloc
/calloc
/realloc
/free
这四个核心函数,实战动态数组的实现,最后重点解决 “内存泄漏”“野指针” 这些致命问题,确保大家能安全、高效地管理堆区内存。
10.1 动态内存的意义:突破栈区 “编译期固定大小” 的限制
在讲具体函数前,我们先明确一个基础:程序运行时的内存主要分四个区 —— 栈区、堆区、全局区、只读数据区。我们之前打交道最多的是栈区,但堆区才是动态内存的 “主战场”,两者的差异非常关键,大家一定要记清:
内存分区 | 大小确定时机 | 生命周期 | 管理方式 |
---|---|---|---|
栈区 | 编译期 | 函数执行结束自动回收 | 系统自动管理 |
堆区 | 运行时 | 手动申请、手动释放 | 程序员手动管理 |
C 标准(ISO/IEC 9899:2011 §7.22.3)明确说:动态内存函数的作用,就是 “让程序在运行时获取和释放内存,支持可变大小的对象存储”。
我们用一段代码直观对比栈区固定数组和堆区动态数组的差异,大家看注释就能明白核心区别:
c代码:
#include <stdio.h>
#include <stdlib.h> // 必须包含,动态内存函数的声明在这里int main() {// 1. 栈区固定数组:长度10在编译时就写死了,哪怕只用5个元素,也占10个int的空间int stack_arr[10]; printf("栈区数组:固定大小10,无法根据用户输入修改\n");// 2. 堆区动态数组:长度由用户运行时输入决定int n;printf("请输入动态数组的长度:");scanf("%d", &n); // 比如用户输入5,就只分配5个int的内存// 申请n个int的堆区内存,用int*指针接收(malloc返回void*,需强制转成对应类型)int* heap_arr = (int*)malloc(n * sizeof(int));// 【关键步骤】必须检查内存是否申请成功!堆区空间不足时,malloc会返回NULLif (heap_arr == NULL) { printf("内存申请失败!\n");return 1; // 申请失败就退出,避免后续操作NULL指针}printf("堆区数组:根据输入n=%d,按需分配了%d个int的空间\n", n, n);// 使用动态数组:和栈区数组一样,用下标访问就行for (int i = 0; i < n; i++) {heap_arr[i] = i + 1; // 给动态数组赋值1~n}// 打印动态数组内容printf("动态数组内容:");for (int i = 0; i < n; i++) {printf("%d ", heap_arr[i]);}printf("\n");// 【重中之重】手动释放堆区内存!不释放会导致内存泄漏free(heap_arr);heap_arr = NULL; // 释放后把指针置为NULL,避免变成野指针return 0;
}
简单总结动态内存的核心意义:
- 解决 “编译期不知道内存大小” 的问题 —— 比如用户输入的长度、从文件读取的数据长度;
- 避免内存浪费 —— 用多少申请多少,不用了就释放;
- 支持内存长期存活 —— 堆区内存不会随函数结束消失,能在多个函数间共享。
10.2 动态内存函数详解(定义在<stdlib.h>
)
C 标准库在<stdlib.h>
里提供了 4 个动态内存核心函数,这四个函数是 “基石”,每个的用法、参数、注意事项都要记牢。我们一个一个来拆解。
10.2.1 malloc:申请指定字节数的未初始化内存
malloc
是最基础的 “内存申请函数”,功能很简单:从堆区拿一块指定字节数的连续内存,返回指向这块内存的指针。
函数原型(C 标准 §7.22.3.1)
c代码:
void* malloc(size_t size);
- 参数
size
:要申请的内存 “总字节数”—— 比如要存 5 个 int,就写5 * sizeof(int)
(sizeof(int)
获取单个 int 的字节数,避免硬写 4,跨平台更安全); - 返回值:申请成功返回
void*
指针(需要强制转成具体类型,比如int*
),失败返回NULL
; - 注意点:申请的内存是 “未初始化” 的,里面是随机的 “垃圾值”,用之前必须手动赋值。
示例代码:用 malloc 实现动态一维数组
c代码:
#include <stdio.h>
#include <stdlib.h>int main() {int n = 3; // 要存3个int元素// 1. 计算总字节数:元素个数 × 单个元素字节数size_t total_bytes = n * sizeof(int);printf("申请的总字节数:%zu 字节(%d个int)\n", total_bytes, n);// 2. 调用malloc申请内存,强制转成int*(因为要存int)int* p = (int*)malloc(total_bytes);// 3. 必须检查返回值!NULL说明内存不够了if (p == NULL) {printf("内存申请失败!程序退出。\n");return 1; // 退出程序,避免后续操作NULL指针}// 4. 手动初始化:malloc的内存是垃圾值,不赋值直接用会出错for (int i = 0; i < n; i++) {p[i] = i * 10; // 赋值0、10、20}// 5. 打印内容printf("动态内存内容:");for (int i = 0; i < n; i++) {printf("%d ", p[i]); // 输出:0 10 20}printf("\n");// 6. 释放内存:不用了一定要还,不然内存就漏了free(p);p = NULL; // 释放后把指针置NULL,避免变成野指针return 0;
}
常见错误提醒
- 忘记检查 NULL:如果堆区满了,
malloc
返回 NULL,后续解引用*p
会直接崩溃; - 字节数算错:比如写
malloc(n)
而不是malloc(n * sizeof(int))
—— 只申请了 n 字节,不够存 n 个 int(比如 int 占 4 字节,n=3 时只申请 3 字节,不够存 3 个 int); - 释放后没置 NULL:
free(p)
后,p 还指向原来的地址,但地址已经无效了,变成 “野指针”,后续误操作会出问题。
10.2.2 calloc:申请内存并自动初始化为 0
calloc
和malloc
功能类似,都是申请堆区内存,但它多了一个 “自动初始化” 的功能 —— 申请的内存会被自动设为 0,不用手动赋值,很方便。
函数原型(C 标准 §7.22.3.2)
c代码:
void* calloc(size_t nmemb, size_t size);
- 参数
nmemb
:元素个数(比如要存 3 个 int,就写 3); - 参数
size
:单个元素的字节数(比如sizeof(int)
); - 返回值:成功返回
void*
指针,失败返回NULL
; - 总字节数:
nmemb × size
—— 和malloc(nmemb * size)
申请的内存大小完全一样,但calloc
会自动把内存清 0; - 优势:避免手动初始化的麻烦,尤其适合大数组(比如 1000 个元素,手动赋值 0 太费劲)。
示例代码:对比 malloc 和 calloc
c代码:
#include <stdio.h>
#include <stdlib.h>int main() {int n = 3;// 1. malloc申请的内存:未初始化,内容是随机垃圾值int* p_malloc = (int*)malloc(n * sizeof(int));if (p_malloc == NULL) { exit(1); } // exit(1)也是退出程序,和return 1类似printf("malloc的内存(未初始化):");for (int i = 0; i < n; i++) {printf("%d ", p_malloc[i]); // 输出随机值,比如32767 0 4195730}printf("\n");// 2. calloc申请的内存:自动初始化为0int* p_calloc = (int*)calloc(n, sizeof(int));if (p_calloc == NULL) { exit(1); }printf("calloc的内存(初始化为0):");for (int i = 0; i < n; i++) {printf("%d ", p_calloc[i]); // 肯定输出:0 0 0}printf("\n");// 释放内存,别忘了置NULLfree(p_malloc);p_malloc = NULL;free(p_calloc);p_calloc = NULL;return 0;
}
适用场景
需要内存初始化为 0 的场景 —— 比如计数器(初始值 0)、存储成绩的数组(默认 0 分)、缓存数据(默认空值)。
10.2.3 realloc:调整已分配内存的大小
realloc
是动态内存的 “灵活担当”—— 它能修改之前用malloc
/calloc
/realloc
申请的内存大小,支持 “扩大” 或 “缩小”,比如把 3 个 int 的数组改成 5 个 int 的。
函数原型(C 标准 §7.22.3.4)
c代码:
void* realloc(void* ptr, size_t size);
- 参数
ptr
:之前申请的堆区内存指针(比如malloc
返回的指针);如果ptr
是NULL
,realloc
就等同于malloc(size)
; - 参数
size
:调整后的 “总字节数”(比如要扩成 5 个 int,就写5 * sizeof(int)
); - 返回值:成功返回指向 “新内存” 的
void*
指针,失败返回NULL
(注意:失败时,原来的ptr
指针还是有效的,不会被释放); - 核心逻辑(重点理解,避免踩坑):
- 如果要缩小内存(新 size < 原 size):直接截断多余部分,原内存的前 size 字节保留,多余的释放;
- 如果要扩大内存(新 size > 原 size):
- 情况 1:原内存后面有足够的连续空间 —— 直接在后面加内存,原内容不变;
- 情况 2:原内存后面空间不够 —— 在堆区找一块新的、够大的空间,把原内容拷贝过去,然后释放原内存。
示例代码:用 realloc 动态扩展数组
c代码:
#include <stdio.h>
#include <stdlib.h>int main() {int n = 3;// 1. 先用calloc申请3个int的内存(初始化为0)int* p = (int*)calloc(n, sizeof(int));if (p == NULL) { exit(1); }// 给初始内存赋值1、2、3for (int i = 0; i < n; i++) {p[i] = i + 1;}printf("初始内存(3个int):");for (int i = 0; i < n; i++) {printf("%d ", p[i]); // 输出:1 2 3}printf("\n");// 2. 扩展内存:从3个int扩到5个intint new_n = 5;size_t new_size = new_n * sizeof(int);// 【关键】用临时指针接收realloc的返回值!// 原因:如果realloc失败返回NULL,原指针p不会被覆盖,避免丢失原内存int* p_new = (int*)realloc(p, new_size);// 检查是否扩展成功if (p_new == NULL) { printf("内存扩展失败!\n");free(p); // 扩展失败,要释放原内存,避免泄漏p = NULL;return 1;}// 扩展成功:把原指针p更新为新地址p = p_new;// 3. 给新增的2个元素(索引3、4)赋值p[3] = 4;p[4] = 5;// 4. 打印扩展后的内存printf("扩展后内存(5个int):");for (int i = 0; i < new_n; i++) {printf("%d ", p[i]); // 输出:1 2 3 4 5(原内容保留)}printf("\n");// 释放内存free(p);p = NULL;return 0;
}
关键注意事项
- 用临时指针接收返回值:这是最容易踩的坑!如果直接写
p = realloc(p, new_size)
,一旦realloc
失败返回 NULL,p
就变成 NULL 了,原来的内存地址也丢了,导致内存泄漏; - 扩展后用新指针:
realloc
成功后,原指针p
可能已经无效了(比如原内存后面空间不够,realloc
会分配新地址并释放原内存),必须用新返回的p_new
; - 缩小内存要谨慎:截断的部分数据会永久丢失,而且找不回来。
10.2.4 free:释放堆区内存(必须与申请函数成对使用)
free
是唯一能 “归还” 堆区内存的函数 —— 不管是malloc
/calloc
/realloc
申请的内存,都必须用free
释放,否则会导致内存泄漏。
函数原型(C 标准 §7.22.3.3)
c代码:
void free(void* ptr);
- 参数
ptr
:要释放的堆区内存指针(必须是malloc
/calloc
/realloc
的返回值,或者NULL
); - 返回值:无;
- 核心规则(记牢,避免崩溃):
- 只能释放堆区内存 —— 不能释放栈区内存(比如
int a; free(&a);
会直接崩溃); free(NULL)
是安全的 —— 如果ptr
是NULL
,free
什么都不做,不会出错;- 不能重复释放 —— 同一内存地址不能
free
多次(会破坏堆区的内存管理结构,导致崩溃)。
- 只能释放堆区内存 —— 不能释放栈区内存(比如
示例代码:free 的正确与错误用法
c代码:
#include <stdio.h>
#include <stdlib.h>int main() {// 1. 正确用法:malloc和free成对用,释放后置NULLint* p1 = (int*)malloc(1 * sizeof(int));if (p1 == NULL) { exit(1); }*p1 = 10;printf("p1的值:%d\n", *p1); // 输出:10free(p1);p1 = NULL; // 释放后置NULL,避免野指针// 2. 错误用法1:释放栈区内存int a = 20;// free(&a); // 运行会崩溃!&a是栈区地址,free只认堆区内存// 3. 错误用法2:重复释放(未置NULL的情况)int* p2 = (int*)malloc(1 * sizeof(int));free(p2); // 第一次释放(正确)// free(p2); // 第二次释放(错误):p2仍指向原地址,重复释放会崩溃p2 = NULL; // 补救:释放后立即置NULLfree(p2); // 此时安全:free(NULL)不做任何操作// 4. 错误用法3:释放未申请的随机指针int* p3 = (int*)0x12345678; // 随便写的无效地址,未通过malloc申请// free(p3); // 运行会崩溃!该地址不是堆区合法内存// 5. 正确用法:free(NULL)安全int* p4 = NULL;free(p4); // 无任何操作,不会出错return 0;
}
为了方便大家记忆,我总结了一句口诀:“申请必检查,释放必置空,不重复释放,不释放栈区”—— 只要遵守这 16 个字,就能避开free
的大部分坑。
10.3 动态一维数组:用指针实现可变长度
动态一维数组是动态内存最基础的应用,核心逻辑很简单:用malloc
/calloc
申请内存,用指针接收,像静态数组一样用arr[i]
访问,最后用free
释放。它最适合 “运行时才知道长度” 的场景,比如用户输入的数组长度、从文件里读取的数据个数。
完整实战案例:用户输入长度的动态数组
下面我们写一个完整的例子,包含 “用户输入长度→申请内存→初始化→打印→扩展数组→释放内存” 的全流程,大家可以跟着敲一遍:
c代码:
#include <stdio.h>
#include <stdlib.h>// 函数1:初始化动态一维数组(给数组赋值1~n)
void init_dynamic_array(int* arr, int n) {for (int i = 0; i < n; i++) {arr[i] = i + 1; // 数组第i个元素赋值为i+1}
}// 函数2:打印动态一维数组
void print_dynamic_array(const int* arr, int n) {printf("动态数组(长度%d):", n);for (int i = 0; i < n; i++) {printf("%d ", arr[i]);}printf("\n");
}int main() {int n;// 1. 运行时获取用户输入的数组长度printf("请输入动态数组的长度:");scanf("%d", &n);// 2. 先检查输入是否合法(长度不能是0或负数)if (n <= 0) {printf("数组长度必须是正整数!\n");return 1;}// 3. 申请动态内存(用calloc,自动初始化为0,后续再赋值)int* dynamic_arr = (int*)calloc(n, sizeof(int));if (dynamic_arr == NULL) { // 申请失败就退出printf("内存申请失败,程序退出!\n");return 1;}// 4. 初始化动态数组(调用函数)init_dynamic_array(dynamic_arr, n);// 5. 打印动态数组(调用函数)print_dynamic_array(dynamic_arr, n);// 6. 扩展数组长度(比如从n扩展到n+2)int new_n = n + 2;// 用临时指针接收realloc返回值,避免丢失原内存int* temp = (int*)realloc(dynamic_arr, new_n * sizeof(int));if (temp == NULL) { // 扩展失败,保持原数组printf("数组扩展失败,保持原长度!\n");} else { // 扩展成功,更新指针和长度dynamic_arr = temp;// 给新增的2个元素赋值(索引new_n-2和new_n-1)dynamic_arr[new_n - 2] = new_n - 1;dynamic_arr[new_n - 1] = new_n;printf("扩展后");print_dynamic_array(dynamic_arr, new_n);}// 7. 最后一定要释放内存,避免泄漏free(dynamic_arr);dynamic_arr = NULL;return 0;
}
运行示例
请输入动态数组的长度:3
动态数组(长度3):1 2 3
扩展后动态数组(长度5):1 2 3 4 5
核心要点
- 访问方式和静态数组一致:动态数组的
arr[i]
和静态数组的arr[i]
用法完全一样,编译器会自动把arr[i]
转成*(arr+i)
,不用手动写指针运算; - 作为函数参数传递:传递动态数组时,要同时传 “指针” 和 “长度”—— 因为指针只存地址,函数不知道数组有多长,必须额外传长度才能遍历;
- 扩展数组的关键:一定要用临时指针接收
realloc
的返回值,防止扩展失败时丢失原内存。
10.4 动态二维数组的两种实现:多层指针与单指针 + 偏移
动态二维数组比一维数组复杂一点,但根据需求不同,主要有两种实现方案:多层指针(比如int**
) 和单指针 + 偏移(比如int*
)。这两种方案没有 “绝对好坏”,只有 “适合不适合”,我们分别讲清楚。
10.4.1 方案 1:多层指针(int**
)—— 适合变长维度
多层指针方案的本质是 “先建一个‘行指针数组’,再给每一行单独分配元素内存”。比如要做一个 3 行的二维数组,先申请一个能存 3 个int*
的数组(行指针数组),然后给第 1 行分配 3 个int
的内存,第 2 行分配 4 个int
的内存…… 这种方案的最大优势是每一行的列数可以不一样,适合变长维度的场景(比如表格中某几行数据列数不同)。
实现步骤
- 分配 “行指针数组”:
int** arr = (int**)malloc(行数 × sizeof(int*))
; - 给每行分配 “元素内存”:循环给
arr[i]
分配第i行列数 × sizeof(int)
的内存; - 使用数组:直接用
arr[i][j]
访问第 i 行第 j 列元素,和静态二维数组一样; - 释放内存:必须按 “先释放每行元素,再释放行指针数组” 的顺序(和分配顺序相反),否则会内存泄漏。
示例代码:变长维度的动态二维数组
比如我们做一个 “3 行,第 0 行 3 列、第 1 行 4 列、第 2 行 2 列” 的二维数组:
c代码:
#include <stdio.h>
#include <stdlib.h>int main() {// 动态二维数组的行数,以及每行的列数(变长!每行列数不同)int row = 3;int cols[] = {3, 4, 2}; // 第0行3列,第1行4列,第2行2列// 步骤1:分配行指针数组(存3个int*指针,每个指针指向一行的元素)int** dynamic_2d = (int**)malloc(row * sizeof(int*));if (dynamic_2d == NULL) { // 检查行指针数组是否申请成功exit(1);}// 步骤2:循环给每行分配元素内存for (int i = 0; i < row; i++) {// 第i行分配cols[i]个int的内存(用calloc自动初始化为0)dynamic_2d[i] = (int*)calloc(cols[i], sizeof(int));if (dynamic_2d[i] == NULL) { // 某一行申请失败,要释放已分配的内存printf("第%d行内存申请失败!\n", i);// 先释放前面已经申请好的行for (int j = 0; j < i; j++) {free(dynamic_2d[j]);dynamic_2d[j] = NULL;}// 再释放行指针数组free(dynamic_2d);dynamic_2d = NULL;return 1;}// 初始化第i行元素(赋值为i*10 + j,方便区分行)for (int j = 0; j < cols[i]; j++) {dynamic_2d[i][j] = i * 10 + j;}}// 步骤3:打印动态二维数组(注意每行列数不同)printf("变长维度动态二维数组:\n");for (int i = 0; i < row; i++) {printf("第%d行(%d列):", i, cols[i]);for (int j = 0; j < cols[i]; j++) {printf("%d ", dynamic_2d[i][j]);}printf("\n");}// 步骤4:释放内存(顺序必须和分配相反!)// 第一步:先释放每行的元素内存for (int i = 0; i < row; i++) {free(dynamic_2d[i]);dynamic_2d[i] = NULL;}// 第二步:再释放行指针数组free(dynamic_2d);dynamic_2d = NULL;return 0;
}
运行示例
变长维度动态二维数组:
第0行(3列):0 1 2
第1行(4列):10 11 12 13
第2行(2列):20 21
优势与缺点
优势 | 缺点 |
---|---|
每行列数可独立设置,灵活性高 | 内存可能不连续(每行元素是单独分配的),缓存利用率低 |
访问方式arr[i][j] 和静态二维数组一致,可读性好 | 分配和释放步骤繁琐,容易漏释放导致内存泄漏 |
10.4.2 方案 2:单指针 + 偏移(int*
)—— 适合固定维度
单指针方案的本质是 “把二维数组当成一块连续的一维内存”,通过手动计算偏移量来访问元素。比如要做一个 2 行 3 列的二维数组,直接申请 2×3=6 个int
的连续内存,然后第 i 行第 j 列的元素,就对应一维内存里的第i×3 + j
个元素(3 是总列数)。这种方案的最大优势是内存连续,缓存命中率高,访问效率高,适合维度固定的场景(比如矩阵运算、图像处理)。
实现步骤
- 一次性分配总内存:
int* arr = (int*)malloc(行数 × 列数 × sizeof(int))
; - 计算偏移量:第 i 行第 j 列的偏移量 =
i × 总列数 + j
(和静态二维数组的内存布局完全一样); - 使用数组:通过
arr[偏移量]
访问元素,比如arr[i×col + j]
; - 释放内存:只需一次
free(arr)
,简单高效。
示例代码:固定维度的动态二维数组
比如我们做一个 “2 行 3 列” 的固定维度二维数组:
c代码:
#include <stdio.h>
#include <stdlib.h>// 宏定义:计算第i行第j列的偏移量(简化代码,避免重复写i*col+j)
#define OFFSET(i, j, col) (i * col + j)int main() {// 动态二维数组的维度(固定:2行3列)int row = 2;int col = 3;int total = row * col; // 总元素数:2×3=6// 步骤1:一次性分配连续内存(6个int)int* dynamic_2d = (int*)calloc(total, sizeof(int));if (dynamic_2d == NULL) {printf("内存申请失败!\n");return 1;}// 步骤2:初始化元素(通过偏移量访问)for (int i = 0; i < row; i++) {for (int j = 0; j < col; j++) {// 用宏OFFSET计算偏移量,等价于dynamic_2d[i*col + j]dynamic_2d[OFFSET(i, j, col)] = i * 10 + j;}}// 步骤3:打印动态二维数组(通过偏移量访问)printf("固定维度动态二维数组(%d行%d列):\n", row, col);for (int i = 0; i < row; i++) {printf("第%d行:", i);for (int j = 0; j < col; j++) {printf("%d ", dynamic_2d[OFFSET(i, j, col)]);}printf("\n");}// 步骤4:释放内存(只需一次free,简单高效)free(dynamic_2d);dynamic_2d = NULL;return 0;
}
运行示例
固定维度动态二维数组(2行3列):
第0行:0 1 2
第1行:10 11 12
优势与缺点
优势 | 缺点 |
---|---|
内存连续,缓存命中率高,访问效率高 | 维度固定,无法单独修改某一行的列数 |
分配和释放只需一次操作,不易泄漏 | 访问需要计算偏移量(可通过宏简化),可读性略差 |
10.4.3 两种方案的对比与选择
为了方便大家在实际开发中快速选对方案,我整理了一张对比表,按需求对号入座即可:
对比维度 | 多层指针(int** ) | 单指针 + 偏移(int* ) |
---|---|---|
内存连续性 | 不连续(每行独立分配) | 连续(一次性分配) |
维度灵活性 | 支持变长维度(每行列数可不同) | 仅支持固定维度(行 × 列固定) |
访问方式 | arr[i][j] (直观,和静态数组一致) | arr[i*col+j] (需计算偏移,可通过宏简化) |
分配 / 释放步骤 | 多步(先分行分配,释放时先释每行) | 一步(一次分配、一次释放) |
缓存效率 | 低(内存碎片化,易触发缓存失效) | 高(连续内存,缓存命中率高) |
适用场景 | 变长维度场景(如列数不同的表格、不规则数据) | 固定维度场景(如矩阵运算、图像处理、规则表格) |
简单总结选择逻辑:
- 若数据是 “不规则的”(比如每行数据列数不一样),优先选 多层指针;
- 若数据是 “规则的”(行 × 列固定),且追求效率,优先选 单指针 + 偏移。
10.5 避坑指南:动态内存的三大核心错误
动态内存管理是 C 语言的 “重灾区”,新手最容易犯三类错误:内存泄漏、野指针、重复释放。这些错误可能导致程序崩溃、内存耗尽,甚至隐藏的逻辑错误,必须重点规避。
10.5.1 错误 1:内存泄漏(申请后未释放)
定义:内存泄漏是指 “动态申请的堆区内存,在不再使用后未被free
释放,导致这部分内存被永久占用,直到程序退出”。比如服务器这类长期运行的程序,若存在内存泄漏,会逐渐耗尽堆区内存,最终崩溃;短期运行的程序可能看不出问题,但这仍是不规范的代码。
错误示例
c代码:
#include <stdio.h>
#include <stdlib.h>void func() {// 申请10个int的内存,但函数结束前未释放int* p = (int*)malloc(10 * sizeof(int));if (p == NULL) { return; }p[0] = 10; // 使用内存// 函数结束时,栈区的指针p被回收,但堆区的内存无人认领→内存泄漏
}int main() {// 多次调用func,每次都泄漏10个int的内存for (int i = 0; i < 100000; i++) {func();}return 0;
}
错误原因
函数func
中,malloc
申请的内存存在堆区,而指针p
是栈区局部变量 —— 函数结束后,p
被自动回收,但堆区的内存没有free
,也没有其他指针指向它,变成 “无人管的内存”,造成泄漏。
解决方案
- 申请必释放:确保每个
malloc
/calloc
/realloc
都有对应的free
,形成 “成对操作”; - 函数内申请的内存要 “闭环”:要么在函数返回前释放,要么通过返回值把指针传给调用者,由调用者释放;
- 用工具检测:Linux 下用
Valgrind
,Windows 下用Visual Leak Detector
,能自动检测内存泄漏位置。
正确示例
c代码:
void func() {int* p = (int*)malloc(10 * sizeof(int));if (p == NULL) { return; }p[0] = 10;free(p); // 函数返回前释放内存p = NULL;
}// 或由调用者释放
int* func() {int* p = (int*)malloc(10 * sizeof(int));if (p == NULL) { return NULL; }p[0] = 10;return p; // 返回指针,由调用者释放
}int main() {int* ptr = func();if (ptr != NULL) {free(ptr); // 调用者释放ptr = NULL;}return 0;
}
10.5.2 错误 2:野指针(释放后未置 NULL)
定义:野指针是指 “指向无效内存的指针”,最常见的场景是 “free
释放内存后,指针仍指向原内存地址(已无效),后续误操作该指针”。野指针的危害比 NULL 指针大 ——NULL 指针解引用会直接崩溃(容易排查),而野指针可能修改随机内存,导致程序逻辑错乱(隐藏错误,难排查)。
错误示例
c代码:
#include <stdio.h>
#include <stdlib.h>int main() {int* p = (int*)malloc(1 * sizeof(int));if (p == NULL) { exit(1); }free(p); // 释放内存,但未置p=NULL// 此时p仍是原地址,但该地址的内存已归还给操作系统→p变成野指针*p = 20; // 错误:解引用野指针,可能崩溃或修改随机内存printf("%d\n", *p); // 输出随机值或程序崩溃return 0;
}
错误原因
free(p)
的作用是 “把内存归还给操作系统”,但不会修改指针p
的值 ——p
仍指向原来的地址,但这个地址的内存已经 “不属于当前程序” 了,后续操作*p
属于 “非法访问”。
解决方案
- 释放必置空:
free(p)
后立即把p
设为NULL
(p = NULL
),让指针指向 “明确的无效地址”; - 解引用前检查:对不确定的指针,先判断是否为
NULL
(if (p != NULL) { *p = 10; }
); - 避免悬垂指针:指针指向的内存被释放后,不再使用该指针(或置
NULL
)。
正确示例
c代码:
int main() {int* p = (int*)malloc(1 * sizeof(int));if (p == NULL) { exit(1); }free(p);p = NULL; // 释放后置NULL,指针不再指向无效内存// 后续误操作会触发NULL指针错误,便于调试if (p != NULL) { // 先检查再操作*p = 20;} else {printf("指针已为NULL,无法操作!\n"); // 正常提示,不会崩溃}return 0;
}
10.5.3 错误 3:重复释放(同一内存释放多次)
定义:重复释放是指 “对同一堆区内存地址,调用free
超过一次”。堆区内存有一套 “空闲链表” 管理结构,free
时会修改这个结构 —— 重复free
会破坏结构,导致程序崩溃或内存错误。
错误示例
c代码:
#include <stdio.h>
#include <stdlib.h>int main() {int* p = (int*)malloc(1 * sizeof(int));if (p == NULL) { exit(1); }free(p); // 第一次释放(正确)// p未置NULL,仍指向原地址(已无效)free(p); // 错误:重复释放同一地址,程序崩溃return 0;
}
错误原因
第一次free(p)
后,内存已归还给操作系统,该地址不再是 “当前程序的合法内存”;第二次free(p)
时,操作系统无法识别这个地址,会触发内存管理错误。
解决方案
- 释放后置 NULL:
free(p)
后立即p = NULL
—— 因为free(NULL)
是安全的(不做任何操作); - 统一管理指针:用全局或静态变量记录已释放的指针,避免重复操作;
- 函数内避免重复释放:确保同一指针在函数内只被
free
一次,可通过 “释放后置 NULL + 检查 NULL” 双重保障。
正确示例
c代码:
int main() {int* p = (int*)malloc(1 * sizeof(int));if (p == NULL) { exit(1); }free(p);p = NULL; // 释放后置NULLfree(p); // 安全:p=NULL,free不做任何操作return 0;
}
本章小结与下章预告
各位同学,今天这一章我们系统学习了动态内存与指针的配合,核心内容可以总结为 “3 个核心、4 个函数、2 种方案、3 类错误”:
- 3 个核心:动态内存突破栈区限制、手动管理生命周期、指针是唯一操作入口;
- 4 个函数:
malloc
(未初始化)、calloc
(初始化为 0)、realloc
(调整大小)、free
(释放),牢记 “申请必检查,释放必置空”; - 2 种方案:动态二维数组的多层指针(变长维度)和单指针 + 偏移(固定维度),按需选择;
- 3 类错误:内存泄漏(未释放)、野指针(释放未置 NULL)、重复释放(多次 free),通过规范操作规避。
动态内存管理是 C 语言的核心技能,也是面试高频考点 —— 比如 “malloc 和 calloc 的区别”“内存泄漏的原因”“野指针的危害”,这些内容一定要吃透。
下一章,我们将攻克 C 指针的 “终极难点”—— 复杂指针解析。面对int (*(*p)[5])(int)
这种嵌套了数组指针和函数指针的复杂类型,我们会学习 “右左法则” 这一通用解析工具,实战拆解 6 类典型复杂指针,并用typedef
简化声明,让你彻底告别 “看不懂复杂指针” 的困扰。期待下一章的学习吧!