/**************************************************************
要求:
1、管理学生的信息,包括,学号,姓名,各科成绩
2、将信息保存成文件,能反复使用
3、对信息操作,包括,添加学生信息,修改,删除,
按学号、姓名查询学生信息
4、能进行科目成绩排序,科目分数段查询
附data文件实例:
001 小黑 语文 60.00 英语 70.00 数学 90.00
002 小天 语文 70.00 英语 90.00 数学 80.00
003 小芳 语文 80.00 英语 80.00 数学 70.00
004 小鱼 语文 90.00 英语 70.00 数学 100.00
***************************************************************/
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
//链表结点结构体声明
typedef struct subjects
{
char name[20];
float score;
}sub;
typedef struct student
{
int num;
char name[20];
sub subject[3];
struct student* next;
}stu,*pstu;
#define SIZE sizeof(stu)
//函数申明
pstu LoadInfo();
void PrintMenu();
pstu AddStu(pstu );
pstu DeleStu(pstu );
pstu RwrStu(pstu );
void FindStu(pstu , char );
void Count(pstu ,char * ,float ,float );
void Rank(pstu ,char * );
void SaveQuit(pstu );
//主函数
int main()
{
float score1,score2;
char n,j;
char subname[20];
pstu head,ptr;
head = LoadInfo();
ptr = head->next;
//创建菜单,进入选择循环
while(1)
{
PrintMenu();
printf("请输入您的选择编号:");
scanf("%d",&n);
getchar(); //接收上次输入选择后的回车输入,不致影响下一次输入
switch(n)
{
case 1:
{
//信息管理
system("cls"); //清屏
j=0;
while(4!=j) //循环进入子菜单选择
{
printf("欢迎进入信息管理版块!\n\n");
printf("\025 1、添加学生\n");
printf("\025 2、删除学生\n");
printf("\025 3、修改学生信息\n");
printf("\025 4、返回\n");
printf("请输入您的选择编号:\n");
scanf("%d",&j);
getchar();
if ( 1 == j) head = AddStu(head); //添加学生
else if( 2 == j) head = DeleStu(head); //删除学生
else if( 3 == j) head = RwrStu(head); //修改重写学生信息
else if( 4 == j) ;
else printf("输入有误,请重新输入!\n");
}
printf("请输入回车键返回主菜单!"); //此处本意按任意键返回,但是任意键的话,需要按键A,再按回车确定
getchar(); //则会连续收到两个按键,造成错误读入,可以改进scanf接收字符串,
system("cls"); //以下所有getchar()、system("cls")同理
break;
}
case 2:
{
//信息查询
system("cls");
printf("欢迎进入信息查询版块!\n");
printf("请输入要查询的学生编号:");
scanf("%d",&j);
getchar();
//printf("%d\n",j); //检测输入是否成功,调试程序用
FindStu(head,j); //查询并输出
printf("\n请输入回车键返回主菜单!");
getchar();
system("cls");
break;
}
case 3:
{
//成绩统计
system("cls");
printf("欢迎进入成绩统计版块!\n");
printf("请输入科目:");
scanf("%s",&subname);
getchar();
printf("请输入分数范围(score1,score2):");
scanf("%f,%f",&score1,&score2);
getchar();
/*printf("%s %5.2f %5.2f\n",subname,
score1,score2); */ //检测输入是否成功,调试程序用
Count(head,subname,score1,score2); //统计并输出
printf("请输入回车键返回主菜单!");
getchar();
system("cls");
break;
}
case 4:
{
//成绩排序
system("cls");
printf("欢迎进入成绩排序版块,请输入科目:");
scanf("%s",&subname);
getchar();
Rank(head,subname); //排序并输出
printf("\n请输入回车键返回主菜单!\n");
getchar();
system("cls");
break;
}
case 5:
{
//保存退出
SaveQuit(head); //文件操作,保存并退出
free(head);
return 0;
}
default:
{
printf("输入有误,按回车键重新选择!\n"); //主菜单错误输出检测
getchar();
system("cls");
}
}
}
}
//加载data数据 ,文件操作
pstu LoadInfo()
{
int num;
char name[20];
char sub1[20];
char sub2[20];
char sub3[20];
float score1;
float score2;
float score3;
char filename[] = "D:\\编程学习\\编程实践\\c语言课程设计1 学生信息管理\\data.txt"; //文件名,此处为简化编程,采用固定地址名称,未作输入
FILE *fp;
pstu head,ptr;
//创建带表头结点的空单链表head,用来存放载入信息
head = (pstu)malloc(SIZE);
ptr = head;
ptr->next = NULL;
//加载data文件,存入head链表
if( NULL == (fp = fopen(filename,"r")) ) //判断文件是否存在及可读
{
printf("error!");
exit(0);
}
while (!feof(fp))
{
fscanf(fp,"%d %s %s %f %s %f %s %f\n",&num,&name,
&sub1,&score1,&sub2,&score2,&sub3,&score3); //读取一行,采用格式化读取,避免了其他各种读取方法的数据处理问题
//该方法缺点明显,对数据格式要求教研,故data文件规定数据格式
ptr->next = (pstu)malloc(SIZE);
ptr = ptr->next;
ptr->next = NULL;
ptr->num = num;
strcpy(ptr->name,name);
strcpy(ptr->subject[0].name,sub1);
ptr->subject[0].score = score1;
strcpy(ptr->subject[1].name,sub2);
ptr->subject[1].score = score2;
strcpy(ptr->subject[2].name,sub3);
ptr->subject[2].score = score3;
}
fclose(fp); //关闭文件,已得到保存data信息的链表head
return head;
}
//打印主菜单
void PrintMenu()
{
printf("***************************************\n");
printf(" 枫枫学生信息管理系统 \n");
printf("***************************************\n");
putchar('\n');
printf("菜单\n");
printf("\025 1、信息管理\n");
printf("\025 2、信息查询\n");
printf("\025 3、成绩统计\n");
printf("\025 4、成绩排序\n");
printf("\025 5、保存退出\n");
}
//添加学生
pstu AddStu(pstu x)
{
char namestu[20];
char *p;
char subname1[20],subname2[20],subname3[20];
pstu head,ptr;
head = x;
ptr = head;
while( NULL != ptr->next ) //遍历链表,找到链尾结点
{
ptr = ptr->next;
}
ptr->next = (pstu)malloc(SIZE); //默认在链表末追加添加信息
ptr = ptr->next;
ptr->next = NULL;
printf("请输入添加学生的信息:\n");
printf("请输入添加学生的学号:");
scanf("%d",&ptr->num);
getchar();
printf("请输入添加学生的姓名:");
scanf("%s",namestu);
getchar();
p = namestu;
strcpy(ptr->name,p);
printf("请输入添加学生的科目1名称:");
scanf("%s",&subname1);
getchar();
p = subname1;
strcpy(ptr->subject[0].name,p);
printf("请输入添加学生的科目1成绩:");
scanf("%f",&ptr->subject[0].score);
getchar();
printf("请输入添加学生的科目2名称:");
scanf("%s",&subname2);
getchar();
p = subname2;
strcpy(ptr->subject[1].name,p);
printf("请输入添加学生的科目2成绩:");
scanf("%f",&ptr->subject[1].score);
getchar();
printf("请输入添加学生的科目3名称:");
scanf("%s",&subname3);
getchar();
p = subname3;
strcpy(ptr->subject[2].name,p);
printf("请输入添加学生的科目3成绩:");
scanf("%f",&ptr->subject[2].score);
getchar();
putchar('\n');
return head;
}
//删除学生
pstu DeleStu(pstu x)
{
int num;
pstu head,ptr,qtr;
head = x;
ptr = head->next;
qtr = head;
printf("请输入要删除的学生的学号:");
scanf("%d",&num);
getchar();
while(ptr!=NULL)
{
if( ptr->num != num) //遍历查找链表结点,未找到跳过该结点
{
ptr = ptr->next;
qtr = qtr->next;
}
else //找到则删除结点
{
ptr = ptr->next;
qtr->next = ptr;
break;
}
}
printf("该学生信息已删除!\n\n");
return head;
}
//修改学生信息
pstu RwrStu(pstu x)
{
char namestu[20];
char *p;
char subname1[20],subname2[20],subname3[20];
int num;
pstu head,ptr;
head = x;
ptr = head->next;
printf("请输入要修改的学生的学号:");
scanf("%d",&num);
getchar();
while(ptr!=NULL)
{
if( ptr->num == num )
{
printf("已找到该学生信息,请填入修改项目:");
printf("请输入修改学生的姓名:");
scanf("%s",namestu);
getchar();
p = namestu;
strcpy(ptr->name,p);
printf("请输入修改学生的科目1名称:");
scanf("%s",subname1);
getchar();
p = subname1;
strcpy(ptr->subject[0].name,p);
printf("请输入修改学生的科目1成绩:");
scanf("%f",&ptr->subject[0].score);
getchar();
printf("请输入修改学生的科目2名称:");
scanf("%s",subname2);
getchar();
p = subname2;
strcpy(ptr->subject[1].name,p);
printf("请输入修改学生的科目2成绩:");
scanf("%f",&ptr->subject[1].score);
getchar();
printf("请输入修改学生的科目3名称:");
scanf("%s",subname3);
getchar();
p = subname3;
strcpy(ptr->subject[2].name,p);
printf("请输入修改学生的科目3成绩:");
scanf("%f",&ptr->subject[2].score);
getchar();
printf("该学生信息已修改!\n\n");
break;
}
else
{
ptr = ptr->next;
}
}
return head;
}
//查找学生,参数为链表指针,和学生学号
//不好,应该将学号输入放进子函数,简化主函数结构,减少子函数参数
void FindStu(pstu x,char y)
{
pstu head,ptr;
head = x;
ptr = head->next;
while( ptr != NULL)
{
if( ptr->num == (int)y) //因主函数中为节省空间,学号输入采用char数据,故强行准换
{
printf("已找到该学生信息!\n如下:");
printf("%03d %s %s %5.2f %s %5.2f %s %5.2f\n",
ptr->num,ptr->name,ptr->subject[0].name,ptr->subject[0].score,ptr->subject[1].name,ptr->subject[1].score,ptr->subject[2].name,ptr->subject[2].score); break; //注意此处找到并输出信息后要手动退出循环
}
else
{
ptr = ptr->next;
}
}
if( ptr == NULL ) //查询成功检测,while循环中若找到,则ptr停留在当前学生的结点上
{
printf("未能找到该学生信息!\n");
}
}
//统计科目分数区间段的学生,参数为链表指针,科目名称,分数区间上下限
//同理,参数的录入应放入子函数,简化结构和编程
void Count(pstu x,char *y,float q,float p)
{
pstu head,ptr;
char name[20];
char flag=0; //手动设置的查找结果flag
head = x;
ptr = head->next;
strcpy(name,y);
//printf("%s %5.2f %5.2f\n",name,q,p); //检测输入参数的传递,调试程序用
while( ptr != NULL) //开始查找统计,科目查找用strcmp函数比较科目字符串,返回值0为字符串相等
{ //此处while循环体中,重复的查找步骤太多,应设置科目匹配flag,参照rank()函数
if( strcmp(name,ptr->subject[0].name) == 0 ) //通过flag将科目确认放在while之外,循环体内只做分数区间的扫描和输出
{
if( q <= ptr->subject[0].score && ptr->subject[0].score<= p )
{
printf("%03d %s %s %5.2f\n",ptr->num,ptr->name,ptr->subject[0].name,ptr->subject[0].score);
flag++;
}
}
if( strcmp(name,ptr->subject[1].name) == 0 )
{
if( q <= ptr->subject[1].score && ptr->subject[1].score<= p )
{
printf("%03d %s %s %5.2f\n",ptr->num,ptr->name,ptr->subject[1].name,ptr->subject[1].score);
flag++;
}
}
if( strcmp(name,ptr->subject[2].name) == 0 )
{
if( q <= ptr->subject[2].score && ptr->subject[2].score<= p )
{
printf("%03d %s %s %5.2f\n",ptr->num,ptr->name,ptr->subject[2].name,ptr->subject[2].score);
flag++;
}
}
ptr = ptr->next;
}
if(flag==0)
{
printf("未能找到该课程该区间分数段的学生!\n");
}
}
//学科成绩排名,采用交换数据的方法,参数为链表指针,科目名称
//同理参数问题
//链表排序问题,此处用交换结点数据方法,还有其他多种排序方法
//如,交换结点,辅助指针数组排序(未实现,过程繁杂),插入法排序等
void Rank(pstu x,char *y)
{
pstu head,ptr,qtr;
char name[20];
char len=0;
char flag=0; //简化算法,设置科目查找结果判断值,flag=0表示科目输入为未知科目,不存在
int i=0; //i、j循环次数控制参数
int j=0;
char temp_name[20]; //数据交换时的暂存信息变量
float temp0,temp1,temp2;
int temp_num;
strcpy(name,y);
head = x;
ptr = head->next;
while( ptr != NULL) //测链表长度,不包括表头结点
{
ptr = ptr->next;
len++;
}
ptr = head->next; //指针ptr用过之后记得回原位
//开始查找科目
if( strcmp(name,ptr->subject[0].name) == 0) flag=1;
if( strcmp(name,ptr->subject[1].name) == 0) flag=2;
if( strcmp(name,ptr->subject[2].name) == 0) flag=3;
if( flag == 0)
{
printf("未找到该科目!");
return;
}
//开始排序,冒泡法比较各结点数据
//此处3个并列的if用switch case更清晰结构
if( n == 1 )
{
for( i=0;i<len;i++)
{
ptr = head->next->next; //每一次内循环之后,ptr、qtr必然在最后两个节点上
qtr = head->next; //故在进行内循环之前,要重新复位ptr、qtr
for( j=0;j<len-i-1;j++)
{
if( qtr->subject[0].score < ptr->subject[0].score )
{
temp_num = qtr->num; //交换数据,因数据格式(科目顺序)明确规定,故不再做科目名称的替换
strcpy(temp_name,qtr->name);
temp0 = qtr->subject[0].score;
temp1 = qtr->subject[1].score;
temp2 = qtr->subject[2].score;
qtr->num = ptr->num;
strcpy(qtr->name,ptr->name);
qtr->subject[0].score = ptr->subject[0].score;
qtr->subject[1].score = ptr->subject[1].score;
qtr->subject[2].score = ptr->subject[2].score;
ptr->num = temp_num;
strcpy(ptr->name,temp_name);
ptr->subject[0].score = temp0;
ptr->subject[1].score = temp1;
ptr->subject[2].score = temp2;
}
qtr = qtr->next;
ptr = ptr->next;
}
}
}
if( n == 2 )
{
for( i=0;i<len;i++)
{
ptr = head->next->next;
qtr = head->next;
for( j=0;j<len-i-1;j++)
{
if( qtr->subject[1].score < ptr->subject[1].score )
{
temp_num = qtr->num;
strcpy(temp_name,qtr->name);
temp0 = qtr->subject[0].score;
temp1 = qtr->subject[1].score;
temp2 = qtr->subject[2].score;
qtr->num = ptr->num;
strcpy(qtr->name,ptr->name);
qtr->subject[0].score = ptr->subject[0].score;
qtr->subject[1].score = ptr->subject[1].score;
qtr->subject[2].score = ptr->subject[2].score;
ptr->num = temp_num;
strcpy(ptr->name,temp_name);
ptr->subject[0].score = temp0;
ptr->subject[1].score = temp1;
ptr->subject[2].score = temp2;
}
qtr = qtr->next;
ptr = ptr->next;
}
}
}
if( n == 3 )
{
for( i=0;i<len;i++)
{
ptr = head->next->next;
qtr = head->next;
for( j=0;j<len-i-1;j++)
{
if( qtr->subject[2].score < ptr->subject[2].score )
{
temp_num = qtr->num;
strcpy(temp_name,qtr->name);
temp0 = qtr->subject[0].score;
temp1 = qtr->subject[1].score;
temp2 = qtr->subject[2].score;
qtr->num = ptr->num;
strcpy(qtr->name,ptr->name);
qtr->subject[0].score = ptr->subject[0].score;
qtr->subject[1].score = ptr->subject[1].score;
qtr->subject[2].score = ptr->subject[2].score;
ptr->num = temp_num;
strcpy(ptr->name,temp_name);
ptr->subject[0].score = temp0;
ptr->subject[1].score = temp1;
ptr->subject[2].score = temp2;
}
qtr = qtr->next;
ptr = ptr->next;
}
}
}
//输出排序过后的链表
ptr = head->next;
while( ptr != NULL )
{
printf("%03d %s %s %5.2f %s %5.2f %s %5.2f\n",
ptr->num,ptr->name,ptr->subject[0].name,ptr->subject[0].score,
ptr->subject[1].name,ptr->subject[1].score,
ptr->subject[2].name,ptr->subject[2].score);
ptr = ptr->next;
}
}
//保存文件并退出,文件操作
void SaveQuit(pstu x)
{
pstu head,ptr;
FILE *fp;
char filename[] = "D:\\编程学习\\编程实践\\c语言课程设计1 学生信息管理\\data.txt";
head = x;
ptr = head->next;
if( NULL == (fp = fopen(filename,"w")) ) //判断文件是否存在及可读
{
printf("error!");
exit(0);
}
while(ptr != NULL) //遍历链表结点,按data约定格式输出数据
{
fprintf(fp,"%03d %s %s %5.2f %s %5.2f %s %5.2f\r",
ptr->num,ptr->name,ptr->subject[0].name,ptr->subject[0].score,
ptr->subject[1].name,ptr->subject[1].score,
ptr->subject[2].name,ptr->subject[2].score);
ptr = ptr->next;
}
fclose(fp);
}
小结:
断断续续快700行终于完成了,其实编码部分对新手而言不算慢,但是调试修改过程及其烦乱漫长,不过毕竟算是大一大二的内容,现在做有点不知道说啥了都。各模块的任务结构比较简单,也觉得本来应该是个很简单的系统,但是在编码过程中,遇到了很多问题,问题主要包括两类,一、思路不清晰,眼光不够高屋建瓴,原因是经验不够丰富。二、函数实现不够熟悉,很多时候在绕弯路,也有很多时候函数使用出错,语法理解不深。现从各小节细点逐步分析。
1、
typedef struct subjects
{
char name[20];
float score;
}sub;
typedef struct student
{
int num;
char name[20];
sub subject[3];
struct student* next;
}stu,*pstu;
该格式的声明定义结构体是通过网络学到的,确实很好用,也算书本的实践运用
2、整个系统的主要流程:
start -> 文件操作:读取data - 新建linkedlist保存数据 -> 菜单循环,进入子模块
-> 信息处理:添删改,因为是对数据的物理操作,子函数有返回值,返回处理后的链表
-> 信息检索:查找、排序等,不改变源数据,故子函数void
-> 保存退出:文件操作
此处重大失策是各子函数的参数较多,因为我选择了在主函数中读入参数数据,再传入子函数,造成主函数臃肿繁琐,而且子函数参数增多,传递关系复杂,这一点真的是经验不足,应该让主函数尽可能简单,所以实现放入子函数中。
3、调试经验匮乏,遇到bug之后的排查能力弱,是效率低下的主要原因,虽然采用笨办法,在程序各执行各阶段插入调试检测步骤,但是不能根源上发现bug原因。
还有就是网络学习的重要性,一个是搜索,函数用法、实例,学习变为己用。二是具体问题的论坛讨论。
4、输入输出检测,因为缓冲的存在,造成不能精确读入。目前的理解是,输入时,完成预定输入后,需要追加输入一个回车,将之前放在输入缓冲中的数据输入到内存,同时计算机也会多记录那一次回车的输入,故采用每次scanf接收输入之后,用getchar接收屏蔽掉后缀的回车键。实例:谭浩强《c程序设计》p338例10.3。
5、为简化编程,系统排错能力较弱,只考虑模拟了主要可能发生情况
6、数组名即首元素地址。故 char p[20]; scanf("%s",&p); 此处 &p = p = &p[0],可不写&符。
7、经验:在大量编码中,遇到问题,重新建立一个工程,设置简单情景,只单独验证这一个问题,不然在大背景下很难排查问题。
8、文件读取问题,fopen参数的理解,feof在读取中的机制,尚未透彻理解,二进制文件与文本文件的差别至今不详。
数据读取的做法:
法一:(最初设想)按行读取fgets保存到str,(fgets的机制,按行读取,读到换行或EOF结束,做键盘输入读取时同gets用法,但是比gets安全,因为fgets指定长度,不像gets会溢出),再通过strtok函数分割str成各个子串数据,再将字串通过atoi、atof等转换成需要的类型,过程何其麻烦,而且strtok机制颇为诡异。最终整体调试阶段出现了问题,放弃,换方法删改。
法二:想到用fgetc一个一个接收字符,在处理,觉得工程量太繁杂,放弃。
法三:反复看书,找到了fscanf函数,问题迎刃而解,只怪之前看书不仔细,这里磨蹭这么久。
9、char a[20]; char b[20] = "xxxxxxx";
a = b错误,不能直接赋值,a、b此时都表示地址,应用字符串函数strcpy(a,b); 程序中void AddStu() 、void RwrStu两个子函数中有大量该库函数的繁琐用法如:
char *p;char a[20]; char b[20] = "xxxxxxx"; p = b; strcpy(a,p);
10、程序中辅助指针的位置每次用完之后都回到链表首位,不至于游离,再次使用的时候也清晰。但程序中内存的管理较差,malloc之后的要free释放,链表的删除应该是对每一个节点的free,而不只是头指针head,且free内存之后,注意当前定义过得指针出现野指针的情况。
11、程序中使用一些flag标志,能减少代码量,简化重复的算法。
12、字符串相等判断函数strcmp()的使用,如char a[20] = "1"; char b[30] = "1" ; strcmp(a,b)结果为0表示字符串相等,可见strcmp对比的是两个字符串中strlen范围内的字符,而与存储空间长度sizeof无关。
有趣的实验:char str[20] = "1\0abc";char stb[30] = "1";
printf("%d %d %d %d %d\n",sizeof(str),sizeof(stb),strlen(str),strlen(stb),strcmp(str,stb));
结果为:20 30 1 1 0 即strcmp比较时,对比各字符串,检测到字符串结束标志\0就不管后面的内容了。
13、链表排序问题,此处也花费我大量时间。
法一:(原先预想) 对比每个链表结点,然后交换两个结点位置,以冒泡法排序,这是最直观的方法,做了设想,并没有去编码,觉得有些复杂,因为要交换两个结点至少要4个指针。a、b交换,同时还要用到a的前一个结点,和b的下一个结点即b->next指针。于是就没有急于编码,而是上网求比较精妙的思想。
法二:(未能完成)在贴吧,我看到了另一种思维,将每一个结点的地址保存到一个指针数组中,通过该数组元素对应的结点数据对比,排列该数组,最后按照该数组,重新链接链表,实现了以空间换时间的的思想。其实仔细想想没有多简化算法,也没有多省事。按此方法编码,最终因为指针应用问题出现错误,论坛讨论说到这样排序链表尾结点会随机出现在某一个指针数组元素中,所以按数组重新链接链表的时候会中间就NULL中断,就会出现访问NULL->next这样的情况,会出现错误。我同意,但是没有去实验,就放弃,转而其他思路了。
法三:(最终方案)我采取了和交换结点异曲同工的方法,交换结点数据,因为结点元素少的情况下,交换结点元素内容要比交换结点简单,但是元素多的情况下不如法一,实现代码会比法一多,但是思维简单,不牵扯到链表的换位。
法四:(其他方案)以上方案都是基于冒泡法,所以效率较低,思路简单。其他方法还有选择排序,插入排序。
14、文件写入,根据fscanf要求相应用fprintf写入。
好吧, 这个总结真啰嗦,但愿一直有这精力。
加油,数据结构与算法。