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

C语言内存精讲系列(二十九):C 语言堆区内存进阶与动态内存实战

上节课我们学了extern关键字的跨文件共享用法,以及堆区内存的基础操作(malloc/calloc/free)。这节课我们聚焦堆区的 “进阶技能”—— 用realloc调整内存大小,同时通过 “动态存储学生信息”“修复内存泄漏” 两个实战案例,帮大家彻底掌握动态内存的规范用法,避开实际开发中的高频坑点。

一、realloc 函数:堆区内存的 “伸缩工具”

在实际开发中,我们经常会遇到 “堆内存不够用” 或 “堆内存用不完浪费” 的情况:比如用malloc申请了 10 个学生的内存,后续需要新增 5 个学生;或者申请了 20 个学生的内存,实际只用到 8 个。这时候就需要realloc函数 —— 它能 “重新分配” 已申请的堆内存大小,是堆区灵活性的核心体现。

1. realloc 的函数原型与核心逻辑

首先明确realloc的基本用法,它需要包含<stdlib.h>头文件:

void* realloc(void* ptr, size_t size);
  • 参数 1:void* ptr:必须是之前用malloc/calloc/realloc申请过的 “有效堆内存地址”(不能是栈地址,也不能是未初始化的野指针);
  • 参数 2:size_t size重新调整后的总字节数(不是 “增加 / 减少的字节数”,比如原内存是5*sizeof(int),想扩到 10 个intsize要写10*sizeof(int));
  • 返回值:成功时返回 “新的堆内存起始地址”,失败时返回NULL(原内存不会丢失,需手动释放)。

2. realloc 的两个关键内存调整策略

realloc调整内存时,会根据 “原内存后面是否有足够空闲空间”,采用两种不同策略,这直接影响程序效率和内存安全:

策略 1:“原地扩展 / 缩减”(高效)

如果原堆内存的后续连续内存是空闲的(没有被其他堆变量占用),realloc会直接在原内存后面 “追加 / 缩减” 空间,此时新地址和原地址完全相同

  • 比如原内存是5*sizeof(int)(地址 0x1000~0x1013),后续 0x1014~0x1027 是空闲的,想扩到 10 个int(40 字节),realloc会直接把 0x1014~0x1027 分配进来,新地址还是 0x1000。
策略 2:“异地迁移”(低效但必要)

如果原内存后续有其他堆变量占用(空间不够),realloc会做三件事:

  1. 在堆区找一块 “大小足够的新空闲内存”;
  2. 把原内存里的所有数据完整复制到新内存
  3. 自动释放原内存(避免内存泄漏);
  4. 返回新内存的起始地址(此时新地址≠原地址)。
  • 比如原内存 0x1000~0x1013 后面被 0x1014~0x1017 的堆变量占用,想扩到 10 个intrealloc会找一块新地址(比如 0x2000~0x2027),复制原数据后释放 0x1000~0x1013,返回 0x2000。

3. realloc 的规范用法(必须避免的 3 个坑)

realloc是动态内存中 “最容易用错” 的函数,必须严格遵循 “先判断返回值,再更新指针” 的流程,我们用 “扩展学生成绩数组” 的例子演示:

#include <stdio.h>
#include <stdlib.h>int main() {int student_num = 5; // 初始学生数// 1. 先用malloc申请5个int的堆内存(存储5个学生成绩)int* score_arr = (int*)malloc(student_num * sizeof(int));if (score_arr == NULL) { // 申请失败判断printf("初始内存申请失败!\n");exit(1);}// 2. 给初始5个学生赋值(100~96分)for (int i = 0; i < student_num; i++) {score_arr[i] = 100 - i;}printf("初始5个学生成绩:");for (int i = 0; i < student_num; i++) {printf("%d ", score_arr[i]); // 输出:100 99 98 97 96}printf("\n");// 3. 需求:新增3个学生,需扩到8个int的内存(总字节数=8*sizeof(int))int new_num = 8;// 关键:用临时指针接收realloc返回值,不要直接用原指针!int* temp_ptr = (int*)realloc(score_arr, new_num * sizeof(int));if (temp_ptr == NULL) { // 扩容失败判断printf("内存扩容失败!\n");free(score_arr); // 失败时原内存还在,必须手动释放,避免泄漏score_arr = NULL;exit(1);}// 扩容成功:更新原指针,指向新内存score_arr = temp_ptr;temp_ptr = NULL; // 临时指针置空,避免后续误操作// 4. 给新增的3个学生赋值(95~93分)for (int i = student_num; i < new_num; i++) {score_arr[i] = 100 - i;}printf("扩容后8个学生成绩:");for (int i = 0; i < new_num; i++) {printf("%d ", score_arr[i]); // 输出:100 99 98 97 96 95 94 93}printf("\n");// 5. 最终释放内存free(score_arr);score_arr = NULL;return 0;
}
必须记牢的 3 个规范点:
  1. 用临时指针接收返回值:如果直接写score_arr = (int*)realloc(score_arr, ...),一旦realloc返回NULL,原score_arr会变成NULL,原内存地址丢失,导致 “内存泄漏”;
  2. 扩容失败必须释放原内存realloc返回NULL时,原内存没有被释放,必须手动free(score_arr),否则内存会一直占用;
  3. 缩减内存时数据安全:如果size比原内存小(比如从 8 个int缩到 5 个),realloc会保留前 5 个int的数据,后面 3 个会被自动丢弃,无需手动处理。

