前言
大概每个学习C语言的学生都会遇到这样一个作业吧:学生信息管理,我们老师布置的作业要求如下:
1. 定义结构体类型,存储每个同学的信息:
学号、姓名、性别、三门课程成绩(可以自由扩充,出生日期、电话、学号等等)
2. 功能要求:
- 显示所有同学的信息
- 查找指定同学的信息 (可扩充其他信息的查找)
- 修改指定同学的信息
- 显示有不及格同学的信息 (统计显示其他信息)
- 按指定课程排序输出
- 增加一个新同学
- 删除一个同学
- (还可以根据自己能力扩充功能,如文件存储读/写)
这个作业可以算是比较具有挑战性的、考验学生综合能力的,比较花时间,而且不仅需要做好应有的功能,也要注重操作界面的可视性(交互),令使用者易于上手。
怎么实现信息管理每个人都会不太一样,比如有人会使用链表+动态内存来存储数据,而我就只会用结构体数组(通过改前缀+排序实现删除功能)。
因此,这里仅仅是我个人的思路,而且,同样的功能我的实现方法可能会较为复杂,这里建议读者阅读自己需要的部分,或者撷取设计灵感应用到自己的程序里面。
功能与特色
本程序受到Excel存储学生信息表格的启发,决定模仿表格处理工具的使用逻辑进行程序的设计,即以行形式展示学生信息,使用学生的“当前编号”来对其执行操作。(这个当前编号其实就是在结构体数组中的位置…)
下面说说本程序的特色吧:
- 支持文件读入与写出,同时以一种用户友好的方式存储,即模仿ini配置文件的存储方式,采用“键名 = 键值”的方式存储,这样方便用户读取和修改文本文件中的内容,并且具有一定的抗扰乱能力(比如随便加一些什么条目在文件里也不会影响读取)。
- 支持课程条目的重命名和删除,并且能够保存下来,下次启动程序仍能够使用;
- 在本人使用的编译器(Dev-C++ 5.9)下,能够实现中文的读取与存储;
- 支持根据学号或姓名的模糊查找,并返回信息列表,方便使用者搜索;
其中特性1 之所以采取上述的方式是因为完成作业期间了解到程序的“反射”——不太清楚是否正确使用了该术语,其实想说的就是“程序能够根据外界条件的变化修改自身运行参数”。
缺点
本程序的文件输出(即保存功能)是覆盖式的,也就是说,程序读取一次并保存后,无关的内容就会消失。
例如,删除了一个课程条目后,内存中加载的数据也会被相应删除(受限于学生信息的数据存储模式),但是文件此时还没有更改,所以,只要不执行保存操作,再把课程条目添加回来,重新载入文件,成绩还会存在;
另外,因为关于课程条目的设置是及时保存的,所以
- 重启程序后执行读取操作,也不会出现已删除课程条目相关的数据(读取是对照内存中的课程条目数量来的,这个会在程序启动时最先加载),除非把课程条目添加回来,重新载入文件。
- 如果重命名了课程名目,而没有手动保存文件,那么重启程序后再加载,将无法读取到该课程的数据
另外,文件的编码格式似乎有一定要求,需要ANSI编码才行。
如果有兴趣运行这段程序的你发现了其他bug,也欢迎交谈!
简介
主要功能函数简要介绍
本程序源代码有超过30个函数,下面介绍其中的一些函数:
void trim(char* strIn, char* strOut);
//用于修剪字符串,删除前后的换行、空格
int GetInt(const char *tgtName, int entriesNum, int nDefault = 0);
//用于在指定的区域内,寻找tgtName对应的整型键值并返回。
int load_stuData(const char *libFile = "stulib.txt")
//从文件中加载学生数据;
void save_stuData(const char *libFile = "stulib .txt")
//保存学生数据到文件;
void edit_courses_tag()
//编辑课程名称,即增加、删除、重命名;
void load_settings(const char* cfgFileName = "settings.ini")
//加载配置文件,包含课程数目,课程名称;
int getline(char* dstStr)
//相当于gets函数的功能,用来读入一整行的输入数据;
//本程序所有键盘输入均由getline()读入,主要用于规避scanf()的不便之处,
//以及获取中间带有空格的学生姓名或其他数据。
int add_student()
void del_student(int i)
//添加和删除学生;
int search_student(const char* keywords, int results_index[])
int search_score(int tgt_score,int results_index[], int mode = 0)
void search_by(int *results)
//以上三个函数搭配,用来根据关键字查找学生,或查找不及格学生;
void edit_info(int i)
void edit_scores(int i)
void quick_scoring() //快速录入学生分数
void edit_student_info()
//以上是编辑学生信息的模块;
数据结构介绍
存储学生信息使用结构体数组,下面是结构体的定义;
struct stuInfo
{
int identify_num;
char Name[64];
char StuID[16];
char Gender[8];
char TelNum[16];
int scores[16];
int tot_scores;
int StuID_int;
};
stuInfo stuData[200];
identify_num
是一个最初想着用来存储分配给学生的唯一识别号的东西,想着在读txt文件时,遇到多个相同识别码学生信息,会以最后读取的为准(不过后来觉得好像没啥用…),所以这方面就没怎么用到了;
但是删除学生的操作后会把这个改成‘99999’,同时对整个数组根据这个号码排序,达到删除的效果;
而且,写入文件时,会重新编号,也就没有唯一识别性了。
预置的课程名称,用char二维数组来存储:
int COURSES_NUM = 3;
char courses_tag[16][16]={"Chn","Math","Eng"};
初始的课程是3门,分别为"Chn"
,"Math"
,"Eng"
;
文本文件(数据库)书写样例
对于存储学生数据的students.txt
,语法大概如下所示:
整数采用整数形式书写,如Math = 99;
,
其他内容采用用双引号括起来的字符串形式,如Name = "Henry William";
。
[00001]
ID = "190103";
Name = "张超";
Gender = "M";
Tel = "0394-6123456";
Chn = 99;
Math = 88;
Eng = 97;
CPP = 99;
Art = 67;
German = 99;
[00007]
ID = "190138";
Name = "Mia Miller";
Gender = "F";
Tel = "13701234567";
Chn = 88;
Math = 88;
Eng = 88;
CPP = 89;
Art = 88;
German = 80;
目前的配置文件settings.ini
如下所示:
[courses]
COURSES_NUM = 5 ;
0 = "Chn";
1 = "Math";
2 = "Eng";
3 = "CPP";
4 = "Art";
至于文件为什么写成这样,大家可以移步查阅有关ini配置文件的资料…这样,看文件读写部分的代码可能会更好理解。
代码实现
代码里可能会多次遇到这个while
语句,它是用来遍历存放学生数据的stuData
结构体数组的,用while
貌似比用for
简单一些哈哈;
while (stuData[k].identify_num!=DEL_IDENTIFY_NUM &&
stuData[k].identify_num != 0)
意为,只要没遇到空的或者删除掉的学生信息,就一直进行下去。
一些简单的函数
计算总分
void cal_tot_scores()
{
int k=0;
while (stuData[k].identify_num!=DEL_IDENTIFY_NUM && stuData[k].identify_num != 0)
{
stuData[k].tot_scores = 0;
for (int i=0;i<COURSES_NUM;i++)
{
stuData[k].tot_scores += stuData[k].scores[i];
}
k++;
}
}
显示当前内存中存储的课程名以及课程序号
void show_courses_name()
{
printf("# Courses identify No.: \n");
for (int i=0;i<COURSES_NUM;i++)
{
printf(" %2d. %s\n",i+1,courses_tag[i]);
}
printf("\n");
return ;
}
输出行标题
void print_title()
{
//先输出前边的部分,即学号、姓名等主要信息
printf("%-4s %-8s %-20s %-4s %-16s ","No.","Stu. ID","Name","Sex","Tel.");
//输出课程名目
for(int i=0;i<COURSES_NUM;i++)
{
//只输出课程名的前三个字母
for (int j=0;j<3;j++) printf("%c",courses_tag[i][j]);
printf(" ");
}
//还有一个总分
printf ("%4s\n","Tot");
//还要打出一行分割线
for (int i=0;i<4+8+20+4+16+5+COURSES_NUM*4+4;i++) printf("-");
printf("\n");
}
显示单个同学的信息
这个是一个基础的函数,很多时候都会调用它;
这个函数会根据参数i
,即结构体数组的下标,输出那个学生的信息;
输出的宽度和用来输出标题的函数保持一致。
因为结构体数组从0
开始存储,所以序号输出使用i+1
;同理,用户输入的是其看到的序号,所以,在后来的函数里,会出现“用户输入-1”的情况;
void view_student(int i)
{
printf("%3d. %-8s %-20s %-4s %-16s ",i+1,
stuData[i].StuID, stuData[i].Name, stuData[i].Gender,stuData[i].TelNum);
int tot_scores=0;
for (int j=0;j<COURSES_NUM;j++)
{
printf("%3d ",stuData[i].scores[j]);
tot_scores+=stuData[i].scores[j];
}
printf("%4d ",tot_scores);
printf("\n");
}
文件读入写出相关
KeyInfo 结构体
这个是用来存储键值的一个结构体;
声明了一个Keys
数组,对每一个Section(或者说一个学生)使用,类似一个缓冲区的感觉。
struct KeyInfo
{
char KeySection[100];
char KeyName[128];
//int KeyValue_int;
char KeyValue[128];
};
KeyInfo Keys[30]; //kind of a buffer
str2num函数:获取字符串中的整型数据
这个函数是作者自己写的,其实读者可以调用stdlib.h
中的atoi
,atof
等函数,使用起来也很方便;使用方法请自行查找,这里暂不解释。
这个手写函数的好处是,会返回结束查找处的位置,也就是说可以对一个字符数组连续使用;
char* str2num(char* a, int *num) //start point, return num;
{
int t=0,tmp[10],p=1,if_minus=0;
char *k=a;
while (*k<'0'||*k>'9')
{
if(*k=='\0') return k;
k++;
}
if(*(k-1)=='-') if_minus=1;
while (*k>='0' && *k<='9') tmp[t++]=*(k++)-'0';
t=t-1;
*num=0;
while(t>=0)
{
*num+=tmp[t]*p;
t--;
p*=10;
}
if(if_minus==1) *num=-*num;
return k; //return final point
}
int2str 函数:将一个整数转换到char型数组
这个函数也是手写的,因为作者比较懒就没有上网搜模板函数…
void int2str(int a, char *begins)
{
if (a<0) *begins++ = '-';
char *p = begins, t;
if (a==0)
{
*p++ = '0';
*p = '\0';
return;
}
while (a>0)
{
*p++ = a%10 + '0';
a /= 10;
}
*p--='\0'; // 添加结束标识符,并且把指针退一格,为倒置做准备
//将数组倒序过来
while ( begins < p )
{
t=*p;
*p = *begins;
*begins = t;
p--;
begins++;
}
}
trim函数:修剪字符串
这个函数很多地方都会用到,不管是针对用户的键盘输入,还是读取到的字符串。
void trim(char* strIn, char* strOut) // support in-place opreation
{
char *a=strIn, *b;
while (*a == ' '||*a == '\n' ) a++; // ignore spaces at the beginning
b = strIn + strlen(strIn) - 1; // get pointer pointing at the end of the line
while (*b == ' '||*b == '\n' ) b--; // ignore spaces at the end
while (a<=b) *strOut++ = *a++; // transplace
*strOut='\0';
}
GetInt,GetStr函数:按照键名搜索,返回相应键值
这里函数直接会在上面提到的Keys
数组里面搜索;参数entriesNum
用来限定搜索的范围(其实没必要,不过懒得改了);tgtName
顾名思义是目标的键名。
这两个函数其实算是相当魔改了…之后还有两个函数相对正常一点点。
int GetInt(const char *tgtName,int entriesNum, int nDefault = 0)
{
for (int i=0; i<entriesNum; i++)
{
if(strcmp(tgtName,Keys[i].KeyName)==0)
str2num(Keys[i].KeyValue, &nDefault);
}
return nDefault;
}
void GetStr(const char *tgtName, int entriesNum, char* dstStr, const char* nDefault = " ")
{
for (int i=0; i<entriesNum; i++)
{
if(strcmp(tgtName,Keys[i].KeyName)==0)
{
char* tgt= Keys[i].KeyValue + 1;
while(*tgt!='"') *dstStr++=*tgt++;
*dstStr ='\0';
return;
}
}
strcpy(dstStr, nDefault);
}
GetInt_1()
,GetStr_1()
这两个函数增加了限定Section(扇区名)的搜索。
同时,这两个函数取消了限定搜索范围的参数,直接用while
来迭代;
int GetInt_1(const char* tgtSection, const char* tgtName, int nDefault = 0)
{
int i=0;
while(Keys[i].KeyName[0]!='\0')
{
if(strcmp(tgtSection,Keys[i].KeySection)==0)
{
if(strcmp(tgtName,Keys[i].KeyName)==0)
{
if(Keys[i].KeyValue[0] == '\0' || Keys[i].KeyValue[0] == '"') return nDefault;
//else str2num(Keys[i].KeyValue,&nDefault);
else return atoi(Keys[i].KeyValue);
break;
}
}
i++;
}
return nDefault; //if didn't find tgt, return ndefault
}
void GetStr_1(const char* tgtSection, const char *tgtName, char* dstStr, const char* nDefault = "NULL")
{
int i=0;
while(Keys[i].KeyName[0]!='\0')
{
if(strcmp(tgtSection,Keys[i].KeySection)==0)
{
if(strcmp(tgtName,Keys[i].KeyName)==0)
{
if(Keys[i].KeyValue[0] == '\0' || Keys[i].KeyValue[0] != '"')
{
strcpy(dstStr, nDefault);
return ;
}
else
{
char* tgt= Keys[i].KeyValue + 1;
while(*tgt!='"') *dstStr++=*tgt++;
*dstStr ='\0';
return;
}
}
}
i++;
}
strcpy(dstStr, nDefault); //if didn't find tgt, return ndefault
}
empty_entries函数:转存Keys缓冲区的条目
将Keys缓冲区的数据,转存到结构体数组stuData里面:
void empty_entries(int now_stu, int now_sec,int k)
{
stuData[now_stu].identify_num = now_sec;
for (int i=0;i<COURSES_NUM;i++)
{
stuData[now_stu].scores[i] = GetInt(courses_tag[i],k,0);
}
//stuData[now_stu].scores[0] = GetInt("Chn",k,0);
//stuData[now_stu].scores[1] = GetInt("Math",k,0);
//stuData[now_stu].scores[2] = GetInt("Eng",k,0);
GetStr("Gender",k,stuData[now_stu].Gender," ");
GetStr("ID",k,stuData[now_stu].StuID ," ");
GetStr("Name",k,stuData[now_stu].Name," ");
GetStr("Tel",k,stuData[now_stu].TelNum," ");
stuData[now_stu].StuID_int = atoi(stuData[now_stu].StuID);
/* int i=now_stu; view_student(i);*/
//printf ("- %d entries for Section [%05d] has been transferred to stuData. \n",k,now_sec);
}
注释掉的代码,大概是为了调试使用,以及一些弃用的做法;放上去也许能够帮助读者更好的理解吧……
load_stuData函数:加载学生数据
这个代码我写的比较麻烦,因为我用的fgetc()
来读取文本,因此遇到了这些问题:
- 文件终止(EOF)判断 ——这个麻烦最大
- 键名、键值的读取与储存
- Section的判断(即每个学生的识别)
- 行末的判断
- 文件编码方式
int load_stuData(const char *libFile = "stulib.txt")
{
memset(stuData, 0x00, sizeof(stuData));
FILE *fp = fopen (libFile,"r");
if (fp==NULL)
{
printf("# Can't open library file \"%s\". \n",libFile);
return 0;
}
else printf("# Successfully open library file \"%s\". \n",libFile);
char c, now_section[16]= {0}, tmp[128],*t;
int now_sec=0,tot_students=0,k=0;
while((c=fgetc(fp))!=EOF)
{
while( c== ' '||c=='\n' ) c=fgetc(fp);
if ( c==EOF ) break;
if (c=='[') // read section identifier;
{
if (k!=0) //meaning there're entries stored in "Keys" for the previous Sec.
{
empty_entries(tot_students,now_sec,k);
tot_students++;
k=0;
}
//update now_sec (int);
t=tmp;
while(c!='\n')
{
*t++=c;
fscanf(fp,"%c",&c);
}
*t = '\0';
strcpy(now_section,tmp);
str2num(now_section,&now_sec);
//printf("- Section Updated: Now section: [%05d] \n",now_sec);
//# now_sec(int) updated;
continue;
}
t = tmp;
while( c!='=' && c!= ':'&& c!='\n') //read entry's name;
{
*t++ = c;
fscanf(fp,"%c",&c);
}
*t = '\0';
trim(tmp,tmp); //dispose spaces
strcpy(Keys[k].KeyName,tmp);
t = tmp;
fscanf(fp,"%c",&c);
while( c == ' ' || c == '\n' ) fscanf(fp,"%c",&c); //looking for the left quotation mark
//fscanf(fp,"%c",&c);
while( c != ';' && c != '\n' && c!= EOF) //read entry's value until the next mark
{
*t++=c;
fscanf(fp,"%c",&c);
}
*t = '\0';
trim(tmp,tmp);
strcpy(Keys[k].KeyValue,tmp);
//printf("- Now Section %d, %d entries has been readed. ",now_sec,k+1);
//printf("Name = %s; Value = %s \n",Keys[k].KeyName,Keys[k].KeyValue);
k++; //finish reading an entry, cnt++
}
if(k!=0)
{
empty_entries(tot_students,now_sec,k);
tot_students++;
k=0;
}
if (fclose(fp)!=0)
{
printf("# Error in closing library file \"%s\". \n",libFile);
}
else printf("# Successfully closed library file \"%s\". \n# %d sections has been readed.\n",libFile,tot_students);
return tot_students; //all the students/sections that've been readed from the file;
}
save_stuData函数:输出学生信息(数据库)到文本
其实就是用fprintf
了,没太大技术含量,比加载简单多了。
void save_stuData(const char *libFile = "stulib.txt")
{
FILE* fp = fopen(libFile,"w");
if (fp==NULL)
{
printf("Can't open library file \"%s\". \n",libFile);
return ;
}
else printf("# Successfully opened library file \"%s\". \n",libFile);
int k=0;
int new_identify_num = 1;
while (stuData[k].identify_num!=DEL_IDENTIFY_NUM && stuData[k].identify_num != 0)
{
fprintf(fp,"[%05d]\n",new_identify_num++); // assign a new identify num when saving the lib;
fprintf(fp,"ID = \"%s\"; \n",stuData[k].StuID);
fprintf(fp,"Name = \"%s\"; \n",stuData[k].Name);
fprintf(fp,"Gender = \"%s\"; \n",stuData[k].Gender);
fprintf(fp,"Tel = \"%s\"; \n",stuData[k].TelNum);
for (int i=0;i<COURSES_NUM;i++)
fprintf(fp,"%s = %d; \n",courses_tag[i],stuData[k].scores[i]);
//printf("- %d students' information has been saved;\n",k+1);
k++;
}
if(fclose(fp)!=0) printf("Unable to close lib file \"%s\". \n",libFile);
printf("# %d students' data has been saved to \"%s\". \n",k-1+1,libFile);
}
读取、应用、保存设置的函数:
set_settings
:将缓冲区的配置键应用到内存中;
save_settings
:保存所有配置信息到文件;
load_settings
:加载整个设置文件到缓冲区;使用了fgets()函数,比用fgetc()好写一些,但是这样文件就必须严格分行;好处是,可以以ini的风格写注释在文件里面了。
void set_settings()
{
//load course num and tags;
COURSES_NUM = GetInt_1("courses","COURSES_NUM",3);
char tmp_vname[8];
for (int i=0;i<COURSES_NUM;i++)
{
int2str(i,tmp_vname);
GetStr_1("courses",tmp_vname,courses_tag[i]);
}
printf("# Settings succeesfully loaded!\n");
}
void save_settings(int if_silence = 0, const char* cfgFileName = "settings.ini")
{
FILE *fp = NULL;
if ( ( fp = fopen(cfgFileName,"w") ) == NULL)
{
printf("Can't open config. file \"%s\". \n", cfgFileName);
return ;
}
else if (if_silence == 0) printf("# Successfully opened config. file \"%s\". \n",cfgFileName);
fprintf(fp,"[courses]\n");
fprintf(fp,"COURSES_NUM = %d ;\n",COURSES_NUM);
for(int i=0;i<COURSES_NUM;i++)
{
fprintf(fp,"%d = \"%s\"; \n",i,courses_tag[i]);
}
if (fclose(fp)!=0) printf("# Error in closing config. file \"%s\". \n",cfgFileName);
else if (if_silence == 0) printf("# Successfully saved settings to config. file \"%s\". \n",cfgFileName);
}
void load_settings(const char* cfgFileName = "settings.ini")
{
memset(Keys, 0x00, sizeof(Keys));
FILE *fp = NULL;
if ( ( fp = fopen(cfgFileName,"r") ) == NULL)
{
printf("Can't open config. file \"%s\". \n", cfgFileName);
return ;
}
else printf("# Successfully opened config. file \"%s\". \n",cfgFileName);
char tmp[128]={0}; //buffer: to store each line
char *p;
int k=0;
char now_section[128]="Default";
while (fgets(tmp,sizeof(tmp),fp))
{
trim(tmp,tmp);
if(tmp[0]=='#'||tmp[0]==',') continue; //ignore comments
if(tmp[0]=='\0') continue; //ignore empty line
char *c=tmp;
if(*c=='[')
{
c++;
char *l=now_section;
while(*c!=']') *l++=*c++;
*l='\0';
continue;
}
strcpy(Keys[k].KeySection,now_section);
char *t;
t = Keys[k].KeyName;
while(*c!='='&&*c!=':') *t++=*c++;
*t = '\0';
c++;
trim (Keys[k].KeyName,Keys[k].KeyName);
t = Keys[k].KeyValue;
while(*c!='\0'&&*c!=';'&&*c!='#') *t++=*c++;
*t = '\0';
trim(Keys[k].KeyValue,Keys[k].KeyValue);
//printf("[%s] %s = %s \n",Keys[k].KeySection,Keys[k].KeyName,Keys[k].KeyValue);//
k++;
}
if (fclose(fp)!=0) printf("# Error in closing config. file \"%s\". \n",cfgFileName);
else printf("# Successfully closed config. file \"%s\". \n",cfgFileName);
set_settings();
}
功能函数
qsort_stuData:使用快速排序对stuData排序
因为很多地方都会用到这个对stuData
数组排序的函数,因此设置了一个type
参数,可以根据这个参数实现多种类型的排序(学号、identify_num
、指定课程的成绩、总分);
不过很遗憾
- 仅支持单关键字
- 只写了升序排序
- 不能根据姓名排序
void qsort_stuData(int l, int r, int type=-1)
{
int x=l,y=r,mid;
char mid_str[128];
stuInfo _temp;
//type: idnum: -1, stuid -2, name -3, gender -4, course tag
//set mid value
if (type == -2) mid = stuData[(l+r)/2].StuID_int;
else if (type == -1) mid = stuData[(l+r)/2].identify_num;
else if (type > 0 && type < COURSES_NUM ) mid = stuData[(l+r)/2].scores[type];
else if (type == COURSES_NUM ) mid = stuData[(l+r)/2].tot_scores;
while (x<=y)
{
if (type == -2)
{
while (stuData[x].StuID_int < mid) x++;
while (stuData[y].StuID_int > mid) y--;
}
else if (type == -1)
{
while (stuData[x].identify_num < mid) x++;
while (stuData[y].identify_num > mid) y--;
}
else if (type > 0 && type < COURSES_NUM) //descending order
{
while (stuData[x].scores[type] > mid) x++;
while (stuData[y].scores[type] < mid) y--;
}
else if (type == COURSES_NUM ) //descending order
{
while (stuData[x].tot_scores > mid) x++;
while (stuData[y].tot_scores < mid) y--;
}
if (x<=y)
{
_temp = stuData[y];
stuData[y] = stuData[x];
stuData[x] = _temp;
x++;
y--;
}
}
if(l<y) qsort_stuData(l,y,type);
if(x<r) qsort_stuData(x,r,type);
}
getline函数:读取一整行键盘输入
也是自定义的函数,前面说过了,和gets()用法差不多,这里不多解释;
int getline(char* dstStr)
{
char c;
int k=0;
scanf("%c",&c);
while(c!='\n')
{
*dstStr++ = c;
k++;
scanf("%c",&c);
}
*dstStr = '\0';
return k;
}
get_choice_int:读取一个用户的整型输入
这个是为了应对不规范的用户输入,因为要写判断语句很多行比较麻烦,所以直接做了一个函数;调用这个函数后,会要求用户输入一个从rangeA
到rangeB
的数据,不输入正确不能走。
void get_choice_int(int &choice, int rangeA, int rangeB)
{
char input[8];
choice = rangeA-1;
while (choice<rangeA||choice>rangeB) {
getline(input);
choice = atoi(input);
if (choice<rangeA||choice>rangeB) printf("Illegal input! Please try again. \n");
}
}
add_student、del_student:添加或删除单一学生
这是做的小模块,后边会有对多个学生的操作函数。
int add_student()
{
int i=0;
//在末尾找到一个位置插入
while(stuData[i].identify_num!=DEL_IDENTIFY_NUM && stuData[i].identify_num != 0) i++;
printf("# After filling in these information, a new student will be added:\n");
printf(" Student ID : ");
getline(stuData[i].StuID); trim(stuData[i].StuID,stuData[i].StuID);
printf(" Name : ");
getline(stuData[i].Name); trim(stuData[i].Name,stuData[i].Name);
printf(" Gender : ");
getline(stuData[i].Gender); trim(stuData[i].Gender,stuData[i].Gender);
printf(" Tel Number : ");
getline(stuData[i].TelNum); trim(stuData[i].TelNum,stuData[i].TelNum);
stuData[i].identify_num = stuData[abs(i-1)].identify_num + 1;
stuData[i].StuID_int = atoi(stuData[i].StuID);
printf("# "); view_student(i);
printf("# Student added successfully! \n");
return i+1; //now students;
}
void del_student(int i)
{
int k=0;
while (stuData[k].identify_num!=DEL_IDENTIFY_NUM && stuData[k].identify_num != 0) k++;
view_student(i); //显示被删除学生的信息
stuData[i].identify_num=DEL_IDENTIFY_NUM;
qsort_stuData(0,k-1);
printf("# Student deleted successfully!\n");
return ;
}
edit_info、edit_scores:编辑单一学生的信息、各科成绩
信息编辑过程中,不想改动的信息可以按下回车,自动进入下一行,而不会修改信息;
void edit_info(int i)
{
printf("* "); view_student(i);
printf("- Input something to replace previoius ones;\n");
printf("- Type '#' or nothing to NOT modify certain items;\n");
char input[128]; //set input buffer
printf(" Stu. ID: %16s | ",stuData[i].StuID);
getline(input); trim(input,input);
if(input[0]!='#'&&input[0]!='\0') { strcpy(stuData[i].StuID,input);
stuData[i].StuID_int = atoi(stuData[i].StuID); }
printf(" Name : %16s | ",stuData[i].Name);
getline(input); trim(input,input);
if(input[0]!='#'&&input[0]!='\0') strcpy(stuData[i].Name,input);
printf(" Tel Num: %16s | ",stuData[i].TelNum);
getline(input); trim(input,input);
if(input[0]!='#'&&input[0]!='\0') strcpy(stuData[i].TelNum,input);
printf(" Gender : %16s | ",stuData[i].Gender);
getline(input); trim(input,input);
if(input[0]!='#'&&input[0]!='\0') strcpy(stuData[i].Gender,input);
printf("# Student's basic information has been updated!\n");
printf("# "); view_student(i);
}
void edit_scores(int i)
{
printf("- Input new scores to replace previoius ones;\n");
printf("- Type '#' or nothing to NOT modify certain items;\n");
//view_scores (i,1);
char input[128]; //set input buffer
view_scores(i,-1);
for(int j=0;j<COURSES_NUM;j++)
{
printf("- %-16s: %3d | ",courses_tag[j],stuData[i].scores[j]);
getline(input); trim(input,input);
if(input[0]!='#'&&input[0]!='\0')
{
stuData[i].scores[j]=atoi(input);
}
}
printf("# Student's scores has been updated!\n");
}
quick_scoring:快速填写所有学生某一科的成绩
进入该功能,选择一项课程,便会按照当前的列表顺序依次输入学生成绩(可以先执行按学号排序再进行成绩录入),过程中按“#”终止进程,否则全部录入完为止;
void quick_scoring()
{
printf("# You're going to input all the students' scores in a row;\n");
show_courses_name();
printf("# Input the course No: ");
int choice;
char input[8];
get_choice_int(choice,1,COURSES_NUM);
printf("# Input '#' to end this process.\n");
int i=0;
while(stuData[i].identify_num!=DEL_IDENTIFY_NUM && stuData[i].identify_num != 0)
{
printf ("%3d. %-20s %-s: %3d | ",i+1,stuData[i].Name,courses_tag[choice-1],stuData[i].scores[choice-1]);
getline(input); trim(input,input);
if(input[0]=='#') break;
if(input[0]!='\0')
{
stuData[i].scores[choice-1]=atoi(input);
}
i++;
}
printf("# %d scores has been readed!\n",i);
}
view_scores函数:显示一个学生的各科成绩
如果mode
参数的值是-1
的话,只输出一行学号和姓名;默认以列表形式输出成绩;
void view_scores(int i, int mode = 0 )
{
printf("# %-8s %-16s ",stuData[i].StuID,stuData[i].Name);
if (mode == -1) {printf("\n"); return;}
if (mode == 0)
{
printf ("\n");
for (int j=0;j < COURSES_NUM ;j++)
{
printf("- %-8s: %3d \n",courses_tag[j],stuData[i].scores[j]);
}
}
}
view_all_students:显示所有学生
void view_all_students()
{
printf("# Viewing all students' information: \n");
print_title();
int i=0;
while(stuData[i].identify_num!=DEL_IDENTIFY_NUM && stuData[i].identify_num != 0)
{
view_student(i);
i++;
}
printf("# Total: %d students\n",i);
}
search_students:利用一段关键字查找学生
用到了strstr()函数;以一个字符串为关键字,查找含有关键字的学生,并把“当前序号”存在数组里,同时列表输出找到的学生的信息。
int search_student(const char* keywords, int results_index[])
{
int i=0,k=0;
while(stuData[i].identify_num!=DEL_IDENTIFY_NUM && stuData[i].identify_num != 0)
{
//search keyword in stuID, name, tel;
char* if_ID = strstr(stuData[i].StuID,keywords);
char* if_Name = strstr (stuData[i].Name,keywords);
char* if_Tel = strstr(stuData[i].TelNum,keywords);
if( if_ID!= NULL || if_Name!= NULL ||if_Tel !=NULL) results_index[k++]=i;
i++;
}
//view search results
printf("# %d results have been found! \n",k);
print_title();
for(int m=0;m<k;m++)
{
//printf ("%2d| ",m);
view_student(results_index[m]);
}
return k; //return num. of the results
}
search_score:按成绩查找学生
逻辑和上面一个比较类似。
int search_score(int tgt_score,int results_index[], int mode = 0) //0 for below and 1 for above
{
int i=0,k=0;
while(stuData[i].identify_num!=DEL_IDENTIFY_NUM && stuData[i].identify_num != 0)
{
for(int j=0;j<COURSES_NUM;j++)
if(stuData[i].scores[j]<tgt_score)
{
results_index[k++]=i;
break;
}
i++;
}
printf("# %d results have been found! \n",k);
print_title();
for(int m=0;m<k;m++)
{
//printf ("%2d| ",m);
view_student(results_index[m]);
}
return k;
}
调用基础功能的“用户友好”函数
编辑学生信息
有三个功能,分别是:
- 编辑一些学生的基本信息
- 编辑一些学生的成绩
- 快速录入成绩
其中,“一些”是因为修改一个学生后,会提示用户继续输入“当前编号”进行修改,需要输入#
才能退出。
而且 ,进入编辑时,也会询问用户是根据“当前编号”,还是先来一波关键字搜索。
另外,这里面用到了指向函数的指针,p_function
。
void edit_student_info()
{
char input[128],choice='0';
void (*p_function)(int);
printf("# You're going to edit one student's information, \n");
printf("- Enter 1: To edit one's basic Info. ; \n");
printf("- Enter 2: To edit one's scores;\n");
printf("- Enter 3: Quick input scores; \n");
while (choice!='1'&&choice!='2'&&choice!='3')
{
getline(input); trim(input,input);
choice = input[0];
if (choice == '1') p_function=edit_info;
else if (choice == '2') p_function=edit_scores;
else if (choice == '3') {quick_scoring(); return;}
else continue;
}
printf("- Enter A: Edit by students current No. ;\n");
printf("- Enter B: Search for students by a keyword, then edit. ;\n");
printf("- Enter #: Exit editing. \n");
getline(input); trim(input,input); //trim user's input
choice=input[0];
if (choice == '#') return;
while (choice == 'A') //Mode 1: edit by No.;
{
printf("- Please enter his/her No.; ");
printf("enter '#' to finish editing. \n" );
getline(input);
if (input[0]=='#') return;
int i = atoi(input);
if(i<=0)
{
printf("Illegal input!\n");
continue;
}
p_function(i-1);
//return ;
}
if (choice =='B')
{
int results_index[128];
printf("- Please input a keyword for searching : ");
getline(input); trim(input,input);
int k = search_student(input, results_index);
while (input[0]!='#')
{
printf("- Enter the No. of a certain student to edit his/her info., \n");
printf("- Enter '#' to stop editing. \n" );
getline(input);
if (input[0]=='#') return;
int i = atoi(input);
if(i<=0)
{
printf("Illegal input!\n");
continue;
}
p_function(i-1);
}
return;
}
}
编辑课程条目
支持对课程条目的添加、删除、重命名。
void edit_courses_tag()
{
printf("# Current courses: \n");
show_courses_name();
printf("- 1. Add a course;\n");
printf("- 2. Delete a course;\n");
printf("- 3. Rename a course;\n");
printf("- 0. Exit.\n");
int choice;// = -1;
get_choice_int(choice,0,3);
if(choice == 0 ) return;
char input[16];
if(choice == 1)
{
printf("# Input the new courses' name: ");
getline(input);
trim(input,input);
strcpy(courses_tag[COURSES_NUM],input);
COURSES_NUM++;
show_courses_name();
save_settings(1);
return ;
}
if (choice == 2)
{
printf("# CAUTION: This will erase all the scores under this course \n");
printf("# and can NOT be undone. Are you sure to continue? (Y/N)\n");
getline(input);
trim(input,input);
if (input[0] != 'Y') return;
printf("# Enter the course No. to delete it: ");
get_choice_int(choice,1,COURSES_NUM);
for(int i=choice;i<COURSES_NUM;i++)
{
strcpy(courses_tag[i-1],courses_tag[i]);
}
int i=0;
while(stuData[i].identify_num!=DEL_IDENTIFY_NUM && stuData[i].identify_num != 0)
{
for (int j=choice;j<COURSES_NUM;j++) stuData[i].scores[j-1] = stuData[i].scores[j];
i++;
}
printf("# Deleted Successfully! \n");
COURSES_NUM--;
show_courses_name();
save_settings(1);
return ;
}
if (choice == 3)
{
printf("# Enter the course No. to rename it: \n");
get_choice_int(choice,1,COURSES_NUM);
printf(" %d. %s | ",choice-1+1,courses_tag[choice-1]);
getline(courses_tag[choice-1]);
trim(courses_tag[choice-1],courses_tag[choice-1]);
show_courses_name();
save_settings(1);
return;
}
}
查找
根据关键字查找同学,或者查找不及格的同学(扎心了)。
void search_by(int *results)
{
char input[128];
printf("# Search for students by: \n");
printf("- 1. A keyword; \n");
printf("- 2. Students who failed; \n");
int choice = 0;
while (choice<=0 || choice > 2)
{
getline(input);
choice = atoi(input);
}
if (choice == 1)
{
getline(input);
trim(input,input);
search_student(input,results);
}
if (choice == 2)
{
search_score(60,results);
}
}
排序
可以从注释看出,本来还想做根据姓名、性别的排序,但是……鸽了鸽了
void sort_by()
{
int k=0;
while (stuData[k].identify_num!=DEL_IDENTIFY_NUM && stuData[k].identify_num != 0) k++;
// find tot student numbers;
//type: idnum: -1, stuid -2, name -3, gender -4, course tag
printf("# You're going to sort the students by: \n");
printf("- 1. By student ID;\n");
printf("- 2. By scores;\n");
int choice = 0;
char input[8];
get_choice_int(choice,1,2);
if (choice == 1)
{
qsort_stuData(0,k-1,-2);
}
if (choice == 2)
{
choice = 0;
show_courses_name();
printf(" %2d. By total scores.\n",COURSES_NUM+1);
while (choice <= 0 || choice > COURSES_NUM + 1 ) //ignore illegal input
{
printf("- Type No. to sort by the scores of it.\n");
getline(input);
choice = atoi (input);
}
if (choice == COURSES_NUM +1) cal_tot_scores();
qsort_stuData(0,k-1,choice-1);
}
printf("# Sorted! \n");
view_all_students();
}
帮助文本
void view_help_text()
{
const char* sep="---------------------------------------------------\n";
printf("%s",sep);
printf(" 0. Show Help text;\n");
printf(" 1. Load students' data from \"stulib.txt\";\n");
printf(" 2. Save students' data to \"stulib.txt\";\n");
printf(" 3. View all students;\n");
printf(" 4. Search for students; \n");
printf(" 5. Add a student;\n");
printf(" 6. Edit Info. & Scores;\n");
printf(" 7. Delete a student;\n");
printf(" 8. Sort by ... ;\n");
printf(" 9. Edit courses tag; \n");
//printf("-1. Exit & Save. \n");
printf("-1. Exit without saving. \n");
printf("%s",sep);
}
主函数
tot_students
这个变量到后来似乎也没有什么用了,都被那个while循环给替代了;
int main()
{
load_settings();
int tot_students=0;
//tot_students = elimination(tot_students);
int ch=0;
int results[256];
char input[128]={0};
view_help_text();
while (ch!=-1&&ch!=-2)
{
getline(input);
ch = atoi(input);
switch(ch)
{
case 0: view_help_text(); break;
case 1: tot_students = load_stuData("stulib.txt"); break;
case 2: save_stuData("stulib.txt"); break;
case 3: view_all_students(); break;
case 4: search_by(results); break;
case 5: tot_students = add_student(); break;
case 6: edit_student_info(); break;
case 7:
printf("# Input the No. of the student to delete: ");
getline(input);
del_student( atoi(input) -1);
break;
case 8: sort_by(); break;
case 9: edit_courses_tag(); break;
//case -1: save_stuData(); break;
case -1: break;
}
if (ch>0) printf("\n# Backed to the main menu. Enter 0 for HELP. \n");
}
return 0;
}
然后,就完了。
结语
所有代码已经在上边了,如果想看看我的作品如何,只需要复制粘贴编译运行即可。可能需要stdlib.h
,string.h
这些头文件。
好像有两个define忘了交代……
#define MAX_STUDENTS_NUM 200
#define DEL_IDENTIFY_NUM 99999
这个,算是我们C语言程序设计的结课作业,一些东西还是后来和同学交流时想到才添加的,比如行标题的打印;写了好久,仅仅是完成文件的输入输出就花了两天时间。
这就是我写了快1000行的代码,实现的功能,呃,好像和别人几百行的差不多,有点小难过,感觉自己很垃圾……
好处是,这个程序的扩展性会比较好吧,可以方便的加入功能,比如添加一些基本信息,比如宿舍号(我一开始没想到,和同学交流时了解到的)、生日、民族等等,甚至可以做成像课程条目那样的支持用户自定义的形式。还可以加上自定义列宽、名片式学生信息展示等功能。
嗯,实现起来简单一些的扩展,就是拓展到多班级的支持,可以看到这个程序的主函数并不会自动加载数据库文件,而且load_stuData函数是可以接受不同的filePath
参数的,这其实是我留的接口,但是目前是不会做了(可能以后也不会做)。
有人说我代码不好懂,所以,看完的你,辛苦了!如果你喜欢这个程序、并且耐心的看完了这么长(我注释写得不多,更没心思画框图,阅读起来属实不容易),可以试试添加些自己喜欢的功能。
而且,估计现实生活中不会有谁会用这样一个程序管理学生信息吧,用击键方式与电脑交流怕是已经过时,图形化才是潮流,所以程序写得再高级估计也就是锻炼一下自己,抑或是自娱自乐吧,意义不是很大……
写这篇博客的目的,大概是为了纪念一下,同时记录下自己的一些想法;也希望能够给需要的同学一些灵感,或者我自己能够收获到一些灵感。
感谢你的阅读!