C语言基础17
内容提要
-
指针
-
void与void*的区别
-
-
内存操作
-
综合案例:学生成绩管理系统2.0
回顾
二级指针:
指向另一个指针的指针。二级指针等效于指针数组,不等效于二维数组
常量指针:
语法:
const int* p;
特点:
-
指针的指向可变,指向的数据不可变
指针常量
语法:
int* const p;
特点:
-
指针的指向不可变,指向的数据可变
常量指针常量
语法:
const int* const p;
特点:
-
指针的指向不可变,指向的数据不可变
内存的动态分配:
-
malloc() 内存分配到堆区,需要初始化,清零使用memset或者realloc或者bzero(非标准库)
-
calloc() 内存分配到堆区,默认已经完成了初始化
-
realloc() 针对malloc、calloc、realloc分配的内存进行扩容,扩容后会保留原数据,空余位置需要初始化
-
free() 释放分配的内存,也可以使用realloc(p,0)
野指针产生的场景:
-
指针未初始化
-
访问已经释放了的指针
-
函数中返回局部变量的地址
-
指针的越界访问
空指针:
值为NULL的指针,初始化的时候可以给指针赋值为NULL,内存释放后,需要对指针置空(NULL)
空悬指针:
野指针的一种特殊表现,指针指向的内存已经被释放,指针还未置空(NULL)
指针
void与void*的区别
定义
-
void:表示“无类型”,用于函数返回值或参数
void func(void); // 不过一般简写为 void func(); 无返回值无参数
-
void*:通用指针类型(万能指针),可指向任意数据类型,但需要强制类型转换后才能解引用
void* ptr = malloc(4); // int* p = (int*)ptr; // *p = 10; float* p1 = (float*)ptr; // 强制类型转换 *p = 20;
注意事项
-
void*
不能直接解引用(如:*ptr
会报错) -
函数返回
void*
需要外部接收的时候明确类型
示例
#include <stdlib.h> /** * 定义一个返回值为void*类型的指针函数 */ void* process_data(void* p) { // 例如 // 处理int类型时:return (int*)p; // 处理double类型时:return (double*)p; // 实际应用中需要配套数据类型表示参数 return p; // 直接返回原指针 } int main(int argc,char*argv[]) { // 演示int类型处理 int m = 10; int* p_int = &m; // 必须进行显示类型的转换(保证类型安全) int* result_int = (int*)process_data(p_int); *result_int = 20; // 安全操作 printf("Integer value:%d\n",*result_int); // 演示double类型处理 double pi = 3.1415926; double* p_double = π // 必须进行显示类型的转换(保证类型安全) double* result_double = (double*)process_data(p_double); *result_double *= 2; // 安全操作 printf("Double value:%.6f\n",*result_double); // void*指针的特殊使用注意事项 void* generic_ptr = process_data(p_int); // *generic_ptr = 30; // 错误! void* 不能直接解引用(必须明确类型) // 正确的写法 int* c_ptr = (int*)generic_ptr; *c_ptr = 30; printf("Converted value:%d\n",*c_ptr); return 0; }
内存操作
我们对于内存操作需要依赖于string.h
头文件中相关的函数完成
内存操作函数
内存填充
memset
-
头文件:
#include <string.h>
-
函数原型:
void* memset(void* s,int c,size_t n);
-
函数功能:将内存卡
s
的前n
个字节填充为c
-
参数说明:
-
void* s
:目标内存首地址 -
int c
:填充值(以unsigned char
形式处理(0~255)) -
size_t n
:填充字节数(size_t
无符号长整型)
-
-
返回值:返回
s
的指针 -
注意事项:
-
常用于动态化初始化,
c
通常设置为0(清零) -
按字节填充,非整型初始化需要谨慎(如填充
int
数组时,0是安全的)
-
-
示例:
#include <stdlib.h> #include <string.h> // 在堆区申请4个int的连续空间 int* p = (int*)malloc(4 * sizeof(int)); // 建议写常量表达式 // 非空校验(校验指针有效性) if(p == NULL) { perror("内存申请失败!"); return -1; } // 初始化堆内存,填充0 memset(p,0,4 * sizeof(int)); // 测试输出 printf("%d\n",*(p+1)); //*p = *(p+0) = p[0] // 内存使用完毕,需要释放 free(p); // 此时要对指针p置空 p = NULL;
内存拷贝
memcpy/memmove
-
头文件:
#include <string.h>
-
函数原型:
-
void* memcpy(void* dest, const void* src, size_t n)
:(源与目标内存无重叠时使用) -
void* memmove(void* dest, const void* src, size_t n)
:(安全处理内存重叠)
-
-
函数功能:将
src
的前n
个字节拷贝到dest
(从低位开始,因为指针指向从低到高) -
参数说明:
-
void* dest
:目标内存首地址 -
const void* src
:源内存首地址 -
size_t n
:拷贝字节数
-
-
返回值:返回
dest
的首地址 -
注意事项:
-
memmove
能正确处理内存重叠,推荐优先使用 -
确保目标内存足够大,避免溢出
-
-
示例:
#include <string.h> // 准备两个数组,用来作为源和目标 int src[4] = {11,22,33,44}; int dest[6] = {111,222,333,444,555,666}; // 进行拷贝 memcpy(dest+1,src+1,2 * sizeof(int)); // 从src拷贝22,33到dest的222的位置 memmove(dest+1,src+1,2 * sizeof(int)); // 从src拷贝22,33到dest的222的位置 printf("源数组:\n"); register int i; for(i = 0; i < 4; i++) { printf("%-6d",src[i]); } printf("\n"); printf("目标数组:\n"); for(i = 0; i < 6; i++) { printf("%-6d",dest[i]); } printf("\n");
课后思考:
什么是内存重叠?
进行内存操作时,源内存区域和目标内存区域存在部分或者完全重叠,如果不处理内存重叠,可能导致数据被意外覆盖,引发未定义行为。
内存比较
memcmp
-
头文件:
#include <string.h>
-
函数原型:
int memcmp(const void* s1, const void* s2, size_t n)
-
函数功能:比较
s1
和s2
的前n
个字节 -
返回值:
-
0
:内存内容相同 -
>0
:s1
中第一个不同字节大于s2
-
<0
:s1
中第一个不同字节小于s2
-
-
注意事项:比较按字节进行,非字符串需确保参与比较的字节数一致(总字节数一致)(不一致按小的来超出不比较)
-
示例:
#include <stdlib.h> #include <string.h> // 准备比较的数据 int* arr1 = (int*)malloc(3 * sizeof(int)); int* arr2 = (int*)calloc(4,sizeof(int)); // 清零 memset(arr1,0,3 * sizeof(int)); // 赋值 arr1[0] = 65; arr1[1] = 66; arr2[0] = 70; arr2[1] = 5; // 比较 int cmp_result = memcmp(arr2,arr1, 2 * sizeof(int)); // 双方比较字节数完全一致 printf("比较结果:%d\n",cmp_result); // 5 70-65 // 内存使用完毕,释放内存 free(arr1); free(arr2); // 如果后面要用到,返回到外面,需要置空;不用的话就不需要置空 arr1 = NULL; arr2 = NULL;
内存查找
memchr/memrchr
-
头文件:
#include <string.h>
-
函数原型:
-
void* memchr(const void* s, int c, size_t n)
:(正向查找) -
void* memrchr(const void* s, int c, size_t n)
:(逆向查找,这个不是C语言标准库提供,属于GNU扩展)
-
-
函数功能:在
s
的前n
个字节中查找字符c
-
返回值:
-
成功
:返回指针 -
失败
:返回NULL
-
-
注意事项:
-
memrchr
:是GNU扩展函数,需手动声明 -
查找单位为字节值,非整型数据需要注意内存布局
-
-
示例:
#include <string.h> // memrchr是GNU扩展函数,需要外部声明 extern void* memrchr(const void* s, int c, size_t n); int main(int argc, char*argv[]) { char str[] = {'A','B','C','B'}; // 查找字符'B'(ASCII 66) char* first = (char*)memchr(str, 'B', sizeof(str)); char* last = (char*)memrchr(str, 'B', sizeof(str)); printf("first=%p,last=%p\n",first,last); printf("第1个B的位置:%ld\n",first - str); // 1 printf("最后1个B的位置:%ld\n",last - str); // 3 return 0; }
综合案例:学生成绩管理系统
-
需求:
要求实现一个基于指针的学生成绩管理系统,具体功能如下:
-
添加学生信息:输入学号和三门成绩,存储到数组中
-
显示所有学生信息:遍历数组,输出每个学生的学号和成绩
-
计算每个学生的平均分和总分:遍历数组,计算每行的总分和平均分
-
根据某科成绩排序:用户选择科目,然后按该科成绩排序,可以升序或降序
-
查找学生信息:按学号查找,显示该生的成绩和平均分
-
退出程序。
-
-
代码:
#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], int count); void sortStudents(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count); void searchStudent(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count); int validateId(char *id); 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作者:momo \033[1;32m |\033[0m\n"); printf("\033[1;32m+----------------------------+\033[0m\n"); printf("\033[1;32m1. 添加学生信息\033[0m\n"); printf("\033[1;32m2. 显示所有记录\033[0m\n"); printf("\033[1;32m3. 查看统计信息\033[0m\n"); printf("\033[1;32m4. 成绩排序\033[0m\n"); printf("\033[1;32m5. 查找学生\033[0m\n"); printf("\033[1;32m6. 退出系统\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, studentCount); break; case 4: // 成绩排序 sortStudents(scores, studentIds, studentCount); break; case 5: // 查找学生 searchStudent(scores, studentIds, studentCount); break; case 6: // 退出系统 printf("系统已退出,感谢使用!\n"); break; default: printf("无效输入,请重新选择!"); } }while(choice != 6); return 0; } /** * 添加学生信息 * @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); // 非法字符校验 int tempScore = scanf("%d",&scores[*count][i]); if(tempScore != 1) { printf("错误:成绩无效,请重新输入!\n"); // 清空缓冲区 while(getchar() != '\n'); continue; } // 输入范围校验 if(scores[*count][i] < 0 || scores[*count][i] > 100) { printf("错误:成绩无效,请重新输入!\n"); continue; } i++; } // 改变studentCount的值 (*count)++; printf("学生信息添加成功!\n"); } /** * 显示所有记录 * @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("%-8s %-6s %-6s %-6s\n","学号","语文","数学","英语"); // 表格-数据 for(int i = 0; i < count; i++) { printf("%-8.4s",ids[i]); // 学号 // 成绩 for(int j = 0; j < COURSE_NUM; j++) { printf("%-6d",*(*(scores + i) + j)); // scores[i][j] } printf("\n"); } printf("\n"); } /** * 显示统计信息 * @param scores 成绩二维数组 * @param count 已录入学生数量 */ void showStatistics(int (*scores)[COURSE_NUM], int count) { // 校验是否存在学生 if(count == 0) { printf("暂无学生数据!\n"); return; } //创建一个数组,用来存储每一科总分 int courseTotal[COURSE_NUM] = {0}; // 创建一个数组,用来存储每一科最高分 int max[COURSE_NUM] = {0}; // 创建一个数组,用来存储每一科最低分 int min[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 > max[i]) max[j] = score; // 单科最低分 if(score < min[j]) min[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",max[i]); printf(" 最低分:%d\n",min[i]); } } /** * 成绩排序 * @param scores 学生成绩二维数组 * @param ids 学生学号二维数组 * @param count 已录入学生人数 */ void sortStudents(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count) { if(count < 2) { printf("需要至少2条记录才能排序!\n"); return; } int course; printf("\n请选择排序科目(1-语文 2-数学 3-英语): "); scanf("%d", &course); if(course < 1 || course > 3) { printf("无效的科目选择!\n"); return; } course--; // 转换为数组索引 // 使用改进的冒泡排序 for(int i = 0; i < count-1; i++) { int swapped = 0; for(int j = 0; j < count-1-i; j++) { if(scores[j][course] < scores[j+1][course]) { // 降序排列 // 交换学号 char tempId[ID_LENGTH]; memcpy(tempId, ids[j], ID_LENGTH); memcpy(ids[j], ids[j+1], ID_LENGTH); memcpy(ids[j+1], tempId, ID_LENGTH); // 交换成绩 int tempScores[COURSE_NUM]; memcpy(tempScores, scores[j], sizeof(tempScores)); memcpy(scores[j], scores[j+1], sizeof(scores[j][0]) * COURSE_NUM); memcpy(scores[j+1], tempScores, sizeof(tempScores)); swapped = 1; } } if(!swapped) break; } printf("排序完成!\n"); } /** * 学生查找 * @param scores 学生成绩二维数组 * @param ids 学生学号二维数组 * @param count 已添加学生数量 */ void searchStudent(int (*scores)[COURSE_NUM], char (*ids)[ID_LENGTH], int count) { char targetId[ID_LENGTH + 1]; printf("\n请输入要查找的学号: "); scanf("%4s", targetId); while (getchar() != '\n'); // 清除输入缓冲区 for(int i = 0; i < count; i++) { if(memcmp(ids[i], targetId, ID_LENGTH) == 0) { printf("找到学生记录:\n"); printf("学号: %.4s\n", ids[i]); printf("成绩: 语文=%d 数学=%d 英语=%d\n",scores[i][0], scores[i][1], scores[i][2]); return; } } printf("未找到该学号的学生记录!\n"); } /** * 学号校验 * @param id 校验的学号(数组) * @return 1-校验合格,0-校验不合格 */ int validateId(char *id) { char *p = id; // 不要直接操作形参,最好使用局部变量接收 int len = 0; // 遍历字符串 while(*p && len < ID_LENGTH) { // 校验输入的是否是数字 if(!(*p >= '0' && *p <= '9')) { printf("学号必须为数字!\n"); return 0; } p++; len++; } // 校验输入的长度 if(len != ID_LENGTH || *p != '\0') { printf("学号必须为4位!\n"); return 0; } return 1; }