学生管理系统
前言:
这个是我写的一个学生管理系统的一个简介,这个系统的主要功能如下:
- 添加学生信息
- 删除学生信息
- 显示所有学生信息
- 查询学生信息
- 修改学生信息
用户可以通过输入的序号来使用对应的功能。
系统框架设计:
核心数据结构
学生结构体
1 | typedef struct Student { |
设计思路:
- 作为系统的基本数据单元
- 包含
next
指针形成链表结构 stuid
作为唯一标识符(键值)- 使用固定大小数组简化内存管理
学生哈希表
1 | //学生数据库结构体 |
设计思路:
- 使用链地址法解决哈希冲突
table
数组存储每个桶的链表头head
指向全局链表头部,便于遍历- 固定大小哈希表简化实现
功能模块
初始化
1 | //初始化学生数据库 |
我们跟踪看看效果:
table 中的所有值也都变成了 NULL
这里的 StudentDB init_system() 中的 StudentDB*是返回类型,表示这个函数是一个指向 StudentDB 结构体的指针
init_system()是一个函数名
我们分配内存给数据库结构,然后设置合理的初始值都为 NULL,最后返回指针供其他函数使用。
添加学生信息
原先思路,将我们输入的每一个学生都添加到一个文件中,文件名为 Student_information.txt
然后代码流程思路为:
输入学生的学号,姓名,年龄和专业,然后通过哈希表来存入,当我们现在存完之后,通过一个文件队哈希表进行读取存放就可以达到本地保存了。但是再哈希表部分我们需要做一个判断就是我们这个学号是否是唯一的。也就是说,我们再添加学生这里我们得有两个函数,一个是输入学生信息到哈希表中,还有一个是保存到文件中
Add_stu()
1 | void Add_Stu(StudentDB *db, Student *stu) { |
在链式哈希表中,我们通常不会直接把 stu->stuid 和 stu->name 赋值给 db->key 和 db->val(正常的哈希表是key和val),因为每个桶实际上是一个链表的起点,链表中的每个节点都保存着一个键值对(在这里是 stuid 和其他学生信息)。链式哈希表通过为每个桶维护一个链表来解决哈希冲突,允许多个键值对存储在同一个桶中。
完整代码:
1 | //输入学生信息到文件里面 |
问题:为什么我们的哈希表和链表是共享的同一份数据还是把同一份输入,保存在两个部分,一部分用于快速查询,一部分用于增删改?
学生信息是在内存中通过指针建立了两种不同的组织结构:一种是以哈希表为基础,用于快速查找;另一种是以全局链表为基础,用于整体遍历。这两种结构共享同一批学生数据节点,从不同角度组织这些数据,以满足不同场景下的操作需求。
现在我们可以来写第二部分的代码了,将学生信息保存到文件中,用于本地保存。
save_students_to_file()
1 | //将学生信息保存到文件 |
查找学生信息
思路:查询模块应该有分为两种情况
我刚添加完数据,所有的数据都还在内存中,我可以直接通过哈希表来通过键值对查询
我添加完了数据,但是我退出了程序,内存中已经没有原来的数据了,我需要从文件中读取源数据,然后再做查询,这个时候应该可以通过链表或者其他思路来做。
也就是说我们这个函数需要再写两个情况函数,query_in_memory()和Load_From_File()
query_in_memory
我们先来写一下 query_in_memory 函数
1 | Student* Search_query_in_memory(StudentDB *db, int stuid) { |
首先我们是计算哈希值,然后我们要从哈希表对应的桶中取出链表头指针,指针指向链表的第一个节点。然后我们需要判断我们当前的这个学号是否是我们要查询的学号,如果不是就跳到下一个节点。如果全部遍历完了都没有找到,则说明没有要查询的学号。
search_student_in_file()
再来看看加载文件的思路是什么
首先我们要打开这个文件(fopen),然后判断我们这个文件是否是空的,非空的话我们就可以定义一个数组来保存每一行的学生信息了。我们将文件中的东西,逐行加载到哈希表中(fget),并且确保每次读取到的东西不超过缓存区的大小(避免缓冲区溢出)。创建一个实列用来保存数据(newstudent)并且动态分配内存,然后通过判断来看看我们输入的信息是否有 4 个信息(学号,名字,年龄,专业),满足则调用 Add_Stu(), 如果不满足则释放我们刚刚开辟的空间。最后读取完了就可以关闭这个文件了(fclose)
1 | //从文件加载学生信息到内存中的哈希表 |
Search_Stu()
ok 现在我们两个情况的代码都完成了,我们现在就可以来写最后的 search 模块了
模块思路:
因为用户添加完数据之后,可能习惯性直接就查询我们的用户有没有添加成功,所以可能添加完用户之后没有退出程序,我们就先看看能不能在内存中找到数据,没有的话在从文件中读取
1 | //综合查询 |
删除学生信息
删除学生信息也是分两种情况,内存中也文件中。也是通过哈希表进行删除操作。
在内存中的时候,我们直接使用 hash 表来删除,然后保存到文件。
remove_student_from_memory()
首先我们看到我们的哈希表结构,我们使用的是链式地址法来解决冲突问题,所以我们的每一个桶就是一个链表。所以我们在内存中的删除思路就是:首先通过学号计算哈希值定位到桶,然后在该桶的链表中进行删除。
1 | int remove_student_from_memory(StudentDB *db, int stuid) { |
我们首先还是计算哈希值,然后让 current 来获取哈希桶中的链表头节点,然后让 prev 来跟踪 current 的前一个链表节点,我的跟踪思路是一直循环遍历链表,直到为空,然后判断我们输入的学号是否匹配,匹配则让 prev 的 next 指向当前节点的 next(也就是 current->next) 然后我们在释放当前节点的内存空间以达到删除的效果。如果没有找到相对应的学号,我们就让 prev 来成为当前节点,current 移动到下一个节点的位置。
举一个实际例子来理解:
我有三个学号,101,102,103,我要删除 102,那代码第一次迭代的时候 current 指向的是 101,学号不匹配,则 prev 指向学号 101,然后让 current 来指向学好 102,第二次迭代,学号相匹配,然后就开始做删除操作。
prev
指针在遍历过程中始终跟随 current
指针,确保在找到目标节点时能够正确调整链表的指针结构。
remove_student_from_file(
由于我们之前的铺垫,我们的这个部分可以写的比较轻松了哈哈哈,按照最开始的删除思路,现在内存中查找,没有就加载文件到内存,然后再一次从内存做查找删除。
1 | // 删除学生信息(从文件) |
remove_student()
有了两个模块我们就可以直接写最终的删除功能了。
1 | // 删除学生信息 |
修改学生信息
思路:老样子,都是从内存和文件两个地方来做修改。
modify_student_in_memory()
我们通过创建了三个新的实例 new_age,new_name,new_major 来修改哈希表中的需要修改的部分。我们通过判断 student 是否为空,并且学号时候对应来做为修改的条件。
修改完之后提示是否修改成功,然后再将修改之后的内容更新到文件中。
1 | //修改学生信息 |
modify_student_in_file()
这里的代码思路其实和 remove_student_from_file()很像
1 | // 修改学生信息(从文件) |
这里就不过多讲述了,因为太相似了。
modify_student
这里我们会让修改单个部分,如果你觉得这个字段没有必要去改变的话,可以直接回车。这里主要讲解的地方是输入新信息部分:
我们会先显示当前学号的学生信息,你可以看看这个学生的那个部分是需要修改的,然后通过fgets来防止缓冲区溢出,通过sizeof来确保不会读取超过缓冲区的大小的内容。然后通过input, "\n")] = '\0'
来判断用户是否直接回车。*strcspn
*:会找到换行符位置并用\0
替换,去除输入中的换行符。最后再通过strncpy
来复制我们字符串。当我们把这个学生的信息都修改好之后,就要可以直接通过save_students_to_file()函数来保存到文件中了。
1 | // 修改学生信息(统一函数) |
显示所有学生信息
这个也可以分为两个情景来写,刚写完就查看和从文件中读取再查看
Show_All_Students_From_Memory()
1 | // 显示内存中的所有学生信息 |
Show_All_Students_From_Memory()
1 | // 显示文件中的所有学生信息 |
文件功能
Load_From_File()
1 | void Load_From_File(StudentDB *db, const char *filename) { |
save_to_file()
1 | //将学生信息保存到文件 |
其他操作
free_database()
1 | // 释放数据库内存 |
这个功能主要是当我们确定要退出程序的时候用来清除内存数值的,但是再这个操作之前,我们一般都会检查一下我们的数据是否已经保存到文件当中了。
完整代码:
1 |
|