Loading... ## 题目 实现“学生随机点名程序”的设计。 ### 功能描述 设计“学生随机点名程序”。 程序从文件(txt文本文件)中读入全体学生名单,随机抽取学生进行点名,并将点名结果更新到原文件中。显示本次点名的出勤信息;显示以往有缺勤的全部学生名单,并按照扣分多少进行排序。根据学号或者姓名,提供学生出勤情况的查询功能。 ### 具体设计要求 #### 1、编写控制台应用程序 采用功能菜单函数(menu)的方式,用户可以选择功能选项,执行相关功能。 功能菜单下图所示:  - 读入学生名单 - 随机点名 - 本次点名出勤情况 - 以往点名出勤情况 - 通过学号查询出勤情况 - 通过姓名查询出勤情况 - 退出程序。 整体设计思路: 1. 根据功能要求,提炼自定义函数,参照前面几次的综合实验,把程序的基本框架先写好(头文件---结构体定义---自定义函数声明---main函数---自定义函数实现); 2. 定义menu函数,用于显示用户可以选择的功能选项,并接收用户的选择; 3. 在main函数中,循环执行menu,并根据用户的选择,通过switch/case结构,执行相对应case下面的自定义函数。 4. 逐个实现自定义函数,实现一个函数,就运行测试,确保有无语法错误,并正确实现函数功能要求。 #### 2、读取学生名单 数据保存在一个**txt**文件中(**stus.txt**),为3个班所有学生的信息,包括姓名、学号、性别、专业、班级、扣分记录。如下图所示。  功能要求:从**stus.txt**文件中,读入学生名单。 设计思路: 1. 创建一个STUDENT学生结构体,根据文件的内容(姓名、学号、性别、专业、班级、扣分记录),确定结构体需要包含的数据成员、每个数据的类型; 2. 在main函数中,定义一个STUDENT类型的数组,用于保存从文件中读入的数据; 3. 文件读入的方式,可以参考前面几次的综合实验。 选择功能菜单的1序号,读入文件中的学生信息:  #### 3、随机点名 随机点名的流程: 在功能菜单中选择2,进入随机点名功能: 1. 程序自动从学生名单中随机抽取一个学生,显示提示语句,询问该生是否出勤。如果出勤,显示“恭喜XXX学生本次点名出勤到课”;如果没有出勤,则显示“很遗憾您没来上课,将被扣除1分”。如果该生是男生,还将多扣1分(😊,男学生们末生气,此功能仅为练习条件选择的设计)。 2. 如果该学生的出勤扣分超过6分,则显示“您的出勤扣分已经超过最高限,课程考勤不达标,将无权参加期末考试”。 3. 程序可以连续进行多位学生的随机点名。在本轮点名中,同一位学生不会被重复点名。 4. 在完成5位学生点名之后,会提示“是否继续点名”,如果选择是,将再点名5位学生;如果选择否,则结束本次点名。在结束本次点名后,会提示“是否查看本次点名的出勤分析”,如果选择是,将显示本次点名的出勤信息,包括本次点到的总人数、出勤人数、到课率、未到学生名单(在设计时,需要定义相关的变量,来记录这些信息。) 5. 需要将本次点名的结果写回到原来的文件中。 随机点名的交互如下图所示: (*图中的扣分信息仅为调试数据*) 选择功能菜单的2序号,开始点名:可以输入y或n,y表示该生出勤,n表示该生缺勤  选择y,完成第一个人的点名,系统将自动显示第二位点名的学生,可以继续输入y或n,进行点名操作。  点名5位学生之后,系统提示“是否还要继续点名”,可以选择y或n。选择y,将继续点名5位学生。选择n,点名结束,询问是否显示本轮点名的情况。  结束点名后,显示本次点名的出勤情况,如下图所示:  设计思路: 1. 确保在执行随机点名前,名单已经读入(定义STUDENT类型的数组保存名单信息); 2. 需要定义相关的变量,存储本次点名的相关信息(被点到的总人数、缺勤人数、缺勤学生的信息等) 3. 随机选取需要点名的学生,可以采用以下方法:自动生成一个随机数(该随机数的范围,应该是数组范围,确保所有学生都可以被点名),以此为下标,从数组中读取该生信息,作为被点名学生; 4. 生成的随机数需要验证是否重复了,避免被重复点名; 5. 交互过程中,接收用户输入Y或N时,验证所输入的字符是否有效; 6. 在结束本次随机点名后,需要将点名结果写回到文件中,文件的写操作,可以参考教材相关章节。 #### 4、显示本次点名出勤情况 选择功能菜单的3序号,进入“本次点名出勤情况”功能;显示本次点名未到的学生名单。  #### 5、显示以往点名出勤情况 选择功能菜单的4序号,进入“以往点名出勤情况” 按照出勤扣分,倒序显示所有有未出勤记录的学生名单。 设计思路: 1. 确保在执行本功能前,名单已经读入 2. 排序可以采用选择法或冒泡法,按照出勤扣分,倒序排序。 3. 显示的信息包括姓名、学号、出勤扣分等。  #### 6、通过学号查询出勤情况 选择功能菜单的5序号,进入“通过学号查询出勤情况”功能,显示的内容包括:以往所有的扣分情况、本次点名的出勤情况。  #### 7、通过学号查询出勤情况 选择功能菜单的6序号,进入“通过姓名查询出勤情况”功能,显示的内容包括:以往所有的扣分情况、本次点名的出勤情况。  ## 思路 在原本的思路上可以添加一些功能: #### 1.添加/删除学生 添加学生时可以把数据添加到原有数组的最后并保存到计算机内的文件内。 删除学生时通过遍历找到要删除的学生下标并把之后的所以学生向前移动一格就可以完成删除,最后在保存到计算机的文件中。 #### 2.以班级为单位查询数据 因为文件存储时不是以班级为单位存储的,所以需要先要找到每个同学匹配的班级,再进行数据的计算。 #### 3.防呆设计 例如没有读入学生名单时不能进行其他操作,没有签到时不显示本次点名情况,以及菜单函数输入错误时的错误提示. #### 4.显示优化 比如显示ta时区分单人旁和女字旁等。 ## 程序实现 ### 0、结构体设计 ```C typedef struct STUDENT{ char name[50];//姓名 char id[50];//学号 char gender;//性别 char stuClass[50];//班级名称全名 int Numclass;//班级序号(用于区分班级) int score;//分数 }STUDENT; ``` 建立一个结构体,包含姓名、学号、性别、班级名称、序号和分数。并设定别名为STUDENT。 ### 1、菜单 经过设计后,我决定设计8个选项。 设计的menu函数为int类型,返回用户的选项,具体更多操作将在main函数中进行。 ```C int menu(void){ int m; printf("-----------------学生随机签到系统-----------------\n"); printf("当前设置的抽取人数:%d人\n",choun); printf("1:读取数据\n"); printf("2:设置抽取人数\n"); printf("3:添加/删除学生\n"); printf("4:查看签到数据\n"); printf("5:查询学生数据\n"); printf("6:开始签到\n"); printf("7:查看统计数据\n"); printf("0:退出\n请选择:"); scanf("%d",&m); return m; } ``` ### 2、文件读入 因为程序中有删除和添加学生功能,所以并不适合使用动态数组分配,所以需要读入数据后返回一个学生总数值,来为以后程序中遍历次数的设定。故使用int类型,返回num为学生总数。 ```C int readData(STUDENT *a){//从文件读取数据 FILE *fp; int num=0; fp = fopen("stus.txt","r"); if(fp==NULL){ printf("文件读取失败!"); exit(0); } while(!feof(fp)&&num<10000){ fscanf(fp,"%s%s %c%s%d%d",a[num].name,a[num].id,&a[num].gender,a[num].stuClass,&a[num].Numclass,&a[num].score); num++; } num-=1;//要去除文件读取时最后一行的空行 fclose(fp); printf("成功读取%d条数据!\n",num); Isread=1;//记录为已读取数据 return num; } ``` ### 3、文件写入 写入函数时不需要像读入时的学生人数数据,故使用void类型。 ```C void writeData(STUDENT *a,int num){//写入数据 FILE *fp; fp = fopen("stus.txt","w"); int i=0; for(;i<num;i++){ fprintf(fp,"%-s\t%s\t%c\t%s\t%d\t%d\t\n",a[i].name,a[i].id,a[i].gender,a[i].stuClass,a[i].Numclass,a[i].score); } fclose(fp); printf("文件保存成功!\n"); } ``` ### 4、签到 没有特定的其他需求,所以我使用了void类型。 #### 4.1防呆 防止用户在没有读取数据的时候开始签到。 ```C if(Isread==0){ printf("没有数据!请先读取数据!\n"); return; } ``` #### 4.2抽取同学 这里需要知道的是每一次都不能抽到相同的同学,这里使用check数组,将抽到学生下标中的check数组变为1,变为1后就会继续循环,直到找到没有抽到的同学。 ```C srand(time(0)); chou=rand()%num; while(check[chou]){//确保这次抽的不是已经抽过的同学 chou=rand()%num; } ``` #### 4.3开始签到 抽好学生以后就可以进行签到。因为要存储签到信息,所以我创建了一个att二级数组,att[0]存储学生下标att[1]存储该学生是否完成签到。j记录的是目前att的下标。 ```C printf("本次抽到%s同学,请问%s出勤了吗(Y/N)\n",a[chou].name,a[chou].gender=='M'?"他":"她"); getchar(); scanf("%c",&s); if(s=='y'||s=='Y'){ printf("恭喜完成出勤!当前分数%d\n",a[chou].score); att[j][0]=chou; att[j][1]=1;//记录已出勤 j++; yes++; } else{ if(a[chou].gender=='M'){ printf("很遗憾,你没有完成出勤,并且由于你是男生,你还要被多扣一分!"); a[chou].score-=2; printf("当前分数:%d\n",a[chou].score); } else{ printf("很遗憾,你没有完成出勤!"); a[chou].score-=1; printf("当前分数:%d\n",a[chou].score); } att[j][0]=chou; att[j][1]=0;//记录未出勤 j++; no++; } check[chou]=1;//记录已经被抽到过 } ``` #### 4.4点名数据查看 通过签到时候存储的att二维数组的数据显示出勤情况。并计算出勤率。 ```C char choice; printf("点名结束,你需要查看点名数据吗?(Y/N)\n"); getchar(); scanf("%c",&choice); if(choice=='Y'||choice=='y'){ printf("以下是点名情况:\n未出勤:共%d人\n",no); for(int i=0;i<n;i++){ if(att[i][1]==0) printf("%s ",a[att[i][0]].name);//找出未出勤的人 } printf("\n已出勤:共%d人\n",yes); for(int i=0;i<n;i++){ if(att[i][1]==1) printf("%s ",a[att[i][0]].name);//找出已出勤的人 } printf("\n出勤率:%g%%\n",(double)yes/(double)n*100); } putchar('\n'); Issign=1; writeData(a,num); } ``` ### 5、查询未出勤同学 #### 5.1防呆 ```C if(Isread==0){//若没有读取数据则不允许查询 printf("没有数据!请先读取数据!\n"); return; } ``` #### 5.2找出有缺勤情况的同学 新建了unsign数组保存缺勤学生数据。 ```C int sum=0;//sum记录有缺勤记录的人数 STUDENT unsign[1000],temp;//unsign保存缺勤学生数据,temp用作排序时的临时变量 for(int i=0;i<num;i++){//找出有缺勤记录的学生 if(a[i].score<0){ unsign[sum]=a[i]; sum++; } } ``` #### 5.3冒泡排序 ```C printf("共%d人有缺勤记录,以下是缺勤名单:\n",sum); for(int i=0;i<sum;i++){ for(int j=sum-1;j>i-1;j--){ if(unsign[j].score>unsign[j+1].score){ temp=unsign[j]; unsign[j]=unsign[j+1]; unsign[j+1]=temp; } } } ``` #### 5.4输出名单 ```C for(int i=0;i<sum;i++){ printf("姓名:%s,学号:%s,班级:%s,性别:%s,分数:%d\n",unsign[i].name,unsign[i].id,unsign[i].stuClass,unsign[i].gender=='M'?"男":"女",unsign[i].score); } ``` ### 6、查询学生数据 #### 6.1防呆 ```C if(Isread==0){//若没有读取数据则不允许查询 printf("没有数据!请先读取数据!\n"); return; } ``` #### 6.2查询学生下标 ```C int j=-1;//j存储查找同学的下标 for(int i=0;i<num;i++){ if(strcmp(a[i].name,name)==0||strcmp(a[i].id,name)==0){//通过学号或姓名查询 j=i; break; } } if(j==-1){//j没改变表明不存在 printf("无法找到该同学\n"); return; } ``` #### 6.3显示学生信息 ```C int flag=0;//flag检查该同学有没有签到 printf("姓名:%s,学号:%s,班级:%s,性别:%s,分数:%d\n",a[j].name,a[j].id,a[j].stuClass,a[j].gender=='M'?"男":"女",a[j].score); if(a[j].score<-6) printf("您的出勤扣分已经超过最高限,课程考勤不达标,将无权参加期末考试\n"); if(Issign==0){//防止没有签到导致数据输出错误 printf("由于还没有签到,无法查询本次签到数据!\n"); return; } for(int i=0;i<n;i++){ if(att[i][0]==j){ printf("在本次签到中,%s同学%s\n",a[j].name,att[i][1]?"已出勤":"未出勤"); flag=1;//记录这次签到抽到了该同学 } } if(flag==0) printf("本次未抽到该同学\n"); ``` ### 7、添加、删除学生 #### 7.1添加学生 ##### 7.1.1防呆 ```C if(Isread==0){//防止没有数据导致添加学生失败 printf("没有数据!请先读取数据!\n"); return; } if(num==9999){//数组定义的是10000 printf("已达到学生上限,无法继续添加学生!\n"); return; } ``` ##### 7.1.2添加学生 ```C printf("请输入新学生姓名:"); scanf("%s",a[num].name); printf("请输入新学生学号:"); scanf("%s",a[num].id); printf("请输入新学生性别(M/F):"); getchar(); scanf("%c",&a[num].gender); printf("请输入新学生班级全称:"); scanf("%s",a[num].stuClass); printf("请输入新学生班级数字:"); scanf("%d",&a[num].Numclass); a[num].score=0; printf("学生添加成功!\n"); writeData(a,num+1); Issign=0;//如果添加完用户去查询签到的话会导致数据异常 ``` #### 7.2删除学生 ##### 7.2.1防呆 ```C if(Isread==0){ printf("没有数据!请先读取数据!\n"); return; } ``` ##### 7.2.2查找删除学生下标 ```C printf("请输入你要删除的学生姓名或学号:"); scanf("%s",name); int j=-1;//j储存查询到的同学的下标 for(int i=0;i<num;i++){ if(strcmp(a[i].name,name)==0||strcmp(a[i].id,name)==0){//匹配姓名或学号 j=i; break; } } if(j==-1){ printf("无法找到该同学\n"); return; } ``` ##### 7.2.3删除操作 ```C char temp; printf("你要删除的学生信息如下:\n姓名:%s,学号:%s,班级:%s,性别:%s,分数:%d\n确认吗?(Y/N)\n",a[j].name,a[j].id,a[j].stuClass,a[j].gender=='M'?"男":"女",a[j].score); getchar();//去除回车 scanf("%c",&temp); if(temp=='Y'||temp=='y'){ for(int i=j+1;i<num;i++){//将这个同学的后面每一个同学都向前移一格就完成了删除 a[i-1]=a[i]; } printf("删除成功!\n"); writeData(a,num-1); Issign=0; } else{ printf("已取消操作\n"); return; } ``` ### 8、统计各班级数据 #### 8.1防呆 ```C if(Isread==0){//防止没有数据导致输出异常 printf("还未读取数据,无法统计!\n"); return; } ``` #### 8.2创建新数据类型 因为要把学生以班级为单位分组,所以要新建一个数据类型,并定义一个数组和一个变量。 ```C struct data//将学生以班级分组 { int sum; double avg; int max; int min; int classNum; double person; char classname[50]; }classData[1000],temp; ``` #### 8.3开始分组 遍历所以学生并为他们找到对应的班级数组,如果找不到对应的班级那么就新建一个班级并对其进行初始化。 ```C for(int i=0;i<num;i++){ int tempclass=-1;//记录这个学生所在班级的下标 for(int j=0;j<classNum;j++){ if(classData[j].classNum==a[i].Numclass){ tempclass=j; break; } } if(tempclass==-1){//如果找不到匹配的班级,则新建一个班级 //新班级初始化 classData[classNum].classNum=a[i].Numclass; classData[classNum].sum=0; classData[classNum].person=0; classData[classNum].max=a[i].score; classData[classNum].min=a[i].score; strcpy(classData[classNum].classname,a[i].stuClass); tempclass=classNum;//新建班级,赋值这个班级下标 classNum++; if(classNum>999){//班级数组定义的是1000 printf("班级数量已经超出范围,无法统计!\n"); return; } } //求和、最大最小值 classData[tempclass].sum+=a[i].score; if(a[i].score>classData[tempclass].max) classData[tempclass].max=a[i].score; if(a[i].score<classData[tempclass].min) classData[tempclass].min=a[i].score; classData[tempclass].person++; } ``` #### 8.4求平均数并排序 ```C for(int i=0;i<classNum;i++){//求平均数 classData[i].avg=classData[i].sum/classData[i].person; } for(int i=0;i<classNum;i++){//以平均数进行冒泡排序 for(int j=classNum-1;j>i;j--){ if(classData[j].avg>classData[j-1].avg){ temp=classData[j]; classData[j]=classData[j-1]; classData[j-1]=temp; } } } ``` #### 8.5显示数据 ```C printf("各班级数据如下:\n班级\t班级人数\t平均分\t最高分\t最低分\n");//输出班级数据 for(int i=0;i<classNum;i++){ printf("%s\t%g\t%f\t%d\t%d\n",classData[i].classname,classData[i].person,classData[i].avg,classData[i].max,classData[i].min); } ``` ### 9、主函数 在这里a数组可以储存数据,但是可以更加优化,根据学生人数来决定数组大小。 ```C int main(){ STUDENT a[10000]; int att[100][2]={-1},num=0; while(1){ switch (menu()) { case 1: num=readData(a); break; case 2: printf("请输入你要设置的人数:"); scanf("%d",&choun); Issign=0;//防止用户更改人数后立刻查询在上次签到数据时,导致的数据异常 break; case 3: int temp; printf("1:添加学生 2:删除学生"); scanf("%d",&temp); if(temp==1){ addStu(a,num); num++;//总人数加一 } else{ delStu(a,num); num--;//总人数减一 } break; case 4: check(a,num); break; case 5: char name[20]; printf("请输入你要查询的同学姓名或学号:"); scanf("%s",name); checkStu(a,num,choun,name,att); break; case 6: sign(a,num,choun,att); break; case 7: tongji(a,num); break; case 0: printf("感谢使用!"); exit(0); default: printf("输入有误!\n"); break; } } return 0; } ``` ## 运行展示  菜单界面  读取完数据后会显示成功读取数据并跳回菜单,若读取失败会退出程序。  菜单中选择2则可以改变签到人数并在菜单中显示  添加学生功能,添加完后自动保存到txt文件中   删除学生测试,在txt文件中同步删除  查看签到数据时可以显示有缺勤记录的学生人数并升序输出名单  签到时使用y和n表示有无出勤,最后可以选择是否显示签到数据  使用查询学生功能时可以显示学生信息以及本次签到出勤情况    若还没有开始签到则会弹出提示无法查询本次数据  查看统计数据功能可以查看以班级为单位的统计数据,以平均分降序排序  输入0即可退出程序  同时,本程序还设计了一些防错机制,防止用户输入错误以及逻辑错误。   最后修改:2025 年 06 月 02 日 © 允许规范转载 赞 不用打赏哦!