中山大学软件工程程序设计I 大作业。
要求
本系统模拟实现学生课程信息管理系统,其中包括学生信息,课程信息以及学生的选课信息(储存在文本文件当中),其中功能包括三部分:
学生相关功能
- 添加学生信息到学生信息文件当中,学生信息包括:学号(stuId),姓名(stuName),性别(stuSex)
- 删除学生信息
- 改变学生信息
- 查看学生信息
课程相关功能
- 添加课程信息到课程信息文件中,课程信息包括:课程编号(couId),
课程名称(couName),课程人数(stuNum),选课人数(curStuNum),平均分(aveScore) - 删除课程信息
- 改变课程信息
选课相关功能
- 选课,即添加学生到课程,将学生添加到当前课程的选课信息表中,需要保证:
- 学生已经添加到学生信息表中;
- 添加到已有的课程中,即课程信息表中有当前课程;
- 当前课程还可选,即已选课的人数未超过课程的最大人数。
此时每个课程都对应一张选课信息文件,存储当前课程的选课情况。
选课信息包括:学生编号(stuId)、成绩(stuScore)默认为0/空
- 退选,即将学生从课程中删除。
- 成绩录入
- 查看选课信息
实现目标
实现学生课程信息管理系统,可以在命令行进行上述功能的操作,最终的信息保存在相应的文件当中。
题解
怕是ACM打多了啥作业都说题解了。。
整体架构
首先我们需要思考程序的主题架构如何。也就类似作文提纲一样的东西。我们需要提炼业务需求,将程序划分成几个模块然后分别实现。这样不仅能使我们的思路清晰,还方便了我们的实现(因为逻辑混在一起一旦出错很难调试,而且后期如果要扩展也会造成困难,一堆绳子缠在一起自然难解开,我们需要先捋顺绳子)。
通过读题,我们发现题目给出了3大部分,共11个要求。这些要求基本都是增删改查系列数据库的经典操作(当然这里不给用数据库了。。)。我们对于每一个要求,实现一个具体的函数表达相关的功能。比如建立函数addStudent
表示添加学生信息操作的功能等等。你可以参考下面的程序看到会有哪些函数。
此外,我们注意到我们还需要将信息存储在文件中,因此很自然地想到我们实际上还有两个需求,分别是从文件中读取信息和将信息存储在文件中。因此最后我们有13个函数,表达我们的业务逻辑(这里的业务逻辑我认为是和数据打交道的意思,这13个函数都是在操作维护学生课程信息,也就是一堆数据)。
然后我们还有绘制界面的函数,一共有4个菜单,分别是主菜单、学生相关功能菜单、课程相关功能菜单和选课相关功能菜单,每个菜单一个函数去绘制界面。比如主菜单应该有4个选项,分别是进入学生菜单、进入课程菜单、进入选课菜单和退出程序。这里要注意我们必须要有退出程序的菜单,以及子菜单必须有返回上一级的选项,作为菜单导航。否则进了程序不能正常退出(按Ctrl-C可以强制结束程序,但我们需要保存信息到文件中,强制退出会导致不保存信息,即丢失信息),进了子菜单不能返回上一级菜单就太滑稽了。
最后我们的主程序,即main函数应该做的事情就是加载数据、控制菜单逻辑和保存数据。要显示那个菜单显然不是菜单自己决定的,我们需要有一个“菜单管理器”去管理菜单,就像文件管理器一样。
部分实现
具体实现细节请参考代码。
main函数和菜单函数
我们在整体架构中已经提了主程序的实现方法,这里我们继续讲。我们给每个菜单标号,这里我们假定主菜单标号为0,学生菜单标号为1,课程菜单标号为2,选课菜单标号为3。如果我们在main函数里设置一个变量loc表示当前显示的菜单标号,然后通过标号调用对应菜单的函数绘制就可以了。注意我们绘制菜单的时候需要清屏(清屏的方法是system("cls")
,当然仅适用于Windows操作系统,表示调用控制台的cls
命令,这个命令的功能就是清屏)。
也就是说我们main
函数实现的代码大概长这样:
loadFrom(FILE); // 加载文件信息
while (1) {
system("cls"); // 清屏
switch (loc) {
case 0: printRootMenu(&loc); break;
case 1: printSecondaryMenuForStudent(&loc); break;
case 2: printSecondaryMenuForCourse(&loc); break;
case 3: printSecondaryMenuForElection(&loc); break;
case 4: return 0;
}
saveTo(FILE); // 保存文件信息
我们还需要一个特别的标号4表示退出程序。这里我的实现思路是每个菜单函数里先绘制界面,也就是不断地printf
,然后scanf
读入我们的选项,即选了菜单的哪一项,再判断对应的操作,比如选了添加学生信息就调用addStudent
函数开始添加学生信息。最后如果选到了返回上一级菜单,也就是说loc
要变了(因为loc
表示当前菜单的编号,返回上一级菜单,编号自然不一样),所以我们允许每个菜单函数修改loc
,比如loc=0
就可以表示回到根菜单,那我们设置loc=4
就表示退出程序,一个道理。当然有大佬会说你这么写很不好,确实,菜单是树形结构,如果遇到多级菜单,我们就需要记录每个菜单的父菜单是哪一个,然后返回上一级菜单就将loc=parent[loc]
即可。
最后文件的管理我们通过fopen
函数获取一个FILE*
指针,然后传给loadFrom
函数和saveTo
函数使用。记得用完后调用fclose
关闭文件。
业务逻辑
到了处理业务的时候了。我们这里着重讲学生信息的增删,分别对应addStudent
,removeStudent
。
结构体
首先我们需要Student
结构体描述一个学生的信息:
struct Student {
int id;
char name[20];
int sex;
};
分别表示学号、名字和性别。课程信息和选课信息的结构体请参考程序。
addStudent
提示用户输入
显然我们需要添加学生的话,就需要用户输入学生id、名字和性别。
所以我们最开始需要提示用户要输入什么,所以:
printf("Please enter student id first, name second and sex later with 0 meaning male, 1 meaning female.\n");
然后要求用户连着输入id、name和sex。当然我们可以这样:
printf("Please enter student id: "); scanf("%d", &student.id);
printf("Please enter student name: "); scanf("%s", student.name);
printf("Please enter student sex with 0 male and 1 female: "); scanf("%d", &student.sex);
输入性别这方面,我们可以要求用户输入male
和female
,不过这可能带来更大的代码量,而且用户可能输入错误,因此我们只允许用户输入0
和1
表示男和女。然后对于用户的输入,我们一定要注意验证用户输入的合法性,我们不能保证用户输入的东西都是正确的,用户可以在输入性别的时候输入不是0和1的数字比如2,更可以是字符串。对于用户输入了错误数字的情况,我们读取了sex
以后要判断如果sex<0||sex>1
直接提示用户输入错误,然后不再添加该学生信息(因为信息是错误的,我们要保证sex是正确的数值)。如果用户输入的不是数字怎么办?这时候我们就需要利用scanf
返回值了,scanf
返回值表示输入成功的参数个数,比如scanf("%d", &sex)
,如果用户输入的是字符串,那么scanf
返回值就为0,否则为1表示成功输入了一个参数sex
,当然还有返回值-1表示文件结束不可以再读入东西了。
因此光用户输入这里我们需要注意的就很多。
数据操作
如果我们使用数组存放学生信息,比如Student stu[1000];
这样,那么很简单,stu[++stuCount]=s
即可(其中Student s
为新学生信息)。
不过我这里使用了链表,大概就是pushFront(&head, &count, &s, sizeof(s))
这样。链表的实现细节后面再讲。
removeStudent
删除学生信息,首先我们需要用户输入我们要删除学生的名字,然后我们找到这个学生删除就可以了。也就是说我们先提示用户输入学生名,然后我们再输入字符串。然后还有用户输入的学生不存在的情况,我们要判断一下,并提示学生不存在。
数据维护
我们的数据维护有两种方式,一个是数组存储,一个是链表存储。
数组存储的好处是实现简单,但是如果数组长度不够的时候需要调用realloc
函数重新申请内存,当然这消耗的时间其实也不会很多。链表存储的好处是删除快而且自由,但是好像我还没有找到好的方法抽象链表的API。。
链表结构
数组实现没啥好说的,我们这里就讲讲链表吧。
首先我们需要一个能存储任意类型的链表。这需要所谓的“泛型指针”的void*
。我们定义链表节点的结构体为
struct Node {
void *data;
Node *next, *prev; // 我的代码是双向链表(虽然代码中不需要双向)
};
链表是啥。。建议另外上网查了。如果说数组在内存中的存储是连续的(这样我们访问数组元素只要再内存中直接定位就可以了),那么链表就是不连续存储的(这样我们就需要一个next
域表示下一个节点的内存位置)。
插入节点
那么向链表头插入元素的代码是:
void pushFront(Node **head, int *count, const void *new_data, size_t size) {
++*count; // 元素个数加一
Node *newNode = (Node *)malloc(sizeof(Node));
newNode->data = malloc(size); // 复制新数据到新节点里,这么做的好处是我们可以保证无论new_data是不是临时变量,函数都可以正常工作
memcpy(newNode->data, new_data, size); // 可以试一试将这两行改成newNode->data = new_data; 试试程序会发生什么。
newNode->next = *head; // 维护链表链接
newNode->prev = NULL;
if (*head != NULL) (*head)->prev = newNode;
*head = newNode;
}
因为我们表示任意类型只有void*
这个类型,还是个指针(当然实际上也只能是指针),因此我们在使用的时候必须适应指针,需要另外申请空间存放数据。也就是malloc(size)
的作用。至于为什么不是直接用new_data
,是因为我想简化程序实现,如果我们直接用new_data
,那么我们在调用pushFront
时传入的new_data
就不能是不持久的变量的地址(比如函数的局部变量在退出函数后就从内存中“消失”了,也就是不持久,这样如果我们持有一个不持久变量的指针,一旦变量从内存中消失了后我们再访问这块内存空间会发生什么呢&