动态内存分配
要实现动态内存分配,需要使用标准C库提供的库函数,我们所说的动态内存分配,其实就是在堆区申请内存(此时的内存回收需要程序员自身来维护)
常用函数
malloc
-
头文件:#
-
函数原型:
void* malloc(size_t size);
-
功能:分配指定字节数的内存到堆区,返回指向内存块首地址的指针。内存内容未初始化(随机值)。
-
参数:
-
:要分配的内存大小(字节),这里的size_t是数据类型 unsigned long int的别名。
-
-
返回值:
-
成功
:返回内存指针(申请到的内存的首地址) -
失败
:失败返回NULL
-
-
示例:
/************************************************************************* > File Name: demo01.c > Author: 久念昕 > Description: > Created Time: 2025-07-31 09:31:49 ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> int main(int argc, char *argv[]) { // 新建一个指针变量,用来接收内存分配后返回的堆内存首地址 int *p = malloc(sizeof(int)); // 4字节 这里尽量不使用常量 // 对于指针的使用,一定要进行有效性校验,防止野指针 if(p == NULL){ // 等价于 !p perror("内存申请失败!");// 这个函数用于向控制台输出异常信息 return -1; } // malloc申请的内存空间,默认填充的是随机值,需要我们手动清零 memset(p,0,sizeof(int)); // 对指定空间赋值0,支持批量赋值 // 向这块空间赋值 *p = 100; // 访问这块空间 printf("%d\n", *p); // 堆空间使用完毕,一定要释放内存,此时需要程序员写代码释放 free(p); // 如果指针对应的内存被释放,此时指针需要置空,防止产生空悬指针 p = NULL; return 0; }
-
注意事项:
-
分配内存后需要手动初始化内存,推荐
或
-
内存空间连续,不可越界访问
-
calloc
-
头文件:#
-
函数原型:
void* calloc(size_t nitems, size_t size);
-
功能:动态分配内存,并初始化为0
-
参数:
-
:元素个数
-
:每个元素的字节大小
-
-
返回值:
-
成功
:返回内存指针(申请到的内存的首地址) -
失败
:失败返回NULL
-
-
示例:
/************************************************************************* > File Name: demo01.c > Author: 久念昕 > Description: > Created Time: 2025-07-31 09:50:17 ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #define LEN 5 int main(int argc, char *argv[]) { // 创建一个指针变量,用来接收内存分配后返回的堆内存地址 int *arr = calloc(LEN, sizeof(int)); // 等价于 int *arr = malloc(LEN * sizeof(int); memset(arr,0,LEN * sizeof(int)); // 对于指针的使用,一定要进行非空检验,防止野指针 if (arr == NULL) // 等价于 !arr { perror("内存申请失败!"); return -1; } // 使用for循环快速赋值 for (int i = 0; i < LEN; i++) { if(i % 2 == 0) continue; arr[i] = (i+1)*10; } // 遍历数组 int *p = arr; for (; p < arr + LEN; p++) { printf("%-6d", *p); } printf("\n"); // 内存使用完毕,释放内存 free(arr); // 置空,防止产生空悬指针 arr = p = NULL; return 0; }
-
使用场景:为数组分配内存时更安全高效。
realloc
-
头文件:#
-
函数原型:
void* realloc(void* ptr, size_t size);
-
功能:调整已分配内存块的大小,可能迁移数据到新地址。扩容后,空余位置是随机值,需要手动清零。
-
原理:
-
当前内存空间后面的剩余空间足够扩容,就在原空间基础上进行扩容,返回原地址;
-
当前内存空间后面的剩余空间不够扩容,会重新开辟一块空间,将原空间数据按顺序拷贝后,销毁原空间,返回新地址。扩容后的内存空间中的数据是随机的。需要手动清零。
-
-
参数:
-
:原内存指针(需要扩容的内存空间的指针,这个ptr的来源(
))
-
:指定重新分配内存的大小(字节)
-
-
返回值:
-
成功
:返回内存指针(申请到的内存的首地址) -
失败
:失败返回
-
-
示例:
/************************************************************************* > File Name: demo03.c > Author: 久念昕 > Description: > Created Time: 2025-07-31 10:21:26 ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #define LEN 5 #define NEW_LEN 8 #define NEW_LEN_MIN 3 int main(int argc, char *argv[]) { // 创建一个指针,指向堆内存分配的内存空间 int *arr = (int*)calloc(LEN, sizeof(int)); // 指针的非空检验,防止野指针 if (!arr){ perror("内存申请失败!"); return -1; } // 使用for循环赋值 for (int i = 0; i < LEN; i++) { arr[i] = (i+1)*10; } // 遍历数组 int *p = arr; for (; p < arr + LEN; p++) printf("%-6d", *p); printf("\n"); // 扩容数组 int *temp = (int*)realloc(arr, NEW_LEN * sizeof(int)); // 扩容出来的空间,默认是随机值 // 非空校验,防止野指针 if(!temp){ perror("内存申请失败!"); free(arr); // 扩容失败,回收原空间 return -1; } // 更新数组指针 arr = temp; // 对扩容部分清零 memset(arr+LEN,0,(NEW_LEN - LEN) * sizeof(int)); // 修改数组元素 arr[7] = 666; // 遍历数组 for (int i = 0; i < NEW_LEN; i++) printf("%-6d", *(arr+i)); printf("\n"); // 缩容数组 temp = (int*)realloc(arr, 0); // 此时,会回收掉内存,等价于free(arr); // 非空校验,防止野指针 if(!temp){ perror("内存申请失败!"); return -1; } // 更新数组指针 arr = temp; // 遍历数组 for (int i = 0; i < NEW_LEN; i++) printf("%-6d", *(arr+i)); printf("\n"); // 释放内存 free(arr); arr = NULL; p = NULL; return 0; }
-
注意事项:
-
分配后需要手动初始化内存,推荐
-
必须用临时变量接收返回值,避免直接覆盖原指针导致内存泄漏。
-
若
为0,等效于
-
free
-
头文件:#
-
函数原型:
void free(void* ptr);
-
功能:释放动态分配的内存。
-
注意事项:
-
只能释放一次,重复释放会导致程序崩溃。
-
释放后应将指针置为
,避免野指针。
-
栈内存由系统自动释放,无需手动
。
-
内存操作
我们对于内存操作需要依赖于头文件中相关的库函数。
常用函数
内存填充
-
头文件:#
-
函数原型:
void* memset(void* s, int c, size_t n);
-
函数功能:将内存块
s
的前n
个字节填充为c
,一般用于初始化或者清零操作。 -
参数说明:
-
:目标内存首地址
-
:填充值(以
形式处理(0~255))
-
:填充字节数
-
-
返回值:
-
成功:返回
的指针
-
失败:返回
-
-
注意事项:
-
常用于动态初始化,
通常设置为0(清零)
-
按字节填充,非整型初始化需要谨慎(如填充
数组时,0是安全的)
-
-
案例:
/************************************************************************* > File Name: demo04.c > Author: 久念昕 > Description: > Created Time: 2025-07-31 11:03:23 ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #define LEN 4 int main(int argc, char *argv[]) { // 在堆内存申请4个int的连续空间 int *p = (int*)malloc(LEN * sizeof(int)); // 非空校验 if (!p){ perror("内存申请失败!"); return -1; } // 初始化堆内存空间,填充0,我们也可以称作 清零操作 memset(p, 0, LEN * sizeof(int)); // 测试输出 printf("%d,%d\n", p[1], *(p+1)); // 释放内存 free(p); // 等价于 realloc(p,0); // 对指针置空,防止空悬指针 p = NULL; return 0; }
内存拷贝
-
头文件:#
-
函数原型:
-
源与目标内存无重叠时使用
void* memcpy(void* dest, const void* src, size_t n);
-
安全处理内存重叠
void* memmove(void* dest, const void* src, size_t n);
-
-
函数功能:将
的前
个字节拷贝到
-
参数说明:
-
:目标内存首地址,支持指针偏移
-
:源内存首地址,支持指针偏移
-
-
-
返回值:
-
成功:返回
的首地址
-
失败:返回
-
-
注意事项:
-
能正确处理内存重叠,推荐优先使用
-
确保目标内存足够大,避免溢出。
-
-
示例:
/************************************************************************* > File Name: demo05.c > Author: 久念昕 > Description: > Created Time: 2025-07-31 11:23:20 ************************************************************************/ #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { // 准备两个数组,用来存储源和目标 int src[4] = {11,22,33,44}; // 拷贝 22,33 从src+1开始,拷贝2个sizeof(int) int dest[6] = {111,222,333,444,555,666}; // 目的地 dest+1,拷贝后:111,22,33,444,555,666 register int i = 0; printf("拷贝前:"); for (i = 0; i < 6; i++) printf("%-6d", dest[i]); // 进行拷贝 // memcpy(dest+1,src+1,2 * sizeof(int)); memmove(dest+1, src+1, 2 * sizeof(int)); printf("\n拷贝后:"); for (i = 0; i < 6; i++) printf("%-6d", dest[i]); printf("\n"); return 0; }
思考:什么是内存重叠?
内存比较
-
头文件:#
-
函数原型:
int memcmp(const void* s1, const void* s2, size_t n);
-
函数功能:比较
和
的前
个字节
-
返回值:
-
:内存内容相同
-
:
中第一个不同字节大于
-
:
中第一个不同字节小于
-
-
注意事项:比较按字节进行,非字符串需确保长度一致(总字节数一致)。
-
示例:
/************************************************************************* > File Name: demo06.c > Author: 久念昕 > Description: > Created Time: 2025-07-31 11:41:20 ************************************************************************/ #include <stdio.h> #include <string.h> int main(int argc, char *argv[]) { // 准备两个测试用的数组 int src[] = {111,22,33,44}; int dest[] = {111,22,10,44,555,666}; // 进行比较 int result = memcmp(src, dest, 4 * sizeof(int)); printf("%d与%d的比较结果是%d\n",*src, *dest, result); return 0; }
内存查找
-
头文件:#
-
函数原型:
-
正向查找,C语言标准库函数
void* memchr(const void* s, int c, size_t n);
-
逆向查找,这个不是C语言标准库函数,属于GNU扩展
void* memrchr(const void* s, int c, size_t n);
-
-
函数功能:在
的前
个字节中查找字符
-
返回值:
-
成功:返回找到内容对应的地址
-
失败:返回
-
-
注意事项:
-
是GNU扩展函数,需手动声明(只要不是C语言标准提供,编译的时候都需要手动声明或链接)
-
查找单位为字节值,非整型数据需要注意内存布局
-
-
示例:
/************************************************************************* > File Name: demo07.c > Author: 久念昕 > Description: > Created Time: 2025-07-31 14:24:24 ************************************************************************/ #include <stdio.h> #include <string.h> // #include <stddef.h> // 逆向查找函数 memrchr是GNU扩展函数,这个函数需要额外声明 extern void* memrchr(const void*, int, size_t); //void* memrchr(const void* s, int c, size_t n) { // const unsigned char* ptr = (const unsigned char*)s + n; // while (n--) { // if (*--ptr == (unsigned char)c) { // return (void*)ptr; // } // } // return NULL; //} int main(int argc, char *argv[]) { // 准备一个测试数组 char str[] = {'A','B','C','B'}; // 查找字符 B char* first = (char*)memchr(str, 'B', sizeof(str)); // 返回 低地址对应的字符地址 char* last = (char*)memrchr(str, 'B', sizeof(str)); // GNU扩展函数,返回 高地址对应的字符地址 printf("first=%p,last=%p\n", first, last);// first=0x7fff65a47875,last=0x7fff65a47877 printf("第1个B的位置:%ld\n", first - str);// 1 printf("最后1个B的位置:%ld\n", last - str);// 3 return 0; }
综合案例:学生成绩管理系统v2.0
-
需求:
要求实现一个基于指针的学生成绩管理系统,具体功能如下:
-
添加学生信息:输入学号和三门成绩,存储到数组中。
-
显示所有学生信息:遍历数组,输出每个学生的学号和成绩。
-
计算每个学生的平均分和总分:遍历数组,计算每行的总分和平均分。
-
根据某科成绩排序:用户选择科目,然后按该科成绩排序,可以升序或降序。
-
查找学生信息:按学号查找,显示该生的成绩和平均分。
-
退出程序。
-
-
代码:
/************************************************************************* > File Name: scoresMgr.c > Author: 久念昕 > Description: 学生成绩管理系统v2.0 > Created Time: 2025-07-31 15:21:00 ************************************************************************/ #include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_STUDENTS 50 // 最大学生数 #define COURSE_NUM 3 // 课程科目数量 #define ID_LENGTH 4 // 学号长度 /** 函数原型声明 **/ // 添加学生信息 void addStudent(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int *count); // 显示所有记录 void displayAll(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count); // 查看统计信息 void showStatistics(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count); // 成绩排序 // 查找学生 // 校验学号 1-校验合格 0-校验不合格 int validateId(char *id); /** * @brief 入口函数 * * @param argc * @param argv * * @return */ int main(int argc, char *argv[]) { int choice; // 用户菜单选择 int studentCount = 0; // 记录当前学生的数量 // 学生数据存储(二维数组) char studentIds[MAX_STUDENTS][ID_LENGTH]; // 学生学号 int scores[MAX_STUDENTS][COURSE_NUM]; // 学生成绩 // 主循环 do{ // 系统菜单界面 printf("\033[1;32m+-----------------------+\033[0m\n"); // 绿色边框 printf("\033[1;32m| \033[1;33m学生成绩管理系统 v2.0\033[1;32m |\033[0m\n"); printf("\033[1;32m| \033[1;33m作者:冯鹏飞\033[1;32m |\033[0m\n"); printf("\033[1;32m+-----------------------+\033[0m\n"); printf("\033[31m1. 添加学生信息\033[0m\n"); printf("\033[31m2. 显示所有记录\033[0m\n"); printf("\033[31m3. 查看统计信息\033[0m\n"); printf("\033[31m4. 成绩排序\033[0m\n"); printf("\033[31m5. 查找学生\033[0m\n"); printf("\033[31m6. 退出系统\033[0m\n"); printf("\n请输入您的选择: "); scanf("%d", &choice); switch (choice) { case 1: // 添加学生信息 addStudent(scores, studentIds, &studentCount); break; case 2: // 显示所有记录 displayAll(scores, studentIds, studentCount); break; case 3: // 查看统计信息 showStatistics(scores, studentIds, studentCount); break; case 4: // 成绩排序 printf("该功能未开放!\n"); break; case 5: // 查找学生 printf("该功能未开放!\n"); break; case 6: // 退出系统 printf("系统已退出,感谢您的使用!\n"); return 0; default: //TODO break; } }while(1); return 0; } /** * @brief 添加学生信息 * * @param scores 学生成绩数组 * @param ids 学生学号数组 * @param count 当前学生数量 */ void addStudent(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int *count) { // 校验存储空间是否已满 if (*count >= MAX_STUDENTS) { printf("错误信息:存储空间已满!\n"); return; } printf("\n--- 添加学生信息 ---\n"); // 创建一个数组,用来存放学生学号 char tempId[ID_LENGTH + 1]; // 控制台输入的字符串以\0结尾 // 学号验证 do{ printf("请输入4位学号:"); scanf("%4s", tempId); // 清空缓冲区 while(getchar() != '\n'); }while(!validateId(tempId)); // 检查学号是否存在 register int i; for (i = 0; i < *count; i++) { // 使用内存比较函数,比较两块内存中的数据是否相等 if (memcmp(ids[i], tempId, ID_LENGTH) == 0) { printf("该学号已存在!\n"); return; } } // 向数组中存入学号 memcpy(ids[*count], tempId, ID_LENGTH); // 输入成绩 printf("请输入%d门课程成绩(0~100):\n", COURSE_NUM); for (i = 0; i < COURSE_NUM;) { printf("课程%d:", i + 1); // 1.非法字符校验 int tempScore = scanf("%d", &scores[*count][i]); if (tempScore != 1) { printf("成绩无效:请重新输入!\n"); while(getchar() != '\n'); // 清空缓冲区 continue; } // 2.输入范围校验 if (scores[*count][i] < 0 || scores[*count][i] > 100) { printf("成绩无效:请重新输入!\n"); continue; } i++; // i++ 写在这里,会受到continue的影响 } // 更新序号,管理当前学生人数 (*count)++; printf("学生信息添加成功!\n"); } /** * @brief 显示所有记录 * * @param scores * @param ids * @param count */ void displayAll(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count) { printf("\n--- 学生成绩列表 ---\n"); // 校验是否存在数据 if (count == 0) { printf("暂无学生数据!\n"); return; } // 表格数据 // 表头 (学号,语文,数学,英语) printf("%s\t%s\t%s\t%s\n","学号","语文","数学","英语"); // 数据 for (int i = 0; i < count; i++) // 人 { // 学号 printf("%.4s\t",ids[i]); // 成绩 for (int j = 0; j < COURSE_NUM; j++) // 科目 { printf("%d\t", *(*(scores + i) + j)); // scores[i][j] } printf("\n"); } printf("\n"); } /** * @brief 查询统计信息 * * @param scores 学生成绩数组 * @param ids 学生学号数组 * @param count 当前学生数 */ void showStatistics(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count) { // 校验是否存在学生 if (count == 0) { printf("暂无学生数据!\n"); return; } // 创建一个数组,用来存储每一科总分 int courseTotal[COURSE_NUM] = {0}; // 创建一个数组,用来存储每一科最高分 int courseMax[COURSE_NUM] = {0}; // 创建一个数组,用来存储每一科最低分 int courseMin[COURSE_NUM] = {100,100,100}; // 遍历:计算每一科总分,最高分,最低分 for (int i = 0; i < count; i++) // 遍历得到每一个学生,行 { for (int j = 0; j < COURSE_NUM; j++) // 遍历得到每一个学生的每一门成绩,列 { // 获取每一个成绩 int score = scores[i][j]; // 单科总分 courseTotal[j] += score; // 单科最高分 if (score > courseMax[j]) courseMax[j] = score; // 单科最低分 if (score < courseMin[j]) courseMin[j] = score; } } // 输出信息 printf("\n--- 课程统计信息 ---\n"); char *courses[] = {"语文","数学","英语"}; for (int i = 0; i < COURSE_NUM; i++) { printf("%s:\n",courses[i]); // 语文 数学 英语 printf(" 平均分:%.2f\n", (float)courseTotal[i] / count); printf(" 最高分:%d\n", courseMax[i]); printf(" 最低分:%d\n", courseMin[i]); } } /** * @brief 学号校验 * * @param id 学号指针 * * @return 1-合法,0-非法 */ int validateId(char *id) { char *p = id; int len = 0; // 1.校验学号是否是数字 while (*p && len < ID_LENGTH) { // 校验输入的是否是数字 if(!(*p >= '0' && *p <= '9')) { printf("学号必须为数字!\n"); return 0;// 不合法 } p++; len++; } // 2. 校验学号的位数是否满足 if (len != ID_LENGTH || *p != '\0') { printf("学号必须为4位!\n"); return 0; } return 1; }