二、实战案例 1:动态存储学生完整信息(结构体 + 堆区)

之前我们只存储了学生成绩(单一int类型),实际开发中需要存储 “姓名 + 学号 + 成绩” 等多字段信息,这时候需要结合结构体和堆区 —— 用堆内存动态存储结构体变量,灵活应对 “不确定数量的学生”。

案例需求:

  1. 让用户输入学生人数;
  2. 动态申请对应数量的 “学生结构体” 堆内存;
  3. 输入每个学生的姓名、学号、成绩;
  4. 遍历输出所有学生信息;
  5. 释放堆内存,避免泄漏。

完整代码实现:

#include <stdio.h>
#include <stdlib.h>
#include <string.h> // 用strcpy给字符串赋值// 1. 定义学生结构体(包含姓名、学号、成绩)
typedef struct Student {char name[20]; // 姓名(固定20字节,也可动态申请字符串,后续进阶)int id;        // 学号int score;     // 成绩
} Student;int main() {int student_num;Student* stu_arr = NULL; // 指向学生结构体数组的堆指针,初始置空// 2. 输入学生人数printf("请输入学生人数:");scanf("%d", &student_num);// 吸收scanf后的换行符,避免后续fgets读取姓名时出错getchar();// 3. 动态申请student_num个Student结构体的堆内存// 总字节数 = 学生数 * 单个结构体字节数(sizeof(Student))stu_arr = (Student*)malloc(student_num * sizeof(Student));if (stu_arr == NULL) {printf("学生内存申请失败!\n");exit(1);}// 4. 输入每个学生的信息for (int i = 0; i < student_num; i++) {printf("\n请输入第%d个学生的信息:\n", i + 1);printf("姓名:");// 用fgets读取姓名(含空格),避免scanf("%s")无法读取空格的问题fgets(stu_arr[i].name, sizeof(stu_arr[i].name), stdin);// 去掉fgets读取的换行符(fgets会把回车也读入字符串)stu_arr[i].name[strcspn(stu_arr[i].name, "\n")] = '\0';printf("学号:");scanf("%d", &stu_arr[i].id);printf("成绩:");scanf("%d", &stu_arr[i].score);getchar(); // 吸收score后的换行符,为下一次fgets做准备}// 5. 遍历输出所有学生信息printf("\n========== 学生信息列表 ==========\n");for (int i = 0; i < student_num; i++) {printf("第%d个学生:\n", i + 1);printf("姓名:%s\n", stu_arr[i].name);printf("学号:%d\n", stu_arr[i].id);printf("成绩:%d\n", stu_arr[i].score);printf("------------------------\n");}// 6. 释放堆内存(结构体数组只需释放一次首地址)free(stu_arr);stu_arr = NULL;return 0;
}

案例关键知识点:

  1. 结构体堆内存的计算student_num * sizeof(Student)——sizeof(Student)会自动计算结构体总字节数(姓名 20 + 学号 4 + 成绩 4 = 28 字节,不同编译器可能有内存对齐差异,但无需手动计算);
  2. 字符串读取问题:用fgets读取姓名(支持含空格的姓名,如 “张三 三”),避免scanf("%s")遇到空格就停止的问题;
  3. 内存释放:结构体数组是 “连续的堆内存”,只需free(stu_arr)一次(释放首地址即可),不需要循环释放每个结构体。

三、实战案例 2:识别并修复内存泄漏(高频面试题)

内存泄漏是动态内存开发中最常见的问题 —— 申请了堆内存但忘记释放,导致内存一直被占用,程序运行时间越长,泄漏的内存越多,最终会耗尽系统内存导致崩溃。下面我们通过 “错误代码” 和 “修复代码” 的对比,掌握内存泄漏的排查思路。

错误代码(含 2 处内存泄漏):

#include <stdio.h>
#include <stdlib.h>// 错误1:函数内申请堆内存,未释放且未返回,导致内存泄漏
void create_leak1() {int* p = (int*)malloc(10 * sizeof(int));// 只申请不释放,函数结束后p被销毁(栈区变量),堆内存地址丢失,无法释放
}// 错误2:realloc失败时,未释放原内存,导致泄漏
void create_leak2() {int* p = (int*)malloc(5 * sizeof(int));if (p == NULL) return;// 直接用原指针接收realloc返回值,若失败,p变成NULL,原内存地址丢失p = (int*)realloc(p, 10 * sizeof(int));if (p == NULL) {printf("realloc失败!\n");// 此处未释放原内存,导致原5个int的内存泄漏return;}free(p); // 若realloc成功,此处释放有效;若失败,p已为NULL,free无意义p = NULL;
}int main() {create_leak1(); // 调用一次,泄漏40字节(10*4)create_leak2(); // 调用一次,若realloc失败,泄漏20字节(5*4)return 0;
}

