此前已经同组员们合作实现了一个较为简易的影院管理系统,积累了一些相关的经验,不至于独立上手制作学生管理系统时一头雾水。
也的确的使得所学习的内容具象化了,不是流于表面的一道道题,更重要的是提供了一个接口,一个浅浅地接触社会、工作中,所真实需求的技能,或者能力的接口。
抛砖引玉,第一次独立操作,很多方式都很老套简单,请多指教
一、开发环境:
开发工具:DEV C++ 5.11
开发语言:C语言
二、功能需求及说明:
数据录入:录入系统所需的数据信息。将录入的数据使用链表这一数据结构进行组织和管理。
数据存储:将录入的数据以文件的形式进行存储,优先推荐使用二进制文件格式。如果使用二进制文件存储,需要将数据按照一定的格式和结构转换为二进制形式,然后写入文件中。如果是文本文件存储,则需要将数据按照一定的文本格式(如JSON等)组织好,再写入文件中。
数据读写:能够从存储文件中读取数据,如果是二进制文件,可以直接按照数据的结构和格式从文件中读取二进制数据,并将其转换回相应的数据结构(如链表中的节点)。可以将数据写入到存储文件中,对于二进制文件,直接将数据以二进制形式写入;对于文本文件,则需要将数据按照文件的文本格式组织好后写入.
数据修改:可以对已存在的数据进行修改操作,即对链表中的某些节点数据进行更新。通过查找需要修改的数据节点,然后将其数据内容进行替换或更新,从而实现数据的修改。
数据插入:可以将新的数据插入到链表的任意指定位置。
数据删除:可以对已存在的数据进行删除操作。
数据查询:按要求对数据进行查找。
登录注册:将用户的账号和密码信息存储在文件中,用于实现用户的登录功能。在用户注册时,将新用户的账号和密码信息写入文件中进行存储;在用户登录时,从文件中读取账号密码信息,与用户输入的账号密码进行比对,从而实现用户的登录验证。
三、具体实现:
<基本框架>:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>
#define MAX_USERNAME_LENGTH 20
#define MAX_PASSWARD_LENGTH 20
#define MAX_NAME_LENGTH 20
#define MAX_CLASS_LENGTH 20
#define MAX_SCORES_LENGTH 50
// 细化超过限定范围数目情况
typedef struct UserNode
{
char username[MAX_USERNAME_LENGTH]; // 账号
char passward[MAX_PASSWARD_LENGTH]; // 密码
int role; // 身份:0-学生,1-教师,2-管理员
struct UserNode *next;
} UserNode;
typedef struct StudentNode
{
char name[MAX_NAME_LENGTH]; // 姓名
char Class[MAX_CLASS_LENGTH]; // 班级-涉及专业
int scoreCount; // 根据专业不同,科目亦不同
double scores[MAX_SCORES_LENGTH]; // 成绩
struct StudentNode *next;
} StudentNode;
// 初始各链表的头结点
UserNode *userList = NULL;
StudentNode *studentList = NULL;
// 加载用户数据
void loadUsersFromFile()
{
}
// 加载学生数据
void loadStudentsFromFile()
{
}
// 主界面
void mainMenu()
{
}
// 常规删除链表操作
void freeList()
{
}
int main()
{
// 事先加载所有信息,便于后续使用
// 实际为读取文件存入链表
loadUsersFromFile();
loadStudentsFromFile();
// 主页面
mainMenu();
// 释放内存
freeList();
return 0;
}
1. 相关load函数用于从文件中加载相关数据,并通过各自链表进行存储,以便于后续的增删改查操作
2. freeList用于在存储完成数据后释放内存,防止泄露
应当注意的是,由于个人实现以及操之过急,很多方面都没有来得及细化明确(后面不提了,过于追求完成而草率处理,羞愧)
例如对于存储的数组,若实际应用中用户传入过长的输入,应当进行相关的反馈处理;
再例如所谓的班级,能否通过事先进行相关班级及其对应的科目的初始化,确保输入的正确以及规范化合理化;
这都是在假定项目可以真实应用的情况下,需要进行的切实的思考,毕竟完工是一个很简单的事情,可现实工作并不只是要求完工就万事大吉。
(1)从文件中加载数据:
void loadUsersFromFile()
{
FILE *file = fopen("users.dat", "rb");
if (file == NULL)
{
return ;
}
// 常规尾插法,持续更新prev
UserNode *prev = NULL;
while (1)
{
UserNode *newNode = (UserNode*)malloc(sizeof(UserNode));
// 读取完毕
if (fread(newNode->username, sizeof(char), MAX_USERNAME_LENGTH, file) != MAX_USERNAME_LENGTH)
{
free(newNode);
break;
}
fread(newNode->passward, sizeof(char), MAX_PASSWARD_LENGTH, file);
fread(&newNode->role, sizeof(int), 1, file);
newNode->next = NULL;
if (userList == NULL)
{
userList = newNode;
}
else
{
prev->next = newNode;
}
prev = newNode;
}
fclose(file);
}
void loadStudentsFromFile()
{
// (如法炮制)
FILE *file = fopen("students.dat", "rb");
if (file == NULL)
{
return ;
}
StudentNode *prev = NULL;
while (1)
{
StudentNode *newNode = (StudentNode*)malloc(sizeof(StudentNode));
if (fread(newNode->name, sizeof(char), MAX_NAME_LENGTH, file) != MAX_NAME_LENGTH)
{
free(newNode);
break;
}
fread(newNode->Class, sizeof(char), MAX_CLASS_LENGTH, file);
fread(newNode->scores, sizeof(double), MAX_SCORES_LENGTH, file);
fread(&newNode->scoreCount, sizeof(int), 1, file);
newNode->next = NULL;
if (studentList == NULL)
{
studentList = newNode;
}
else
{
prev->next = newNode;
}
prev = newNode;
}
fclose(file);
}
从事实上讲,用户和学生没有本质区别,只是实现过程中的处理的数据不一样,流程基本一致。
1. 事先尝试打开用于存储的文件
显然,如果file为NULL,说明此文件目前没有被创建,可以直接返回,因为并不影响后续的使用
2. 而存储数据不同
存储时,由于相关文件无法创建打开,就会造成后续操作无法进行,因此需要进行相关的反馈报错处理
这里通过常规的尾插法进行数据存储,而当fread的姓名或者账号的长度不再是定义的最大长度时,就说明读取已经完结,或者干脆出错了,就应当停止了。(一定记得关闭文件)
(一个tip):
// 常规删除链表操作
void freeList()
{
UserNode *curr1 = userList;
while (curr1 != NULL)
{
UserNode *tmp1 = curr1;
curr1 = curr1->next;
free(tmp1);
}
StudentNode *curr2 = studentList;
while (curr2 != NULL)
{
StudentNode *tmp2 = curr2;
curr2 = curr2->next;
free(tmp2);
}
}
实际为一个简单的对单链表的删除操作
(2)主界面:
void mainMenu()
{
// 未退出则始终循环
int choice;
do
{
system("pause"); // 暂停清屏,一方面看起来方便简洁
system("cls"); // 另一方面,更加符合现实情况,后续亦会使用
printf("1. 注册\n");
printf("2. 登录\n");
printf("3. 修改密码\n");
printf("4. 退出\n");
printf("请选择: ");
scanf("%d", &choice);
// 根据选择进入不同函数
switch (choice)
{
case 1:
registerUser(); // 注册
break;
case 2:
{
// 不同返回值进入各自的界面
int role = loginUser(); // 登录
if (role != -1)
{
switch (role)
{
case 0:
studentQueryScores(); // 学生
break;
case 1:
teacherManageStudents(); // 教师
break;
case 2:
adminManage(); // 管理员
break;
}
}
break;
}
case 3:
changePassward(); // 修改密码
break;
case 4:
system("cls");
printf("退出系统\n");
system("pause");
break;
default:
printf("无效输入,请重试\n");
}
} while (choice != 4);
}
<相关核心函数的实现>:
// 保存数据
void saveUsersToFile()
{
}
void saveStudentsToFile()
{
}
void registerUser() // 注册
{
}
int loginUser() // 登录
{
}
void studentQueryScores() // 学生
{
}
void changePassward() // 修改密码
{
}
void teacherManageStudents() // 教师
{
}
void adminManage() // 管理员
{
}
(一个tip):
为确保数据的持续更新,每次增删改查等操作后,都会,且应当进行相关的文件保存操作,以保证对数据的更改能存入文件,而不滞后性
(1)数据保存:
void saveUsersToFile()
{
FILE *file = fopen("users.dat", "wb");
if (file == NULL)
{
printf("打开失败\n");
return ;
}
UserNode *curr = userList;
while (curr != NULL)
{
fwrite(curr->username, sizeof(char), MAX_USERNAME_LENGTH, file);
fwrite(curr->passward, sizeof(char), MAX_PASSWARD_LENGTH, file);
fwrite(&curr->role, sizeof(int), 1, file);
curr = curr->next;
}
fclose(file);
}
void saveStudentsToFile()
{
FILE *file = fopen("students.dat", "wb");
if (file == NULL)
{
printf("打开失败\n");
return ;
}
StudentNode * curr = studentList;
while (curr != NULL)
{
fwrite(curr->name, sizeof(char), MAX_NAME_LENGTH, file);
fwrite(curr->Class, sizeof(char), MAX_CLASS_LENGTH, file);
fwrite(&curr->scoreCount, sizeof(int), 1, file);
fwrite(curr->scores, sizeof(float), MAX_SCORES_LENGTH, file);
curr = curr->next;
}
fclose(file);
}
此刻若打开失败,应当进行反馈,因为我们的目的是写入文件,而非读取。
(2) 注册:
void registerUser()
{
UserNode *newUser = (UserNode*)malloc(sizeof(UserNode));
printf("输入用户名:");
scanf("%s", newUser->username);
// 遍历链表,确保账号首次注册
UserNode *curr = userList;
while (curr != NULL)
{
if (strcmp(curr->username, newUser->username) == 0)
{
printf("用户名已注册\n");
free(newUser);
return ;
}
curr = curr->next;
}
printf("输入密码:");
scanf("%s", newUser->passward);
// 身份验证细化,防止恶意注册
int role;
do
{
printf("输入身份:(0-学生,1-教师,2-管理员)");
scanf("%d", &role);
if (role < 0 || role > 2)
{
printf("无效输入\n");
}
// 循环保证数字输入正确
} while (role < 0 || role > 2);
newUser->role = role;
newUser->next = userList; // 接入链表
userList = newUser;
saveUsersToFile(); // 保存
printf("操作成功\n");
}
同样的,作为一个练习项目,它本身可以处理的草率,但还是从现实角度考虑:
如何进行能保证诸如:
1. 多次输入强制退出
2. 身份细化上,不会出现学生恶意注册管理员、教师的情况
(第一反应是进行类似验证码机制,比如为每个人或者每类人发布一串码,这个码具有某些性质,可以利用相关性质进行身份验证,只是在个人的实现上有些难以处理)
(3)登录:
int loginUser()
{
char username[MAX_USERNAME_LENGTH];
char passward[MAX_PASSWARD_LENGTH];
printf("输入账号:");
scanf("%s", username);
printf("输入密码:");
scanf("%s", passward);
UserNode *curr = userList;
while (curr != NULL) // 对比文件内的数据,是否存在账号密码完全对应的情况
{
if (strcmp(curr->username, username) == 0 &&
strcmp(curr->passward, passward) == 0)
{
printf("登陆成功\n");
return curr->role;
}
}
printf("账号密码有误\n");
return -1;
}
与其他函数不同,登录函数有着确定身份的作用,通过返回相关的role以保证进入界面不同
(4)相关的学生、教师、管理员界面:
1/ 学生:
void studentQueryScores()
{
char className[MAX_CLASS_LENGTH];
printf("输入班级:");
scanf("%s", className);
bool check = false; // 查看是否存在此班
StudentNode *curr = studentList;
while (curr != NULL)
{
if (strcmp(curr->Class, className) == 0)
{
check = true;
printf("姓名: %s\n", curr->name);
printf("成绩: \n");
for (int i = 0; i < curr->scoreCount; i ++)
{
printf("第 %d 个科目成绩为 %.2f\n", i + 1, curr->scores[i]);
}
printf("\n");
}
curr = curr->next;
}
if (check == false)
{
printf("查无此班\n");
system("pause");
}
}
该区块较为简单的实现了学生查看全班成绩的功能;
从事实上说,该区块可以结合对学生结点的细化来修改,即:是否可以通过事先声名好专业班级列表,改为选择而不是手动输入,以确保数据的规范化
(一个tip):
在教师和管理员的实现上,出于对视觉方面即可能因为函数过多带来不必要错误的考虑,最终选择了将各自模块写在了case内部,现实开发中应当避免这种行为。
2/ 教师:
void teacherManageStudents()
{
int choice;
do
{
system("pause");
system("cls");
printf("1. 添加学生信息\n");
printf("2. 删除学生信息\n");
printf("3. 修改学生信息\n");
printf("4. 查询学生信息\n");
printf("5. 查询班内成绩\n");
printf("6. 返回\n");
printf("请选择操作: ");
scanf("%d", &choice);
switch(choice)
{
case 1:
{
StudentNode *newStudent = (StudentNode*)malloc(sizeof(StudentNode));
if (newStudent == NULL)
{
printf("内存分配失败\n");
break;
}
printf("输入学生姓名: ");
scanf("%s", newStudent->name);
printf("输入学生班级: ");
scanf("%s", newStudent->Class);
printf("输入成绩数量: ");
scanf("%d", &newStudent->scoreCount);
for (int i = 0; i < newStudent->scoreCount; i++)
{
printf("输入第 %d 门成绩: ", i + 1);
scanf("%lf", &newStudent->scores[i]);
}
newStudent->next = studentList;
studentList = newStudent;
saveStudentsToFile();
printf("添加成功\n");
break;
}
case 2:
{
char name[MAX_NAME_LENGTH];
printf("输入要删除的学生姓名: ");
scanf("%s", name);
StudentNode *curr = studentList;
StudentNode *prev = NULL;
while (curr != NULL)
{
if (strcmp(curr->name, name) == 0)
{
if (prev == NULL)
{
studentList = curr->next;
}
else
{
prev->next = curr->next;
}
free(curr);
saveStudentsToFile();
printf("删除成功\n");
break;
}
prev = curr;
curr = curr->next;
}
break;
}
case 3:
{
char name[MAX_NAME_LENGTH];
printf("输入要修改的学生姓名: ");
scanf("%s", name);
StudentNode *curr = studentList;
while (curr != NULL)
{
if (strcmp(curr->name, name) == 0)
{
printf("输入新的学生姓名: ");
scanf("%s", curr->name);
printf("输入新的学生班级: ");
scanf("%s", curr->Class);
printf("输入新的成绩数量: ");
scanf("%d", &curr->scoreCount);
for (int i = 0; i < curr->scoreCount; i++)
{
printf("输入第 %d 门成绩: ", i + 1);
scanf("%lf", &curr->scores[i]);
}
saveStudentsToFile();
printf("修改成功\n");
break;
}
curr = curr->next;
}
break;
}
case 4:
{
char name[MAX_NAME_LENGTH];
printf("输入要查询的学生姓名: ");
scanf("%s", name);
StudentNode *curr = studentList;
while (curr != NULL)
{
if (strcmp(curr->name, name) == 0)
{
printf("姓名: %s\n", curr->name);
printf("班级: %s\n", curr->Class);
printf("成绩: ");
for (int i = 0; i < curr->scoreCount; i++)
{
printf("%.2f ", curr->scores[i]);
}
printf("\n");
break;
}
curr = curr->next;
}
break;
}
case 5:
{
char className[MAX_CLASS_LENGTH];
printf("输入班级名称: ");
scanf("%s", className);
StudentNode *curr = studentList;
while (curr != NULL)
{
if (strcmp(curr->Class, className) == 0)
{
printf("姓名: %s\n", curr->name);
printf("成绩: ");
for (int i = 0; i < curr->scoreCount; i++)
{
printf("%.2f ", curr->scores[i]);
}
printf("\n");
}
curr = curr->next;
}
break;
}
case 6:
printf("返回\n");
system("pause");
break;
default:
printf("无效输入\n");
}
} while (choice != 6);
}
3/ 管理员:
void adminManage()
{
int choice;
do
{
system("pause");
system("cls");
printf("1. 添加学生账号\n");
printf("2. 删除学生账号\n");
printf("3. 修改学生账号密码\n");
printf("4. 查询学生账号信息\n");
printf("5. 增删改查学生成绩\n");
printf("6. 导出学生数据\n");
printf("7. 返回\n");
printf("请选择操作: ");
scanf("%d", &choice);
switch (choice)
{
case 1: // 初始账号初始化
{
UserNode *newUser = (UserNode*)malloc(sizeof(UserNode));
if (newUser == NULL)
{
printf("内存分配失败");
break;
}
strcpy(newUser->username, "student");
int count = 0;
UserNode *curr = userList;
// 获取初始账号个数
while (curr != NULL)
{
if (strstr(curr->username, "student") != NULL)
{
count ++;
}
curr = curr->next;
}
sprintf(newUser->username + 7, "%d", count + 1);
strcpy(newUser->passward, "123456");
newUser->role = 0;
newUser->next = userList;
userList = newUser;
saveUsersToFile();
printf("初始账号添加成功\n用户名: %s,初始密码: 123456\n", newUser->username);
break;
}
case 2:
{
char username[MAX_USERNAME_LENGTH];
printf("输入要删除的学生名: ");
scanf("%s", username);
UserNode *curr = userList;
UserNode *prev = NULL;
while (curr != NULL)
{
if (strcmp(curr->username, username) == 0 && curr->role == 0)
{
if (prev == NULL)
{
userList = curr->next;
}
else
{
prev->next = curr->next;
}
free(curr);
saveUsersToFile();
printf("删除成功\n");
break;
}
prev = curr;
curr = curr->next;
}
break;
}
case 3:
{
char username[MAX_USERNAME_LENGTH];
char newPassward[MAX_PASSWARD_LENGTH];
printf("输入要修改的学生名: ");
scanf("%s", username);
UserNode *curr = userList;
while (curr != NULL)
{
if (strcmp(curr->username, username) == 0 && curr->role == 0)
{
printf("输入新密码: ");
scanf("%s", newPassward);
strcpy(curr->passward, newPassward);
saveUsersToFile();
printf("修改成功\n");
break;
}
curr = curr->next;
}
break;
}
case 4:
{
char username[MAX_USERNAME_LENGTH];
printf("输入要查询的学生名: ");
scanf("%s", username);
UserNode *curr = userList;
while (curr != NULL)
{
if (strcmp(curr->username, username) == 0 && curr->role == 0)
{
printf("用户名: %s,密码: %s\n", curr->username, curr->passward);
break;
}
curr = curr->next;
}
break;
}
case 5:
{
teacherManageStudents(); // 和教师相同功能
break;
}
case 6:
{
FILE *eFile = fopen("stuList.dat", "wb");
if (eFile == NULL)
{
break;
}
StudentNode *curr = studentList;
while (curr != NULL)
{
fwrite(curr->name, sizeof(char), MAX_NAME_LENGTH, eFile);
fwrite(curr->Class, sizeof(char), MAX_CLASS_LENGTH, eFile);
fwrite(&curr->scoreCount, sizeof(int), 1, eFile);
fwrite(curr->scores, sizeof(float), MAX_SCORES_LENGTH, eFile);
curr = curr->next;
}
fclose(eFile);
printf("已导出\n");
break;
}
case 7:
printf("返回\n");
system("pause");
break;
default:
printf("无效输入\n");
}
} while (choice != 7);
}
模拟实现管理员设置初始账号,并发给教师进行细化修改的情况,即:
1. 建立student n (n为序号数字)账号,并初始化诸如123456的密码
(sprintf大法好)2. (一系列的增删改查)
3. 导出文件,类似备份的操作
(5)修改密码:
void changePassward()
{
char username[MAX_USERNAME_LENGTH];
char oldPassward[MAX_PASSWARD_LENGTH];
char newPassward[MAX_PASSWARD_LENGTH];
printf("输入用户名:");
scanf("%s", username);
printf("输入旧密码:");
scanf("%s", oldPassward);
UserNode *curr = userList;
while (curr != NULL)
{
if (strcmp(curr->username, username) == 0 &&
strcmp(curr->passward, oldPassward) == 0)
{
printf("请输入新密码:");
scanf("%s", newPassward);
strcpy(curr->passward, newPassward);
printf("修改成功\n");
return ;
}
curr = curr->next;
}
printf("账号密码有误\n");
}
同样的,遍历寻找oldPassward和账号完全匹配的用户,这里依旧可以细化处理输入新密码时,格式和长度的问题
四、总结:
虽然事前已经通过课设简单的完成过部分TTMS的模块,真正的到手上手自己独立的小项目,哪怕很多部分都很像,依旧感到有些困难。
不过即使过程中很困难,做出一个哪怕这么很多方面有些草率的成品后,还是会有一些成就感,真正的感受的自己学的东西到底可以用来做什么,而不是考个试做个题这么简单。
项目实现简单,请多指教!orz