前言
大概每个学习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++)
{