当前位置: 首页 > news >正文

从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;
}

简单总结动态内存的核心意义:

  1. 解决 “编译期不知道内存大小” 的问题 —— 比如用户输入的长度、从文件读取的数据长度;
  2. 避免内存浪费 —— 用多少申请多少,不用了就释放;
  3. 支持内存长期存活 —— 堆区内存不会随函数结束消失,能在多个函数间共享。

 

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;
}

常见错误提醒

  1. 忘记检查 NULL:如果堆区满了,malloc返回 NULL,后续解引用*p会直接崩溃;
  2. 字节数算错:比如写malloc(n)而不是malloc(n * sizeof(int))—— 只申请了 n 字节,不够存 n 个 int(比如 int 占 4 字节,n=3 时只申请 3 字节,不够存 3 个 int);
  3. 释放后没置 NULLfree(p)后,p 还指向原来的地址,但地址已经无效了,变成 “野指针”,后续误操作会出问题。

10.2.2 calloc:申请内存并自动初始化为 0

callocmalloc功能类似,都是申请堆区内存,但它多了一个 “自动初始化” 的功能 —— 申请的内存会被自动设为 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返回的指针);如果ptrNULLrealloc就等同于malloc(size)
  • 参数size:调整后的 “总字节数”(比如要扩成 5 个 int,就写5 * sizeof(int));
  • 返回值:成功返回指向 “新内存” 的void*指针,失败返回NULL(注意:失败时,原来的ptr指针还是有效的,不会被释放);
  • 核心逻辑(重点理解,避免踩坑):
    1. 如果要缩小内存(新 size < 原 size):直接截断多余部分,原内存的前 size 字节保留,多余的释放;
    2. 如果要扩大内存(新 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;
}

关键注意事项

  1. 用临时指针接收返回值:这是最容易踩的坑!如果直接写p = realloc(p, new_size),一旦realloc失败返回 NULL,p就变成 NULL 了,原来的内存地址也丢了,导致内存泄漏;
  2. 扩展后用新指针realloc成功后,原指针p可能已经无效了(比如原内存后面空间不够,realloc会分配新地址并释放原内存),必须用新返回的p_new
  3. 缩小内存要谨慎:截断的部分数据会永久丢失,而且找不回来。

10.2.4 free:释放堆区内存(必须与申请函数成对使用)

free是唯一能 “归还” 堆区内存的函数 —— 不管是malloc/calloc/realloc申请的内存,都必须用free释放,否则会导致内存泄漏。

函数原型(C 标准 §7.22.3.3)

c代码:

void free(void* ptr);
  • 参数ptr:要释放的堆区内存指针(必须是malloc/calloc/realloc的返回值,或者NULL);
  • 返回值:无;
  • 核心规则(记牢,避免崩溃):
    1. 只能释放堆区内存 —— 不能释放栈区内存(比如int a; free(&a);会直接崩溃);
    2. free(NULL)是安全的 —— 如果ptrNULLfree什么都不做,不会出错;
    3. 不能重复释放 —— 同一内存地址不能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 

核心要点

  1. 访问方式和静态数组一致:动态数组的arr[i]和静态数组的arr[i]用法完全一样,编译器会自动把arr[i]转成*(arr+i),不用手动写指针运算;
  2. 作为函数参数传递:传递动态数组时,要同时传 “指针” 和 “长度”—— 因为指针只存地址,函数不知道数组有多长,必须额外传长度才能遍历;
  3. 扩展数组的关键:一定要用临时指针接收realloc的返回值,防止扩展失败时丢失原内存。

 

10.4 动态二维数组的两种实现:多层指针与单指针 + 偏移

