实现简单的学生选课信息管理系统

本文介绍了一项中山大学软件工程课程的大作业,任务是设计并实现一个学生选课信息管理系统,包括学生信息、课程信息及选课信息的增删改查功能。系统采用C语言编写,数据存储在文本文件中,利用链表结构进行数据管理。文章讨论了整体架构、部分实现和业务逻辑,包括结构体设计、文件读写以及菜单系统等。
摘要由CSDN通过智能技术生成

中山大学软件工程程序设计I 大作业。

要求

本系统模拟实现学生课程信息管理系统,其中包括学生信息,课程信息以及学生的选课信息(储存在文本文件当中),其中功能包括三部分:

学生相关功能

  1. 添加学生信息到学生信息文件当中,学生信息包括:学号(stuId),姓名(stuName),性别(stuSex)
  2. 删除学生信息
  3. 改变学生信息
  4. 查看学生信息

课程相关功能

  1. 添加课程信息到课程信息文件中,课程信息包括:课程编号(couId),
    课程名称(couName),课程人数(stuNum),选课人数(curStuNum),平均分(aveScore)
  2. 删除课程信息
  3. 改变课程信息

选课相关功能

  1. 选课,即添加学生到课程,将学生添加到当前课程的选课信息表中,需要保证:
    1. 学生已经添加到学生信息表中;
    2. 添加到已有的课程中,即课程信息表中有当前课程;
    3. 当前课程还可选,即已选课的人数未超过课程的最大人数。
      此时每个课程都对应一张选课信息文件,存储当前课程的选课情况。
      选课信息包括:学生编号(stuId)、成绩(stuScore)默认为0/空
  2. 退选,即将学生从课程中删除。
  3. 成绩录入
  4. 查看选课信息

实现目标

实现学生课程信息管理系统,可以在命令行进行上述功能的操作,最终的信息保存在相应的文件当中。

题解

怕是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关闭文件。

业务逻辑

到了处理业务的时候了。我们这里着重讲学生信息的增删,分别对应addStudentremoveStudent

结构体

首先我们需要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);

输入性别这方面,我们可以要求用户输入malefemale,不过这可能带来更大的代码量,而且用户可能输入错误,因此我们只允许用户输入01表示男和女。然后对于用户的输入,我们一定要注意验证用户输入的合法性,我们不能保证用户输入的东西都是正确的,用户可以在输入性别的时候输入不是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就不能是不持久的变量的地址(比如函数的局部变量在退出函数后就从内存中“消失”了,也就是不持久,这样如果我们持有一个不持久变量的指针,一旦变量从内存中消失了后我们再访问这块内存空间会发生什么呢&

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值