C语言程序设计 - 学生信息管理(支持文件读写、课程条目自定义)

前言

大概每个学习C语言的学生都会遇到这样一个作业吧:学生信息管理,我们老师布置的作业要求如下:

1. 定义结构体类型,存储每个同学的信息:
学号、姓名、性别、三门课程成绩(可以自由扩充,出生日期、电话、学号等等)
2. 功能要求:

  1. 显示所有同学的信息
  2. 查找指定同学的信息 (可扩充其他信息的查找)
  3. 修改指定同学的信息
  4. 显示有不及格同学的信息 (统计显示其他信息)
  5. 按指定课程排序输出
  6. 增加一个新同学
  7. 删除一个同学
  8. (还可以根据自己能力扩充功能,如文件存储读/写)

这个作业可以算是比较具有挑战性的、考验学生综合能力的,比较花时间,而且不仅需要做好应有的功能,也要注重操作界面的可视性(交互),令使用者易于上手。

怎么实现信息管理每个人都会不太一样,比如有人会使用链表+动态内存来存储数据,而我就只会用结构体数组(通过改前缀+排序实现删除功能)。

因此,这里仅仅是我个人的思路,而且,同样的功能我的实现方法可能会较为复杂,这里建议读者阅读自己需要的部分,或者撷取设计灵感应用到自己的程序里面。

功能与特色

本程序受到Excel存储学生信息表格的启发,决定模仿表格处理工具的使用逻辑进行程序的设计,即以行形式展示学生信息,使用学生的“当前编号”来对其执行操作。(这个当前编号其实就是在结构体数组中的位置…)

下面说说本程序的特色吧:

  1. 支持文件读入与写出,同时以一种用户友好的方式存储,即模仿ini配置文件的存储方式,采用“键名 = 键值”的方式存储,这样方便用户读取和修改文本文件中的内容,并且具有一定的抗扰乱能力(比如随便加一些什么条目在文件里也不会影响读取)。
  2. 支持课程条目的重命名和删除,并且能够保存下来,下次启动程序仍能够使用;
  3. 在本人使用的编译器(Dev-C++ 5.9)下,能够实现中文的读取与存储
  4. 支持根据学号或姓名的模糊查找,并返回信息列表,方便使用者搜索;

其中特性1 之所以采取上述的方式是因为完成作业期间了解到程序的“反射”——不太清楚是否正确使用了该术语,其实想说的就是“程序能够根据外界条件的变化修改自身运行参数”。

缺点

本程序的文件输出(即保存功能)是覆盖式的,也就是说,程序读取一次并保存后,无关的内容就会消失。

例如,删除了一个课程条目后,内存中加载的数据也会被相应删除(受限于学生信息的数据存储模式),但是文件此时还没有更改,所以,只要不执行保存操作,再把课程条目添加回来,重新载入文件,成绩还会存在;

另外,因为关于课程条目的设置是及时保存的,所以

  1. 重启程序后执行读取操作,也不会出现已删除课程条目相关的数据(读取是对照内存中的课程条目数量来的,这个会在程序启动时最先加载),除非把课程条目添加回来,重新载入文件。
  2. 如果重命名了课程名目,而没有手动保存文件,那么重启程序后再加载,将无法读取到该课程的数据

另外,文件的编码格式似乎有一定要求,需要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中的atoiatof等函数,使用起来也很方便;使用方法请自行查找,这里暂不解释。
这个手写函数的好处是,会返回结束查找处的位置,也就是说可以对一个字符数组连续使用;

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:读取一个用户的整型输入

这个是为了应对不规范的用户输入,因为要写判断语句很多行比较麻烦,所以直接做了一个函数;调用这个函数后,会要求用户输入一个从rangeArangeB的数据,不输入正确不能走。

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.hstring.h这些头文件。

好像有两个define忘了交代……

#define MAX_STUDENTS_NUM 200
#define DEL_IDENTIFY_NUM 99999

这个,算是我们C语言程序设计的结课作业,一些东西还是后来和同学交流时想到才添加的,比如行标题的打印;写了好久,仅仅是完成文件的输入输出就花了两天时间。

这就是我写了快1000行的代码,实现的功能,呃,好像和别人几百行的差不多,有点小难过,感觉自己很垃圾……

好处是,这个程序的扩展性会比较好吧,可以方便的加入功能,比如添加一些基本信息,比如宿舍号(我一开始没想到,和同学交流时了解到的)、生日民族等等,甚至可以做成像课程条目那样的支持用户自定义的形式。还可以加上自定义列宽名片式学生信息展示等功能。

嗯,实现起来简单一些的扩展,就是拓展到多班级的支持,可以看到这个程序的主函数并不会自动加载数据库文件,而且load_stuData函数是可以接受不同的filePath参数的,这其实是我留的接口,但是目前是不会做了(可能以后也不会做)。

有人说我代码不好懂,所以,看完的你,辛苦了!如果你喜欢这个程序、并且耐心的看完了这么长(我注释写得不多,更没心思画框图,阅读起来属实不容易),可以试试添加些自己喜欢的功能。

而且,估计现实生活中不会有谁会用这样一个程序管理学生信息吧,用击键方式与电脑交流怕是已经过时,图形化才是潮流,所以程序写得再高级估计也就是锻炼一下自己,抑或是自娱自乐吧,意义不是很大……

写这篇博客的目的,大概是为了纪念一下,同时记录下自己的一些想法;也希望能够给需要的同学一些灵感,或者我自己能够收获到一些灵感。

感谢你的阅读!

猜你喜欢

转载自blog.csdn.net/henry_23/article/details/104861446
今日推荐