动态二维数组比一维数组复杂一点,但根据需求不同,主要有两种实现方案:多层指针(比如int** 和单指针 + 偏移(比如int*。这两种方案没有 “绝对好坏”,只有 “适合不适合”,我们分别讲清楚。

10.4.1 方案 1:多层指针(int**)—— 适合变长维度

多层指针方案的本质是 “先建一个‘行指针数组’,再给每一行单独分配元素内存”。比如要做一个 3 行的二维数组,先申请一个能存 3 个int*的数组(行指针数组),然后给第 1 行分配 3 个int的内存,第 2 行分配 4 个int的内存…… 这种方案的最大优势是每一行的列数可以不一样,适合变长维度的场景(比如表格中某几行数据列数不同)。

实现步骤

  1. 分配 “行指针数组”:int** arr = (int**)malloc(行数 × sizeof(int*))
  2. 给每行分配 “元素内存”:循环给arr[i]分配第i行列数 × sizeof(int)的内存;
  3. 使用数组:直接用arr[i][j]访问第 i 行第 j 列元素,和静态二维数组一样;
  4. 释放内存:必须按 “先释放每行元素,再释放行指针数组” 的顺序(和分配顺序相反),否则会内存泄漏。

示例代码:变长维度的动态二维数组

比如我们做一个 “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 是总列数)。这种方案的最大优势是内存连续,缓存命中率高,访问效率高,适合维度固定的场景(比如矩阵运算、图像处理)。

实现步骤

  1. 一次性分配总内存:int* arr = (int*)malloc(行数 × 列数 × sizeof(int))
  2. 计算偏移量:第 i 行第 j 列的偏移量 = i × 总列数 + j(和静态二维数组的内存布局完全一样);
  3. 使用数组:通过arr[偏移量]访问元素,比如arr[i×col + j]
  4. 释放内存:只需一次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,也没有其他指针指向它,变成 “无人管的内存”,造成泄漏。

解决方案

  1. 申请必释放:确保每个malloc/calloc/realloc都有对应的free,形成 “成对操作”;
  2. 函数内申请的内存要 “闭环”:要么在函数返回前释放,要么通过返回值把指针传给调用者,由调用者释放;
  3. 用工具检测: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属于 “非法访问”。

解决方案

  1. 释放必置空free(p)后立即把p设为NULLp = NULL),让指针指向 “明确的无效地址”;
  2. 解引用前检查:对不确定的指针,先判断是否为NULLif (p != NULL) { *p = 10; });
  3. 避免悬垂指针:指针指向的内存被释放后,不再使用该指针(或置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)时,操作系统无法识别这个地址,会触发内存管理错误。

解决方案

  1. 释放后置 NULLfree(p)后立即p = NULL—— 因为free(NULL)是安全的(不做任何操作);
  2. 统一管理指针:用全局或静态变量记录已释放的指针,避免重复操作;
  3. 函数内避免重复释放:确保同一指针在函数内只被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 类错误”:

  1. 3 个核心:动态内存突破栈区限制、手动管理生命周期、指针是唯一操作入口;
  2. 4 个函数malloc(未初始化)、calloc(初始化为 0)、realloc(调整大小)、free(释放),牢记 “申请必检查,释放必置空”;
  3. 2 种方案:动态二维数组的多层指针(变长维度)和单指针 + 偏移(固定维度),按需选择;
  4. 3 类错误:内存泄漏(未释放)、野指针(释放未置 NULL)、重复释放(多次 free),通过规范操作规避。

动态内存管理是 C 语言的核心技能,也是面试高频考点 —— 比如 “malloc 和 calloc 的区别”“内存泄漏的原因”“野指针的危害”,这些内容一定要吃透。

 

下一章,我们将攻克 C 指针的 “终极难点”—— 复杂指针解析。面对int (*(*p)[5])(int)这种嵌套了数组指针和函数指针的复杂类型,我们会学习 “右左法则” 这一通用解析工具,实战拆解 6 类典型复杂指针,并用typedef简化声明,让你彻底告别 “看不懂复杂指针” 的困扰。期待下一章的学习吧!

 

http://www.dtcms.com/a/507073.html

相关文章:

  • 设计汽车网站外贸建站服务器怎么选
  • 微网站制作超链接太原网站开发工程师
  • 服装生产管理系统|基于SpringBoot和Vue的服装生产管理系统(源码+数据库+文档)
  • 牛客101:链表
  • 量化策略中三周期共振策略的仓位管理方法
  • 【python】快速实现pdf批量去除指定位置水印
  • 在 macOS 上用 Docker 为 Java 后端 常见开发需求搭建完整服务(详尽教程)
  • 网站建设翻译网站添加二维码
  • Debug —— Docker配置镜像后下载Mysql等报连接超时
  • 中冶交通建设集团网站发网站视频做啥格式最好
  • 软件定制一条龙整站多关键词优化
  • 【Vscode】显示多个文件 打开多个文件时实现标签栏多行显示
  • vue 技巧与易错
  • vscode编写Markdown文档
  • 使用VScode 插件,连接MySQL,可视化操作数据库
  • 基于微信小程序的公益捐赠安全平台9hp4t247 包含完整开发套件(程序、源码、数据库、调试部署方案及开发环境)系统界面展示及获取方式置于文档末尾,可供参考。
  • 【论文精读】FlowVid:驯服不完美的光流,实现一致的视频到视频合成
  • 【C++】滑动窗口算法习题
  • C语言趣味小游戏----扫雷游戏
  • 三款AI平台部署实战体验:Dify、扣子与BuildingAI深度对比
  • 网站制作难不难小红书搜索优化
  • Python如何使用NumPy对图像进行处理
  • 房产中介网站开发站长工具之家
  • Linux服务器编程实践60-双向管道:socketpair函数的实现与应用场景
  • c++结构体讲解
  • 青岛商城网站建设网站相互推广怎么做
  • Linux学习笔记(九)--Linux进程终止与进程等待
  • 虚幻引擎5 GAS开发俯视角RPG游戏 P06-09 玩家等级与战斗接口
  • JavaSE内容梳理与整合
  • JavaScript日期处理:格式化与倒计时实现