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++)
	{
   
		
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值