声明
本题在以C语言编写,在VS2022 X64的环境下运行
一、题目简介
一、任务
基于c语言的简易多级菜单管理系统实现多次执行指定任务的功能。
工作过程:参赛者通过按键进行菜单控制,并实现一些简单的数据操作。
二、要求
1、基础部分(60分)
(1)运行后自动进入一级菜单,显示4栏信息,命名为1.录入学生信息,2.查询学生信息、3.删除学生信息,4.根据学生成绩排序,5.退出系统。
(2)键入对应信息栏的数字后进入相应的次级菜单或返回父级菜单(二级菜单1,2,3;三级菜单4)。
2、发挥部分(40分)
(1)重新运行后学生信息不丢失。
(2)能用链表进行数据储存。
(3)检测用户输入合法性,在用户输入错误时不予执行并提醒。
- 说明
(1)学生信息有学号,姓名,单科成绩;系统内内置参赛者本人的学号姓名信息,成绩只需录入一科即可;
(2)要求能够流畅进入和退出任意级菜单3次以上,不得出现卡死等现象;二级菜单内的所有功能要求能够反复运行,直到用户选择退出。
(3)在录入学生信息时,可分别录入学号、姓名、成绩,顺序不限。
(4)用户输入合法性包括:录入学号时输入了文字、姓名中出现数字、选择菜单时未选择正确。
二、一些相关知识
我在此处收集了一些我们接下来在实现时会使用到的相关知识,在接下来的函数中首次使用时我会声明该知识的编号。
1、链表的建立与连接
我们在正式录入数据之前,先在主函数和各个功能函数之外声明链表的头指针。
struct stu_inf* head = NULL;
我们这里插播一下链表的相关概念:
(1)链表的相关概念
链表就像火车,在它中存的数据的地址本来在内存中是不连续的,但是链表就像一节火车一样,将每一个节点连接在一起。
-
每节车厢就像链表的一个节点
-
车厢之间的连接钩就像节点的指针
-
车头就像链表的头指针
-
链表通过在上一个节点中声明下一个节点的指针来实现各个节点之间的连接
-
火车: [车厢1] → [车厢2] → [车厢3] → ... → [最后一节车厢]
链表: [节点1] → [节点2] → [节点3] → ... → [尾节点]
引自deepseek
链表的使用是相当灵活的,我们可以自由的插入新的数据、在不改变数据的情况下通过移动各个节点的位置来实现链表顺序的转换。
刚才提到了链表通过在上一个节点中声明下一个节点的指针来实现各个节点之间的连接
我们应该在结构体的定义中实现这一点
struct stu_inf
{
int num;
char name[20];
int score;
struct stu_inf* next;//旨在声明结构体的下一个元素仍旧是这个结构体类型
};
struct stu_inf* next;//sh
现在我们就可以认为这是一个标准的链表节点的模板了
现在模板有了,我们如何实现链表的连接呢??
(2)链表的连接(添加元素使用)
我介绍一种最常用的连接方法:头插法
我先展示代码
struct stu_inf* new_stu=NULL
//录入过程.....
new_stu->next=head;
//将新元素插在“火车头”head指针的前面
head=new_stu;
//将“火车头”head指针放回之前的位置
这里的原理我简单解释一下:
第一步是将head的地址赋给new_stu的next,即表示new_stu的下一个结点的地址是head的地址。
而head的地址本来是指向haed的下一个节点,所以可以等价于将new_stu于他的下一个节点连在了一起。
下一步,我们将head的地址指向new_stu,即是将new_stu的地址赋值给head。
通过以上操作,我们就成功地将新元素添加到了链表中。
在了解了链表中元素的添加后,我们就可以实现学生信息的录入了
(3)链表中结点的调用
我们在本项目会采用的方式是遍历调用:
struct stu_inf* p=head;
while(p!=NULL)
{
if(/*判标*/)
{
//内容
break;
}
}
p=head;//重置
(4)链表中数据的删除
链表中数据的删除可以看作是火车的脱钩,即将想要删除的数据先从链表中隔离并将它的内存释放
想实现这个操作,一般是采用双指针法。
struct stu_inf* left = NULL;
struct stu_inf* right = head;
在双指针法中,right指针总是比left领先一个”身位“
left=right;
right=right->next;
right用来找到满足删除条件的节点
当找到符合删除条件的节点时,我们可以很轻易的实现想要删除的节点的”脱钩“,如下:
left->next=right->next;
right=right->next;
这时,我们想要删除的right指针指向的内容就成功被隔离了,我们最好追加定义一个指针专门用来释放该节点的内容。
struct stu_inf* left = NULL;
struct stu_inf* right = head;
struct stu_inf* to_delete = NULL;
left->next=right->next;
to_delete=right;
right=right->next;
free(to_delete);
这样我们就可以将想要删除的节点的内存释放干净,防止内存泄露,而且不影响后续的删除(但是本题的学生的学号是补重复的,所以后续也不需要删除)
2、 sscanf_s函数
函数原型
int sscanf_s(
const char *buffer, // 参数1:输入字符串
const char *format, // 参数2:格式字符串
... // 参数3+:可变参数(目标变量)
);
参数中的格式符
常用格式符总结表
| 格式符 | 用途 | 是否需要大小参数 |
|---|---|---|
%d | 十进制整数 | ❌ |
%i | 自动识别进制 | ❌ |
%u | 无符号整数 | ❌ |
%o | 八进制整数 | ❌ |
%x | 十六进制整数 | ❌ |
%f | 浮点数 | ❌ |
%c | 单个字符 | ✅ |
%s | 字符串 | ✅ |
%[] | 字符集合 | ✅ |
%p | 指针 | ❌ |
%n | 字符计数 | ❌ |
%* | 跳过输入 |
❌ |
格式字符串应该保持与输入字符串中的形式相同
在此项目中的体现就是文件的形式应该和sscanf_s函数中格式字符串的格式相符
返回值
返回值类型:int
-
含义:成功解析并赋值的参数数量
-
范围:
0到格式字符串中的格式说明符数量 -
特殊值:
EOF(通常是 -1)表示错误
这个返回值是有说法的!!!
例如在本题的的环境下,我们就可以通过它的返回值来判断是否读取了我们想要的数据,从而筛选出一些不正确的输入。
使用方法
读取数字:(不需要大小参数)
char input[] = "1001 85";
int num, score;
// 数字格式不需要缓冲区大小
int result = sscanf_s(input, "%d %d", &num, &score);
if (result == 2) {
printf("学号: %d, 成绩: %d\n", num, score);
}
读取字符串:(需要大小参数)
char input[] = "1001 张三 85";
int num, score;
char name[20];
// 字符串格式需要缓冲区大小
int result = sscanf_s(input, "%d %s %d",
&num, name, (unsigned)sizeof(name), &score);
if (result == 3) {
printf("学号: %d, 姓名: %s, 成绩: %d\n", num, name, score);
}
3、qsort函数
函数原型:
void qsort(void* base, size_t num, size_t size, int (*compar)(const void*, const void*));
-
base:指向要排序数组的指针
-
num:数组中元素的个数 -
size:每个元素的大小(字节) -
compar:比较函数指针
在使用的时候应该注意num和size的类型size_t(无符号整型)
compar函数
在自定义compar函数的时候也要注意它的参数是两个const void*类型的指针。
返回值应该是整形,当前一个指针指向的数字大于后一个指针指向的数字时返回值大于0,两个值相等时是0,前一个小于后一个时返回值小于0。
我在此处对qsort的compar函数做一个例子:
我们这里是想通过compar来实现比较p1和p2指向的数据
void compar(const void* p1,const void* p1)
{
return *(unsigned int)p1 - *(unsigned int)p2;
}
const void*详解
两个函数是const void*类型的,const指的是该指针指向的数据是不可改变的。
void*是一个可以接受所有类型的指针的指针,但是在使用的时候应该进行强制类型转换。
4、文件操作
1、fopen_s
参数
-
pFile:指向 FILE 指针的指针(二级指针)(FILE是文件指针) -
filename:要打开的文件名 -
mode:打开模式
返回值
-
成功返回 0
-
失败返回错误代码
文件打开模式:
| 模式 | 说明 | 文件存在 | 文件不存在 |
|---|---|---|---|
| "r" | 只读 | 打开文件 | 失败 |
| "w" | 只写 | 清空文件 | 创建新文件 |
| "a" | 追加 | 追加到末尾 | 创建新文件 |
| "r+" | 读写 | 打开文件 | 失败 |
| "w+" | 读写 | 清空文件 | 创建新文件 |
| "a+" | 读写 | 追加到末尾 | 创建新文件 |
2、fclose
函数原型
int fclose(FILE* stream);
参数
-
stream:要关闭的文件指针
返回值
-
成功返回 0
-
失败返回 EOF
5、fprintf函数
fprintf 是 C 语言中用于格式化输出到文件的函数。
函数原型
int fprintf(FILE* stream, const char* format, ...);
参数说明
-
stream:文件指针 -
format:格式字符串 -
...:可变参数(要输出的数据)
返回值
-
成功:返回输出的字符数
-
失败:返回负值
6、fgets函数
函数原型:
char* fgets(char* str, int num, FILE* stream);
参数说明
-
str:指向字符数组的指针,用于存储读取的字符串 -
num:要读取的最大字符数(包括空字符) -
stream:文件指针
返回值
-
成功:返回
str指针 -
失败或到达文件末尾:返回
NULL
读取示例
输入: "Hello World\n"
fgets 读取: "Hello World\n\0"
移除换行符: "Hello World\0"
遇到 \n 自动停止读取
三、主函数基本框架
我们首先来构思一下,本题要求较为复杂,我们肯定不能在一个主函数中全部实现,故我们先在主函数中实现菜单和一级菜单选项功能,并声明函数。
本题要实现储存学生的学号、姓名、成绩,且要使用链表进行数据储存。这里的三个数据都是属于一个人,如果使用三个独立的变量来储存,难免会出现数据完整度不佳的问题等诸多问题。所以我们这里先定义一个结构体
(一)、定义结构体
struct stu_inf
{
//学号,姓名,单科成绩
int num;
char name[20];
int score;
struct stu_inf* head = NULL;
};
然后我们准备链表的头指针
struct stu_inf* head = NULL;
至于链表的连接,我们在输入函数中实现
(二)、主菜单的实现
int main()
{
load_from_file();
int flag = 0;
do
{
printf_menu();
scanf_s("%d", &flag);
switch (flag)
{
case 1:
{
enter_stu();
break;
}
case 2:
{
check_stu();
break;
}
case 3:
{
delete_stu();
break;
}
case 4:
{
sort_stu_score();
break;
}
case 5:
{
printf("退出简易管理系统\n");
break;
}
default:
{
printf("无效的输入,请重新输入\n");
break;
}
}
} while (flag!=5);
save_to_file();
free_line();
return 0;
}
游戏的一级菜单的实现依旧采用经典的do while和switch嵌套,该结构可以很好地实现选项和输入合法性验证,是新手不得不尝的经典模型。
在开始的时候,自定义一个函数load_from_file,指从内存中读取数据;最后的save_to_file函数旨在在函数结束的时候将数据储存在文件中,free_line是释放链表中的数据,其他的函数则对应各自的功能。
在这里我们还应该重视一个问题:在菜单之后的功能中,我们要实现诸如姓名等的字符串的输入,正常来讲想要实现字符串的输入基本上是采用fgets的形式。
但是fgets会读取换行符\n,而菜单选择中的scanf_s又不会读取换行符,这会导致\n残留在缓存区中,在读取字符串的时候直接读到换行符,导致字符串提前读完。
所以我们这里封装一个函数来专职清除缓存区
(三)、通用函数clear_input:清除缓存区
void clear_input()
{
int c = 0;
while ((c = getchar()) != '\n' && c != EOF);
}
这一段代码看起来很少,但是它还是很值得说道的
getchar函数
getchar函数是一个很常用的函数,它的函数原型是
int getchar(void);
返回值:
-
成功:返回读取的字符(作为
int类型) -
失败或文件结束:返回
EOF(通常是 -1)
函数功能:读取缓冲区中的单个字符。
我们可以看到,getchar没有参数,返回值是int类型的。
所以我们这里使用了一个int类型的变量c来承接getchar的返回值,并使用了一个while循环来实现在 同时满足缓冲区尚未读完和未读到换行符时 继续读取的功能。
在读到\n的时候,while循环就会结束,我们就可以通过这个函数实现读取并丢弃缓冲区中的所有字符,直到遇到换行符或文件结束。
然后我们来逐一实现这些函数
四、四个主功能的实现
(零)、printf_menu---打印菜单
这应该是最简单的函数了,我甚至感觉将他封装都不是很有必要,但是出于使函数更加简洁的目的,我还是将他封装了。
void printf_menu()
{
printf("==========================\n");
printf("**************************\n");
printf("**** 1.录入学生信息 ****\n");
printf("**** 2.查询学生信息 ****\n");
printf("**** 3.删除学生信息 ****\n");
printf("** 4.根据学生成绩排序 **\n");
printf("****** 5.退出系统 ******\n");
printf("**************************\n");
printf("==========================\n");
printf("请输入想要进行的操作\n");
}
OK了,就是这么简单,这个函数通俗易懂,并且只用一次,所以不过多介绍。
(一)、enter_stu---录入数据
在学生数据的录入这个环节,我们就要考虑输入的合法性问题了,这里我想到了几个点:
1、输入的学生数据是否是重复的
此处可以使用学号判断,毕竟姓名可能相同,但是学号一定是独一无二的
2、输入的姓名是否包含字符
此处我们将除了空格之外的所以符号视为不合法
3、输入的单科成绩是否在正确的范围内
我们采取最普遍的100分制,在0到100之间的是合法的
我们依旧先搭框架:
void enter_stu()//1.录入学生信息
{
int flag = 0;//记录选项
do
{
printf("1.录入,2.退出\n");
printf("请选择\n");
scanf_s("%d", &flag);
switch (flag)
{
case 1:
{
}
case 2:
{
printf("退出\n");
break;
}
}
} while (flag != 2);
}
框架搭好了,我们来实现一下录入的功能
1、输入的标准模板
考虑到在输入的时候可能会出现输入不合法需要重新输入的情况,我们这里采用while循环来实现输入的逻辑。
我们在每一个函数中使用字符类型的mid数组来读取输入的数值并使用sscanf_s函数来实现将数字存入链表中。
while (1)
{
char mid[100];
printf("请输入学生的学号\n");
fgets(mid, sizeof(mid), stdin);
mid[strcspn(mid, "\n")] = '\0';
if (strlen(mid) <= 0)
{
printf("输入不能为空\n");
continue;
}
if (sscanf_s(mid, "%d", &new_stu->num )== 1)//代表成功读取
{
//可以添加专属的判断标准
}
else
{
printf("错误形式的输入\n");
continue;
}
break;
}
while (1)
经过简单分析,我们可以知道输入不合法的的共性条件无非是输入为空和输入错误的类型,那我们通用框架中实现它
在这里使用sscanf_s可以很好地利用sscanf_s的特性,我设置了一个if来判断sscanf_s的返回值是不是我想要的1(因为这里我们想实现正确赋值的参数数量只有一个);
如果不是的话,就代表识别出来的数据类型不符合我们在sscnaf_s函数中设置的期望的函数类型;
此时我就设置一个else来表达这种不合法的输入类型,并且在给予提示后continue跳过,进入下一次输入。
通过这样的方式,我们就可以在输入数据的同时实现对错误数据类型的筛选。
2、子功能一:学号的输入
while (1)
{
char mid[100];
printf("请输入学生的学号\n");
fgets(mid, sizeof(mid), stdin);
mid[strcspn(mid, "\n")] = '\0';
if (strlen(mid) <= 0)
{
printf("输入不能为空\n");
continue;
}
if (sscanf_s(mid, "%d", &new_stu->num )== 1)//代表成功读取
{
if (check_num(new_stu->num) == 0)
{
printf("输入重复的学号\n");
continue;
}
}
else
{
printf("错误形式的输入\n");
continue;
}
break;
}
我将检查是否重复的功能封装为了一个函数,如下:
int check_num(int n)
{
if (head != NULL)
{
struct stu_inf* p = head;
printf("正在检查学号是否重复\n");
Sleep(1000);
while (p != NULL)
{
if (p->num == n)
{
printf("学号重复,已有学号%d,姓名%s,成绩%d", p->num, p->name, p->score);
return 0;
}
p = p->next;
}
}
printf("学号%d可用\n", n);
return 1;
}
这里先分head是否是空指针的两种情况(不分的话编译器会报错),当head不是空指针的时候才进行验证,当head是空指针的时候直接跳过。
再依次遍历链表中数据,验证是否重复。
最后返回值,1代表不重复,0代表重复。
3、子功能二:姓名的输入
姓名的输入和检查较为复杂,我们先来思考一下应该在格式化字符中使用什么类型的格式符
在sscanf_s中选用哪个格式符
我一开始时使用%s,但在后续测试中发现%s在读取中间带空格的字符是会读不到后面的内容
我查了一下%s的工作原理
-
从非空白字符开始读取
-
遇到空格、制表符、换行符时停止
不难发现,%s读到空格就截止了,那我们有没有办法在不改变sscanf_s这个主要的输入方式的情况下正常读取呢?
有的,兄弟,有的
我们可以使用 %[^]
%[^排除字符]
作用:
-
读取字符,直到遇到排除字符为止
-
类似于"读取直到..."的功能
我们只要在函数中使用%[^\n]就可以完美实现读取整行的目标了
while (1)
{
printf("请输入该同学的姓名\n");
printf("支持中英文,空格,.(最多20个字符)\n");
char mid[20];//输入的中间值
fgets(mid, sizeof(mid), stdin);
mid[strcspn(mid, "\n")] = '\0';
if (strlen(mid) <= 0)
{
printf("输入不能为空\n");
continue;
}
if (sscanf_s(mid, "%[^\n]", new_stu->name, (unsigned int)sizeof(new_stu->name)) == 1)
{
if (check_name(new_stu->name) == 0)
{
printf("输入不合法\n");
continue;
}
}
else
{
printf("错误形式的输入,请重新输入\n");
continue;
}
printf("输入合法\n");
break;
}
我在函数内部自定义了一个检查函数check_name,现在我们来实现它:
check_name的实现
int check_name(char name[])
{
char c;
printf("正在检查输入的合法性......\n");
Sleep(1000);
for (int i = 0; name[i] != '\0'; i++)
{
c = name[i];
if ((name[i]>'0'&&name[i]<'9')||(name[i] == '*') || (name[i]=='%')|| (name[i] == '&')||(name[i]=='%')||
(name[i] == '#') || (name[i] == '@')|| (name[i] == '!')|| (name[i] == '(')|| (name[i] == ')')|| (name[i] == '^')
|| (name[i] == ',')|| (name[i] == '/')|| (name[i] == '?')|| (name[i] == '\'')|| (name[i] == '\"')|| (name[i] == '<')
|| (name[i] == '>')|| (name[i] == '-')|| (name[i] == '_')|| (name[i] == '+')|| (name[i] == '=')|| (name[i] == '|')||
(name[i] == '\\'))
{
return 0;
}
}
return 1;
}
4、子功能三:成绩的输入
这个子功能就比较简单了,和之前的学号输入差不多,我们直接实现:
while (1)//成绩的录入
{
char mid[100];
printf("请输入学生的成绩\n");
fgets(mid, sizeof(mid), stdin);
mid[strcspn(mid, "\n")] = '\0';
if (strlen(mid) <= 0)
{
printf("输入不能为空\n");
continue;
}
if (sscanf_s(mid, "%d", &new_stu->score) == 1)
{
if (new_stu->score > 100 || new_stu->score < 0)
{
printf("成绩必须在0到100之间\n");
continue;
}
}
else
{
printf("错误形式的输入\n");
continue;
}
break;
}
在这里直接检查录入的成绩是不是在0到100之间就行了,其他的功能在框架中就已经实现了。
enter_stu的最终实现
void enter_stu()//1.录入学生信息
{
int flag = 0;
do
{
printf("1.录入、2.退出\n");
scanf_s("%d", &flag);
clear_input();
switch (flag)
{
case 1:
{
struct stu_inf* new_stu = NULL;
new_stu = (struct stu_inf*)malloc((unsigned)sizeof(struct stu_inf));
if (new_stu == NULL)
{
printf("内存分配失败\n");
}
if (new_stu == NULL)
{
printf("内存分配失败\n");
return;
}
//因为是实际录入,所以要实际分配内存
while (1)//学号的录入
{
char mid[100];
printf("请输入学生的学号\n");
fgets(mid, sizeof(mid), stdin);
mid[strcspn(mid, "\n")] = '\0';
if (strlen(mid) <= 0)
{
printf("输入不能为空\n");
continue;
}
if (sscanf_s(mid, "%d", &new_stu->num )== 1)//代表成功读取
{
if (check_num(new_stu->num) == 0)
{
printf("输入重复的学号\n");
continue;
}
}
else
{
printf("错误形式的输入\n");
continue;
}
break;
}
while (1)//姓名的录入
{
char mid[100];
printf("请输入学生的姓名\n");
fgets(mid, sizeof(mid), stdin);
mid[strcspn(mid, "\n")] = '\0';
if (strlen(mid) <= 0)
{
printf("输入不能为空\n");
continue;
}
if (sscanf_s(mid, "%[^\n]", new_stu->name,(unsigned int)sizeof(new_stu->name)) == 1)
{
if (check_name(new_stu->name) == 0)
{
printf("学生名字中仅支持中英文和空格\n");
continue;
}
}
else
{
printf("错误形式的输入\n");
continue;
}
break;
}
while (1)//成绩的录入
{
char mid[100];
printf("请输入学生的成绩(整数)\n");
fgets(mid, sizeof(mid), stdin);
mid[strcspn(mid, "\n")] = '\0';
if (strlen(mid) <= 0)
{
printf("输入不能为空\n");
continue;
}
if (sscanf_s(mid, "%d", &new_stu->score) == 1)
{
if (new_stu->score > 100 || new_stu->score < 0)
{
printf("成绩必须在0到100之间\n");
continue;
}
}
else
{
printf("错误形式的输入\n");
continue;
}
break;
}
new_stu->next = head;
head = new_stu;
printf("学号为%d的同学的数据储存成功\n", new_stu->num);
break;
}
case 2:
{
printf("退出\n");
break;
}
default:
{
printf("无效的输入\n");
break;
}
}
} while (flag != 2);
}
int check_name(char name[])
{
if (name == NULL || strlen(name) <= 0)
{
return 0;
}
int i = 0;
const char* un_char = "!@#$%^&*()_+{}[]:;\"\'\\/.,><`~";
while (name[i] != '\0')
{
if ((unsigned char)name[i] < 32 || strchr(un_char, name[i]) != NULL)
{
return 0;
}
i++;
}
return 1;
}
(二)、check_stu---查询信息
涉及到的知识:1、(3)链表中结点的调用
我们的内容都是在链表中储存的,我们可以采用遍历链表的方式对其中的数据进行访问。
至于菜单的实现,我们依旧采用标准的do while+switch,具体如下:
void check_stu()//2.查询学生信息
{
int flag = 0;
struct stu_inf* p = head;
do
{
printf("1.查询(使用学号),2.退出\n");
scanf_s("%d", &flag);
clear_input();
switch (flag)
{
case 1:
{
check_by_num();
break;
}
case 2:
{
printf("退出到主菜单\n");
break;
}
default:
{
printf("错误形式的输入\n");
break;
}
}
} while (flag != 2);
}
可以看到:我将查询的逻辑单独封装为了一个函数,现在我们来实现它:
void check_by_num()
{
struct stu_inf* p = head;
int n = 0;
printf("请输入想要查询的人的学号\n");
scanf_s("%d", &n);
clear_input();
if (head == NULL)
{
printf("未录入数据,没有查到\n");
return;
}
else
{
int found = 0;//作为找没找到的判标
while (p != NULL)
{
if (n == p->num)
{
printf("找到了,学号是%d,姓名是%s,成绩是%d\n", p->num, p->name, p->score);
found = 1;
return;
}
p = p->next;
}
if (found == 0)
{
printf("没找到学号为%d的学生\n", n);
}
}
}
当head为NULL的时候说明链表都没有被动过,也就是链表中根本没有存数据,此时我们直接return结束函数,剩下的就不用执行了;
如果链表中存数据了,那我们就使用遍历链表的方式来依次检查学号,从而找到想查找的学生的数据。
在正式开始查找之前,我设立了一个找没找到的判标found,初始值为0如果找到了就将判标赋值为1;然后我使用while循环来实现查找链表中的数据。
while循环的终止条件是p!=NULL,p从链表头head开始,在循环中如果没有找到就将p移向链表中下一个元素继续判断直到找到并直接return或遍历了每一个节点没有找到后跳出循环依据found==0打印没找到。
(三)、delete_stu删除信息
所用知识:1、(4)链表中数据的删除
删除的逻辑可以理解为先查找再删除,此处使用我之前提到的双指针法是一个不错的选择(可以自行查阅,此处不做赘述)
由于很多的逻辑是相通的,我这里直接上代码:
void delete_stu()//3.删除学生信息
{
int flag = 0;
do
{
printf("1.删除(使用学号),2.退出\n");
scanf_s("%d", &flag);
clear_input();
if (head == NULL)
{
printf("尚未存入数据,不能删除\n");
return;
}
switch (flag)
{
case 1:
{
struct stu_inf* left = NULL;//在循环里定义,省下重置
struct stu_inf* right = head;
struct stu_inf* to_delete = NULL;
int n = 0;
int found = 0;
int flag1 = 0;
printf("请输入学号\n");
scanf_s("%d", &n);
clear_input();
while (right != NULL)
{
if (right->num == n)
{
printf("找到了,学号是%d,姓名是%s,成绩是%d\n", right->num, right->name, right->score);
found = 1;
do
{
printf("是否确认删除\n");
printf("1.删除,2.取消\n");
scanf_s("%d", &flag1);
clear_input();
switch (flag1)
{
case 1:
{
if (left == NULL)
{
to_delete = head;
head = head->next;
}
else
{
to_delete = right;
left->next = right->next;
}
free(to_delete);
printf("学号为%d的学生的数据已删除\n", n);
break;
}
case 2:
{
printf("取消删除\n");
break;
}
default:
{
printf("错误形式的输入\n");
break;
}
}
} while (flag1 != 2&&flag1 != 1);//当选择了1或2时就不选了
}
if (found == 1)
{
break;
}
left = right;
right = right->next;
}
if (found == 0)
{
printf("没找到该同学的信息\n");
}
break;
}
case 2:
{
printf("退出\n");
break;
}
default:
{
printf("错误形式的输入\n");
break;
}
}
} while (flag != 2);
}
(四)、sort_stu_score按成绩排序
所用知识:3、qsort函数
题目的要求是将显示的数据按照成绩的大小来进行升序或降序排列。如果使用交换数据的思路来实现是很繁琐的,我们应该在不改变数据的情况下对数据的地址进行重新排列。
至于指针的排列,我们采用qsort的方式,因为qsort可以自定义排列方式,并且运行效率较高。
我们在使用qsort对链表实现重新排列的时候,先将所有的节点的指针存储在一个指针数组中,再将其作为参数传给qsort函数。
struct stu_inf* temp = NULL;
while (p != NULL)
{
num_stu++;
p = p->next;
}
p = head;
struct stu_inf** arr = (struct stu_inf**)malloc(num_stu * sizeof(struct stu_inf*));
//将链表中的数据的地址填入指针数组
for (int i = 0; p != NULL; i++)
{
arr[i] = p;
p = p->next;
}
compar函数
int sort_by_score_up(const void* p1, const void* p2)
{
return (*(struct stu_inf**)p1)->score - (*(struct stu_inf**)p2)->score;
}
int sort_by_score_down(const void* p1, const void* p2)
{
return (*(struct stu_inf**)p2)->score - (*(struct stu_inf**)p1)->score;
}
(struct stu_inf**)p2是将指针转换为结构体二级指针类型。
(*(struct stu_inf**)p2)是将二级指针解应用,使其类型为结构体一级指针。
(*(struct stu_inf**)p2)->score是结构体中的score。
最后我们来实现整个函数
void sort_stu_score()//4.排序
{
int num_stu = 0;
struct stu_inf* p = head;
if (head == NULL)
{
printf("目前尚未储存数据,无需排序\n");
return;
}
struct stu_inf* temp = NULL;
while (p != NULL)
{
num_stu++;
p = p->next;
}
p = head;
struct stu_inf** arr = (struct stu_inf**)malloc(num_stu * sizeof(struct stu_inf*));
//将链表中的数据的地址填入指针数组
for (int i = 0; p != NULL; i++)
{
arr[i] = p;
p = p->next;
}
int flag = 0;
do
{
printf("1.升序,2.降序,3.退出\n");
scanf_s("%d", &flag);
switch (flag)
{
case 1:
{
qsort(arr, num_stu, sizeof(struct stu_inf*), sort_by_score_up);
head = NULL;
relink(arr,num_stu);
printf("以下是排序后的数据:\n");
printf_file();
break;
}
case 2:
{
qsort(arr, num_stu, sizeof(struct stu_inf*), sort_by_score_down);
head = NULL;
relink(arr, num_stu);
printf("以下是排序后的数据:\n");
printf_file();
break;
}
case 3:
{
printf("退出\n");
break;
}
default:
{
printf("错误形式的输入\n");
break;
}
}
}while(flag != 3);
free(arr);//不然会内存泄漏
}
五、文件操作
所用知识:2、ssanf_s函数,4、文件操作,5、fprintf函数,6、fgets函数
1、load_form_file
void load_from_file()
{
if (DATA_FILE == NULL)
{
printf("数据储存文件不存在\n");
}
FILE* file=NULL;
errno_t flag = 0;
flag = fopen_s(&file, DATA_FILE, "r");
//只读
//fopen_s成功返回0,失败返回错误代码
if (flag != 0)
{
printf("打开储存文件失败\n");
return;
}
int num = 0;
char name[20];
int score = 0;
int count = 0;
int succes_num = 0;
int file_num = 0;
char line[100];//中间值
while (fgets(line,sizeof(line),file)!=NULL)
{
if (sscanf_s(line, "学号是%d 姓名是%s 成绩是%d\n", &num, name,(unsigned)sizeof(name), &score) == 3)
{
struct stu_inf* p = (struct stu_inf*)malloc(sizeof(struct stu_inf));
p->num = num;
strcpy_s(p->name, sizeof(p->name), name);
p->score = score;
printf("学号是%d 姓名是%s 成绩是%d的数据读取成功\n", p->num, p->name, p->score);
succes_num++;
p->next = head;
head = p;
}
else
{
printf("第%d个数据读取失败\n",count);
file_num++;
}
count++;
}
printf("在%d个数据中,%d个读取成功,%d个读取失败\n", count, succes_num, file_num);
fclose(file);
}
FILE*文件指针
文件指针的状态:
| 状态 | 说明 |
|---|---|
FILE* fp = NULL | 未指向任何文件 |
FILE* fp = 有效值 | 指向打开的文件 |
FILE* fp = 无效值 | 文件已关闭或操作失败 |
读取文件
#define DATA_FILE "student_data.txt"
我们如何实现读取文件中每行的内容呢?
我在函数中定义了一个中间值line,并使用fgets从文件代理指针file中读取每一行的数据,并在接下来对读取的数据进行处理。
2、save_to_file
void save_to_file()
{
if (DATA_FILE == NULL)
{
printf("文件不存在\n");
}
if (head == NULL)
{
printf("当前未录入数据,无需储存\n");
return;
}
FILE* file = NULL;
//FILE类型本来就是指针
if (fopen_s(&file, DATA_FILE, "w") != 0)
{
printf("打开文件失败,无法储存\n");
return;
}
else
{
int num = 0;
char name[20];
int score=0;
int succes_num = 0;
int file_num = 0;
struct stu_inf* p = head;
while (p != NULL)
{
if (fprintf(file, "学号是%d 姓名是%s 成绩是%d\n", p->num, p->name, p->score) >= 0)
{
printf("学号是%d,姓名是%s,成绩是%d的数据储存成功\n", p->num, p->name, p->score);
succes_num++;
}
else
{
printf("学号是%d,姓名是%s,成绩是%d的数据储存失败\n", p->num, p->name, p->score);
file_num++;
}
p = p->next;
}
int total = succes_num + file_num;
printf("在%d人中有%d个储存成功、%d个储存失败\n",total,succes_num,file_num);
fclose(file);
//free(file);此处不应该free,因为free是释放内存,而且之前就已经fclose过了
}
}
在实现将数据储存的时候,采用fprintf函数来将数据存入
注意:要注意sscanf_s中读取的格式字符串的形式应该和fprintf中存入的格式字符串的形式一致
六、最终代码
#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<windows.h>
#include<ctype.h>
#include<errno.h>
#define DATA_FILE "student_data.txt"
//宏定义
struct stu_inf
{
int num;
char name[20];
int score;
struct stu_inf* next;
};
struct stu_inf* head = NULL;
void load_from_file()
{
if (DATA_FILE == NULL)
{
printf("数据储存文件不存在\n");
}
FILE* file=NULL;
errno_t flag = 0;
flag = fopen_s(&file, DATA_FILE, "r");
//只读
//fopen_s成功返回0,失败返回错误代码
if (flag != 0)
{
printf("打开储存文件失败\n");
return;
}
int num = 0;
char name[20];
int score = 0;
int count = 0;
int succes_num = 0;
int file_num = 0;
char line[100];
while (fgets(line,sizeof(line),file)!=NULL)
{
if (sscanf_s(line, "学号是%d 姓名是%s 成绩是%d\n", &num, name,(unsigned)sizeof(name), &score) == 3)
{
struct stu_inf* p = (struct stu_inf*)malloc(sizeof(struct stu_inf));
p->num = num;
strcpy_s(p->name, sizeof(p->name), name);
p->score = score;
printf("学号是%d 姓名是%s 成绩是%d的数据读取成功\n", p->num, p->name, p->score);
succes_num++;
p->next = head;
head = p;
}
else
{
printf("第%d个数据读取失败\n",count);
file_num++;
}
count++;
}
printf("在%d个数据中,%d个读取成功,%d个读取失败\n", count, succes_num, file_num);
fclose(file);
}
void save_to_file()
{
if (DATA_FILE == NULL)
{
printf("文件不存在\n");
}
if (head == NULL)
{
printf("当前未录入数据,无需储存\n");
return;
}
FILE* file = NULL;
//FILE类型本来就是指针
if (fopen_s(&file, DATA_FILE, "w") != 0)
{
printf("打开文件失败,无法储存\n");
return;
}
else
{
int num = 0;
char name[20];
int score=0;
int succes_num = 0;
int file_num = 0;
struct stu_inf* p = head;
while (p != NULL)
{
if (fprintf(file, "学号是%d 姓名是%s 成绩是%d\n", p->num, p->name, p->score) >= 0)
{
printf("学号是%d,姓名是%s,成绩是%d的数据储存成功\n", p->num, p->name, p->score);
succes_num++;
}
else
{
printf("学号是%d,姓名是%s,成绩是%d的数据储存失败\n", p->num, p->name, p->score);
file_num++;
}
p = p->next;
}
int total = succes_num + file_num;
printf("在%d人中有%d个储存成功、%d个储存失败\n",total,succes_num,file_num);
fclose(file);
//free(file);此处不应该free,因为free是释放内存,而且之前就已经fclose过了
}
}
int check_name(char name[])
{
if (name == NULL || strlen(name) <= 0)
{
return 0;
}
int i = 0;
const char* un_char = "!@#$%^&*()_+{}[]:;\"\'\\/.,><`~";
while (name[i] != '\0')
{
if ((unsigned char)name[i] < 32 || strchr(un_char, name[i]) != NULL)
{
return 0;
}
i++;
}
return 1;
}
void free_line()
{
struct stu_inf* temp = NULL;
struct stu_inf* p = head;
while (p != NULL)
{
temp = p;
p = p->next;
free(temp);
}
head = NULL;
//曾经把head free了两次
printf("链表已释放完毕\n");
}
int sort_by_score_up(const void* p1, const void* p2)
{
return (*(struct stu_inf**)p1)->score - (*(struct stu_inf**)p2)->score;
}
int sort_by_score_down(const void* p1, const void* p2)
{
return (*(struct stu_inf**)p2)->score - (*(struct stu_inf**)p1)->score;
}
void clear_input()//清除缓存区
{
int c = 0;
while ((c = getchar()) != '\n' && c != EOF);
}
void relink(struct stu_inf* arr[],int num)
{
for (int i = num-1; i >=0; i--)
{
if (arr[i] != NULL)
{
arr[i]->next = head;
head = arr[i];
}
else
{
printf("第%d个数据出错,无法储存\n",i+1);
}
}
}
void printf_file()
{
int count = 1;
struct stu_inf* p = head;
while (p != NULL)
{
printf("第%d个:学号:%d,姓名:%s,成绩%d\n",count, p->num, p->name, p->score);
count++;
p = p->next;
}
}
int check_num(int n)
{
if (head != NULL)
{
struct stu_inf* p = head;
printf("正在检查学号是否重复\n");
Sleep(500);
while (p != NULL)
{
if (p->num == n)
{
printf("学号重复,已有学号%d,姓名%s,成绩%d", p->num, p->name, p->score);
return 0;
}
p = p->next;
}
}
printf("学号%d可用\n", n);
return 1;
}
void check_by_num()
{
struct stu_inf* p = head;
int n = 0;
printf("请输入想要查询的人的学号\n");
scanf_s("%d", &n);
clear_input();
if (head == NULL)
{
printf("未录入数据,没有查到\n");
return;
}
else
{
int found = 0;
while (p != NULL)
{
if (n == p->num)
{
printf("找到了,学号是%d,姓名是%s,成绩是%d\n", p->num, p->name, p->score);
found = 1;
return;
}
p = p->next;
}
if (found == 0)
{
printf("没找到学号为%d的学生\n", n);
}
}
}
void enter_stu()//1.录入学生信息
{
int flag = 0;
do
{
printf("1.录入、2.退出\n");
scanf_s("%d", &flag);
clear_input();
switch (flag)
{
case 1:
{
struct stu_inf* new_stu = NULL;
new_stu = (struct stu_inf*)malloc((unsigned)sizeof(struct stu_inf));
if (new_stu == NULL)
{
printf("内存分配失败\n");
}
if (new_stu == NULL)
{
printf("内存分配失败\n");
return;
}
//因为是实际录入,所以要实际分配内存
while (1)//学号的录入
{
char mid[100];
printf("请输入学生的学号\n");
fgets(mid, sizeof(mid), stdin);
mid[strcspn(mid, "\n")] = '\0';
if (strlen(mid) <= 0)
{
printf("输入不能为空\n");
continue;
}
if (sscanf_s(mid, "%d", &new_stu->num )== 1)//代表成功读取
{
if (check_num(new_stu->num) == 0)
{
printf("输入重复的学号\n");
continue;
}
}
else
{
printf("错误形式的输入\n");
continue;
}
break;
}
while (1)//姓名的录入
{
char mid[100];
printf("请输入学生的姓名\n");
fgets(mid, sizeof(mid), stdin);
mid[strcspn(mid, "\n")] = '\0';
if (strlen(mid) <= 0)
{
printf("输入不能为空\n");
continue;
}
if (sscanf_s(mid, "%[^\n]", new_stu->name,(unsigned int)sizeof(new_stu->name)) == 1)
{
if (check_name(new_stu->name) == 0)
{
printf("学生名字中仅支持中英文和空格\n");
continue;
}
}
else
{
printf("错误形式的输入\n");
continue;
}
break;
}
while (1)//成绩的录入
{
char mid[100];
printf("请输入学生的成绩(整数)\n");
fgets(mid, sizeof(mid), stdin);
mid[strcspn(mid, "\n")] = '\0';
if (strlen(mid) <= 0)
{
printf("输入不能为空\n");
continue;
}
if (sscanf_s(mid, "%d", &new_stu->score) == 1)
{
if (new_stu->score > 100 || new_stu->score < 0)
{
printf("成绩必须在0到100之间\n");
continue;
}
}
else
{
printf("错误形式的输入\n");
continue;
}
break;
}
new_stu->next = head;
head = new_stu;
printf("学号为%d的同学的数据储存成功\n", new_stu->num);
break;
}
case 2:
{
printf("退出\n");
break;
}
default:
{
printf("无效的输入\n");
break;
}
}
} while (flag != 2);
}
void check_stu()//2.查询学生信息
{
int flag = 0;
struct stu_inf* p = head;
do
{
printf("1.查询(使用学号),2.退出\n");
scanf_s("%d", &flag);
clear_input();
switch (flag)
{
case 1:
{
check_by_num();
break;
}
case 2:
{
printf("退出到主菜单\n");
break;
}
default:
{
printf("错误形式的输入\n");
break;
}
}
} while (flag != 2);
}
void delete_stu()//3.删除学生信息
{
int flag = 0;
do
{
printf("1.删除(使用学号),2.退出\n");
scanf_s("%d", &flag);
clear_input();
if (head == NULL)
{
printf("尚未存入数据,不能删除\n");
return;
}
switch (flag)
{
case 1:
{
struct stu_inf* left = NULL;
struct stu_inf* right = head;
struct stu_inf* to_delete = NULL;
int n = 0;
int found = 0;
int flag1 = 0;
printf("请输入学号\n");
scanf_s("%d", &n);
clear_input();
while (right != NULL)
{
if (right->num == n)
{
printf("找到了,学号是%d,姓名是%s,成绩是%d\n", right->num, right->name, right->score);
found = 1;
do
{
printf("是否确认删除\n");
printf("1.删除,2.取消\n");
scanf_s("%d", &flag1);
clear_input();
switch (flag1)
{
case 1:
{
if (left == NULL)
{
to_delete = head;
head = head->next;
}
else
{
to_delete = right;
left->next = right->next;
}
free(to_delete);
printf("学号为%d的学生的数据已删除\n", n);
break;
}
case 2:
{
printf("取消删除\n");
break;
}
default:
{
printf("错误形式的输入\n");
break;
}
}
} while (flag1 != 2&&flag1!=1);
}
if (found == 1)
{
break;
}
left = right;
right = right->next;
}
if (found == 0)
{
printf("没找到该同学的信息\n");
}
break;
}
case 2:
{
printf("退出\n");
break;
}
default:
{
printf("错误形式的输入\n");
break;
}
}
} while (flag != 2);
}
void sort_stu_score()//4.排序
{
int num_stu = 0;
struct stu_inf* p = head;
if (head == NULL)
{
printf("目前尚未储存数据,无需排序\n");
return;
}
struct stu_inf* temp = NULL;
while (p != NULL)
{
num_stu++;
p = p->next;
}
p = head;
struct stu_inf** arr = (struct stu_inf**)malloc(num_stu * sizeof(struct stu_inf*));
//将链表中的数据的地址填入指针数组
for (int i = 0; p != NULL; i++)
{
arr[i] = p;
p = p->next;
}
int flag = 0;
do
{
printf("1.升序,2.降序,3.退出\n");
scanf_s("%d", &flag);
switch (flag)
{
case 1:
{
qsort(arr, num_stu, sizeof(struct stu_inf*), sort_by_score_up);
head = NULL;
relink(arr,num_stu);
printf("以下是排序后的数据:\n");
printf_file();
break;
}
case 2:
{
qsort(arr, num_stu, sizeof(struct stu_inf*), sort_by_score_down);
head = NULL;
relink(arr, num_stu);
printf("以下是排序后的数据:\n");
printf_file();
break;
}
case 3:
{
printf("退出\n");
break;
}
default:
{
printf("错误形式的输入\n");
break;
}
}
}while(flag != 3);
free(arr);//不然会内存泄漏
}
void printf_menu()
{
printf("==========================\n");
printf("**************************\n");
printf("**** 1.录入学生信息 ****\n");
printf("**** 2.查询学生信息 ****\n");
printf("**** 3.删除学生信息 ****\n");
printf("** 4.根据学生成绩排序 **\n");
printf("****** 5.退出系统 ******\n");
printf("**************************\n");
printf("==========================\n");
printf("请输入想要进行的操作\n");
}
int main()
{
load_from_file();
int flag = 0;
do
{
printf_menu();
scanf_s("%d", &flag);
clear_input();
switch (flag)
{
case 1:
{
enter_stu();
break;
}
case 2:
{
check_stu();
break;
}
case 3:
{
delete_stu();
break;
}
case 4:
{
sort_stu_score();
break;
}
case 5:
{
printf("退出简易管理系统\n");
break;
}
default:
{
printf("无效的输入,请重新输入\n");
break;
}
}
} while (flag != 5);
save_to_file();
free_line();
return 0;
}
1万+

被折叠的 条评论
为什么被折叠?