修复后的正确代码:

#include <stdio.h>
#include <stdlib.h>// 修复1:要么在函数内释放,要么返回指针让调用者释放
void fix_leak1() {int* p = (int*)malloc(10 * sizeof(int));if (p == NULL) return;// 若函数内用不到p,直接释放free(p);p = NULL;// 若函数需要给调用者返回数据,可改为返回指针(需在调用者处释放)// return p;
}// 修复2:realloc用临时指针接收,失败时释放原内存
void fix_leak2() {int* p = (int*)malloc(5 * sizeof(int));if (p == NULL) return;// 用临时指针接收realloc返回值int* temp = (int*)realloc(p, 10 * sizeof(int));if (temp == NULL) {printf("realloc失败!\n");free(p); // 修复:失败时释放原内存p = NULL;return;}p = temp;temp = NULL;free(p); // 成功时释放新内存p = NULL;
}int main() {fix_leak1(); fix_leak2(); return 0;
}

内存泄漏的 3 个排查原则:

  1. “申请 - 释放” 成对出现:每一个malloc/calloc/realloc(成功的),都要有对应的free,且不能遗漏;
  2. 函数返回前检查:函数内申请的堆内存,若不返回给调用者,必须在函数返回前free;若返回给调用者,需在文档中明确 “调用者需释放”;
  3. realloc 必用临时指针:永远不要用原指针直接接收realloc返回值,必须用临时指针,失败时手动释放原内存。

四、总结:堆区内存的 “黄金法则”

到这里,我们已经学完了堆区内存的所有核心操作(malloc/calloc/realloc/free),以及实战案例,最后总结 6 条 “黄金法则”,帮大家彻底避开坑点:

  1. 申请必判空malloc/calloc/realloc返回后,必须判断是否为NULL,否则访问NULL会崩溃;
  2. realloc 用临时指针:避免原内存地址丢失,失败时手动释放原内存;
  3. 释放后置 NULLfree后必须把指针置为NULL,避免野指针(后续误操作会报错,便于排查);
  4. 不重复释放:同一个堆地址只能free一次,重复释放会报错;
  5. 不释放非堆内存free只能释放malloc/calloc/realloc申请的地址,不能释放栈地址(如&a)或NULL以外的野指针;
  6. 函数内内存不 “丢”:函数内申请的堆内存,要么在函数内释放,要么返回给调用者释放,避免地址丢失导致泄漏。
http://www.dtcms.com/a/392340.html

相关文章:

  • 6G量子通信融合:破解未来网络的安全与效能密码
  • C#练习题——泛型实现单例模式和增删改查
  • 网关登录校验
  • Kubernetes Fluent Bit Pod Pending 问题解决方案
  • 我爱学算法之—— 位运算(中)
  • 什么是差分信号
  • 相机标定(Camera Calibration)原理及步骤:从 “像素模糊” 到 “毫米精准” 的关键一步
  • 用 【C# + WinUI3 + 图像动画】 来理解:高数 - 函数 - 初等函数
  • ​​[硬件电路-296]:单刀双掷(SPDT)模拟开关
  • 【MAVLink】MAVSDK编程入门、基本概念
  • MAC-基于反射的枚举工具类优化
  • 防御性编程:编程界的‘安全驾驶‘指南
  • Qt绘图方式有哪些
  • 使用python创建、管理、分析和可视化“欲望”之间的关系网络图
  • 铸铁平台:工业制造的基石与精密测量的核心
  • Mac环境安装Nginx指南实录
  • 《RAG是什么?为什么它比微调更适合让AI拥有“专业知识”?》
  • 【Python】控制台界面演示
  • 软考中级习题与解答——第九章_信息安全(2)
  • [新启航]民航发动机燃油喷嘴的多孔阵列孔深光学 3D 轮廓测量 - 激光频率梳 3D 轮廓技术
  • 【测试开发/测试】详解测试用例(下):详解设计测试用例的方法
  • Go基础:Go语言错误和异常详解
  • kubeadm部署K8S单master架构实战
  • npx命令介绍(Node Package Execute)(允许开发者直接执行来自npm注册表的包中的二进制文件,而无需全局安装)临时使用
  • LeetCode 3508.设计路由器:STL套STL——有什么需求就设计什么数据结构
  • 基本排序算法
  • 学习Python中Selenium模块的基本用法(15:窗口操作)
  • 能力(1)
  • UE4/UE5 如何迁移HotPatcher插件
  • SQL从入门到起飞:完整数据库操作练习