1. 项目概述
这个学生成绩管理系统是我在课程设计期间完成的一个C++项目,使用EasyX图形库构建图形界面。系统实现了管理员、教师和学生三种角色的功能,包括用户管理、成绩录入、排序分页、数据导出等核心功能。整个开发过程历时两周,从最初的框架搭建到最终功能调通,期间遇到了不少技术难点,也积累了很多宝贵的经验。
1.1 核心功能解析
系统主要分为三个模块:
- 用户管理模块:管理员可以增删改查系统用户,设置用户角色(管理员/教师/学生)
- 成绩管理模块:教师可以管理学生信息(学号、姓名、班级、语文成绩、数学成绩)
- 查询统计模块:支持按总分排序、分页显示、班级平均分计算等功能
数据存储方面,系统使用二进制文件保存用户数据(user.bin)和学生数据(stu.bin),确保程序关闭后数据不会丢失。
2. 开发环境与技术选型
2.1 开发工具
- Visual Studio 2022:作为主开发环境,提供了强大的代码编辑和调试功能
- EasyX图形库:一个轻量级的Windows图形库,适合快速开发图形界面应用
- C++11标准:虽然主要使用C风格编程,但部分特性利用了C++11的便利
提示:EasyX安装非常简单,直接从官网下载安装包即可,它会自动集成到Visual Studio中。
2.2 技术选型考量
选择EasyX主要基于以下考虑:
- 轻量级,学习曲线平缓
- 纯C接口,与课程要求的C++基础契合度高
- 足够满足课程设计的图形界面需求
- 不需要复杂的配置和依赖管理
虽然EasyX的功能相对简单,但对于学生成绩管理系统这类应用已经足够。如果需要更复杂的界面效果,可以考虑Qt等更强大的框架,但学习成本也会相应提高。
3. 系统架构设计
3.1 项目目录结构
code复制├── main.cpp // 程序入口,负责页面切换
├── user.h / user.cpp // 登录验证功能
├── administrator.h / administrator.cpp // 管理员操作
├── teacher.h / teacher.cpp // 教师操作
├── tool.h / tool.cpp // 输入验证函数
├── myConst.h // 常量定义
├── button.h / button.cpp // 自绘按钮类
├── page.h / page.cpp // 页面基类及子页面
└── 数据文件 user.bin / stu.bin // 运行时自动生成
3.2 核心数据结构
系统主要使用了两种数据结构:
- 单向链表:用于管理用户数据
cpp复制typedef struct UserList {
User data;
struct UserList* next;
} UserList;
- 双向链表:用于管理学生数据
cpp复制typedef struct StudentListNode {
StudentMsg data;
struct StudentListNode* prev;
struct StudentListNode* next;
} StudentListNode;
选择双向链表管理学生数据主要是为了支持分页功能,可以方便地向前和向后遍历。
4. 关键功能实现
4.1 自定义按钮控件
由于EasyX没有提供现成的按钮控件,我实现了一个简单的Button类:
cpp复制class Button {
public:
Button(int x, int y, int width, int height, const char* text);
void drawButton();
bool checkButtonStatus(const ExMessage& msg);
private:
int m_x, m_y, m_width, m_height;
COLORREF m_normalColor;
char m_text[50];
};
这个类提供了基本的按钮绘制和点击检测功能。虽然最初计划实现悬停变色效果,但考虑到项目时间限制,最终简化了设计。
4.2 页面管理机制
系统采用基于继承的页面管理方式:
cpp复制class Page {
public:
virtual void drawPage() = 0;
virtual void handlePage(int& currentPage) = 0;
};
class LoginPage : public Page {
// 实现登录页面特定逻辑
};
主循环通过currentPage变量控制页面切换:
cpp复制while (1) {
switch (currentPage) {
case LOGIN_PAGE:
page[LOGIN_PAGE]->drawPage();
page[LOGIN_PAGE]->handlePage(currentPage);
break;
// 其他页面...
}
}
这种设计使得每个页面可以独立管理自己的布局和事件处理,代码结构清晰。
4.3 分页显示实现
分页功能是系统的核心难点之一,主要实现思路:
- 使用双向链表存储学生数据
- 维护当前页的起始和结束节点指针
- 提供四个分页操作函数:
- firstPrint:显示第一页
- frontPrint:显示上一页
- nextPrint:显示下一页
- jumpPage:跳转到指定页
关键代码片段:
cpp复制int firstPrint(StudentMsg* outData) {
if (!head || !outData) return 0;
currentPage = 1;
curPageStartNode = curPageEndNode = head->next;
int index = 0;
while (curPageEndNode && index < PAGE_SIZE) {
outData[index++] = curPageEndNode->data;
curPageEndNode = curPageEndNode->next;
}
return index;
}
4.4 排序算法实现
系统实现了按总分升序和降序排列的功能,采用冒泡排序算法直接操作链表节点:
cpp复制void bubbleSortUp() {
if (!head || !head->next) return;
StudentListNode* last = NULL;
bool isSwaped = false;
do {
isSwaped = false;
StudentListNode* prev = head->next;
StudentListNode* cur = prev->next;
while (cur != last) {
if (prev->data.allScore > cur->data.allScore) {
swap(prev, cur);
isSwaped = true;
cur = prev->next;
} else {
prev = cur;
cur = cur->next;
}
}
last = prev;
} while (isSwaped);
}
节点交换是排序的关键,需要小心处理四个指针:
cpp复制static void swap(StudentListNode* start, StudentListNode* cur) {
start->prev->next = cur;
if (cur->next) {
cur->next->prev = start;
}
start->next = cur->next;
cur->prev = start->prev;
cur->next = start;
start->prev = cur;
}
4.5 输入验证机制
为了防止用户输入无效数据,系统实现了一系列验证函数:
cpp复制bool getValidStuName(char* stuName, const char* prompt);
bool getValidUserId(char* userId, const char* prompt);
bool getValidUserPwd(char* userPwd, const char* prompt);
bool getValidClass(char* className, const char* prompt);
bool getValidRole(Role* outRole, const char* prompt);
float getScoreInRange(const char* prompt);
int getInt(int totalPage, const char* prompt);
其中,汉字验证是一个技术难点,需要处理GBK编码:
cpp复制static bool isGBKChinese(char* str, int idx) {
if (idx + 1 >= strlen(str)) return false;
unsigned char high = (unsigned char)str[idx];
unsigned char low = (unsigned char)str[idx + 1];
return (high >= 0xB0 && high <= 0xF7 && low >= 0xA1 && low <= 0xFE);
}
5. 数据持久化设计
5.1 二进制文件存储
系统使用二进制文件保存数据,读写效率高:
cpp复制bool saveStuMsgToBin() {
FILE* f = fopen("stu.bin", "wb");
if (!f) return false;
StudentListNode* cur = head->next;
while (cur) {
fwrite(&cur->data, sizeof(StudentMsg), 1, f);
cur = cur->next;
}
fclose(f);
return true;
}
5.2 文本导出功能
为了方便数据交换,系统支持将学生数据导出为文本文件:
cpp复制bool saveStuMsgToTxT() {
FILE* f = fopen("stu.txt", "w");
if (!f) return false;
fprintf(f, "+-----------------------+\n");
fprintf(f, "| id | 姓名 | 班级 | 语文 | 数学 | 总分 |\n");
fprintf(f, "|-----------------------|\n");
StudentListNode* cur = head->next;
while (cur) {
fprintf(f, "|%s|%s|%s|%.2f|%.2f|%.2f|\n",
cur->data.id, cur->data.name, cur->data.stuClass,
cur->data.chineseScore, cur->data.mathScore, cur->data.allScore);
cur = cur->next;
}
fclose(f);
return true;
}
6. 开发中的问题与解决方案
6.1 链表操作常见错误
- 删除节点时指针更新不全:
- 问题:删除中间节点时只更新了前驱节点的next指针,忘记更新后继节点的prev指针
- 解决:确保同时更新前后节点的指针
cpp复制// 错误代码
cur->prev->next = cur->next;
// 正确代码
cur->prev->next = cur->next;
if (cur->next) {
cur->next->prev = cur->prev;
}
- 排序后分页指针失效:
- 问题:排序改变了链表顺序,但分页指针仍指向原节点
- 解决:排序后重置分页指针
cpp复制void bubbleSortUp() {
// 排序逻辑...
curPageStartNode = curPageEndNode = NULL; // 重置分页指针
}
6.2 中文输入处理
- 问题:直接使用strlen计算包含中文的字符串长度不准确
- 解决:特殊处理GBK编码的中文字符
cpp复制int getRealLength(const char* str) {
int len = 0;
for (int i = 0; str[i]; ) {
if (isGBKChinese(str, i)) {
i += 2;
len += 2; // 一个汉字占2个位置
} else {
i++;
len++;
}
}
return len;
}
6.3 内存管理问题
- 问题:异常路径下可能出现内存泄漏
- 解决:使用RAII技术或确保所有路径都有正确的资源释放
cpp复制void destroyStuList() {
if (!head) return;
StudentListNode* cur = head->next;
while (cur) {
StudentListNode* temp = cur;
cur = cur->next;
free(temp);
}
free(head);
head = NULL;
}
7. 项目优化建议
虽然基本功能已经实现,但系统还有不少改进空间:
-
界面美化:
- 实现按钮的悬停和点击效果
- 添加更美观的布局和配色
-
安全性增强:
- 密码加密存储(如简单的异或加密)
- 增加登录尝试次数限制
-
功能扩展:
- 支持更多科目成绩
- 添加成绩统计分析图表
- 实现数据导入功能
-
代码优化:
- 使用模板减少重复的链表操作代码
- 引入更高效的数据结构(如跳表)
- 添加更完善的错误处理机制
-
性能改进:
- 对于大数据量,考虑使用数据库替代文件存储
- 实现懒加载优化分页性能
8. 开发经验总结
通过这个项目的开发,我获得了以下宝贵经验:
-
数据结构选择很重要:双向链表虽然比单向链表复杂,但对于需要双向遍历的场景非常有用。
-
边界条件测试必不可少:很多bug都出现在边界情况下,如空链表、第一页/最后一页等。
-
可视化调试很有帮助:在纸上画出链表结构的变化过程,能快速定位指针操作的问题。
-
代码重构要趁早:随着功能增加,及时清理无用代码和优化结构,避免技术债务积累。
-
用户输入永远不可信:完善的输入验证是健壮程序的基础。
这个项目让我深刻理解了数据结构在实际应用中的重要性,也锻炼了我的调试和问题解决能力。虽然EasyX的功能有限,但它确实是一个快速开发图形界面应用的好工具。