资讯详情

资讯详情

C++结合SQLite构建学生信息管理系统:完整工程与答辩实战解析

简介这是一套用C结合数据库技术实现的学生信息管理系统适用于本科毕设、课程设计以及需要快速上手完整学籍管理项目的开发者。资源从C核心逻辑、关系型数据库表结构与SQL操作到基于Qt或wxWidgets的前端交互和登录注册、信息查询、成绩管理等模块均有覆盖能帮助读者理解面向对象编程、第三范式数据库设计及系统模块化开发流程。压缩包共146个文件以63个h头文件、62个cpp源文件为主另含icone图标、项目工程配置与mdb数据库文件等整体大小3.76MB目录结构便于对照代码与工程配置进行学习。已有1414人学习下载适合希望获得可运行参考实现并在其基础上扩展功能的C学习者。1. 学生信息管理系统C 配 SQLite一份能直接跑进答辩现场的完整工程学生信息管理系统是 C 课设里出场率最高的题目之一但大多数版本止步于「能演示」数据放在内存链表里程序一关全部归零。上学期帮某高校一位学弟调试课设他交的初版就是六百行代码塞在单个 main.cpp 里连数据存哪都说不清。后来我按项目标准重搭了一版带数据库持久化的学生信息管理系统他直接拿去答辩从演示到讲解一气呵成。这份资源就是那套完整工程C 写的控制台程序内嵌 SQLite 数据库覆盖学生、课程、成绩三大实体的增删改查、条件筛选、成绩统计排序以及菜单驱动的交互界面。适合三类人正在准备 C 课设的学生、想搞清楚 C 怎么连数据库的开发者、想找结构清晰的小工程做参考的人。下面从架构、核心代码、编译运行到二次开发一条线拆完。2. 架构与目录结构一个请求从菜单输入到数据库落盘的完整路径2.1 技术选型为什么是这个组合写课设总有人纠结C 配数据库选 MySQL 还是 SQLite我的建议很直接——课程设计优先选 SQLite。原因有三个一是免安装SQLite 是嵌入式数据库只是一个动态库加一个 .db 文件不需要单独装服务端也不用配账户密码拿到代码编译完就能跑答辩现场换台机器也不怕二是数据落盘是单个文件拷走即备份对「演示完要交作业」这种场景非常友好三是 C 语言 API 足够底层能让你把 prepare、bind、step、finalize 这一套完整走一遍反而比直接用 ORM 更容易在答辩时把原理讲清楚。那什么时候才上 MySQL如果题目明确要求「客户端/服务端架构」或者要演示多用户并发写入再换 MySQL 不迟。这套工程的 db.cpp 把连接和操作封装成了独立函数真到了要换库那一步只需要改 db.cpp 这一个文件上层业务代码完全不用动。这就是分层的好处——数据库细节被挡在接口后面业务逻辑不感知底层实现。C 标准方面工程按 C17 编写主要用到 std::string、std::vector、std::function 这些常规设施没有引入 boost 之类的重依赖。编译只需要 g 加 SQLite 的开发库Windows 下用 MinGW 或者 Visual Studio 都行跨平台性在课设这个体量里算很好了。2.2 目录结构源码、头文件、数据文件的分层拿到源码先别急着编译先把目录结构看清楚。整个工程按 include / src / data 三层拆分头文件和实现分离业务模块各占一个文件路径职责include/db.h数据库连接与初始化接口include/student.h学生实体与增删改查接口include/course.h课程实体与增删改查接口include/score.h成绩实体与跨表查询接口include/menu.h菜单循环与功能分发声明src/db.cppSQLite 连接、建表、错误处理src/student.cpp学生模块实现src/course.cpp课程模块实现src/score.cpp成绩模块实现src/menu.cpp菜单循环与分发逻辑src/main.cpp程序入口初始化和启动菜单data/运行时生成的 student.db 所在目录Makefile一键构建脚本这样的拆分主要是为了让每一层职责单一。menu.cpp 只管「用户按了哪个键、该调哪个模块」student.cpp / course.cpp / score.cpp 只管「某个业务动作怎么翻译成 SQL」db.cpp 只管「怎么和 SQLite 对话」。答辩时被问到「系统怎么设计」从这个目录结构讲起比从代码细节讲起要清楚得多也更容易拿高分。2.3 数据表设计三张表把学生、课程、成绩串起来数据库部分一共三张表分别对应三个核心实体。学生表 student 存基本信息课程表 course 存开课信息成绩表 score 作为关联表用外键把学生和课程连起来。这样设计的原因很实际如果不拆表把课程名和分数直接塞进学生表一个学生选三门课就得重复三行学生信息改个电话要改三处数据冗余和一致性风险全都上来了。建表语句长这样CREATE TABLE IF NOT EXISTS student ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, gender TEXT CHECK(gender IN (男,女)), age INTEGER CHECK(age 0 AND age 100), major TEXT, class_name TEXT, phone TEXT UNIQUE ); CREATE TABLE IF NOT EXISTS course ( course_id INTEGER PRIMARY KEY AUTOINCREMENT, course_name TEXT NOT NULL, credit REAL CHECK(credit 0), teacher TEXT, semester TEXT ); CREATE TABLE IF NOT EXISTS score ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, score REAL CHECK(score 0 AND score 100), exam_date TEXT, FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) );参数说明学生表的 phone 字段加了 UNIQUE 约束防止同一个手机号登记两次score 表用 student_id 加 course_id 两个外键做关联再配一个自增 id 做主键是为了支持一个学生同一门课补考两次的情况补考记录作为独立行存在不覆盖原记录。CHECK 约束在数据库层兜底了最基础的数据校验——性别只能是男或女成绩必须在 0 到 100 之间比在 C 代码里用 if 判断可靠得多也省得每次插入都要写一遍校验逻辑。为什么用 AUTOINCREMENT 而不是手动传 id因为程序里用户不应该感知 id 的存在插入时只传业务字段主键交给数据库自动生成。这在批量导入数据时尤其省心不用先查一次「当前最大 id 是多少」。查询侧student.id 和 score.student_id 这种高频关联字段就是天然索引SQLite 会自动为主键建索引直接 join 就行不用额外操心。提示如果要支持「一个学生同一门课只允许一条成绩」这种规则把 UNIQUE(student_id, course_id) 加在 score 表上即可这是后续改需求时最常动的地方。3. 核心模块拆解增删改查背后那些不能含糊的实现细节3.1 数据库初始化连接、建表、错误处理一套走完先看 db.cpp这是整个工程的地基。程序启动时 main.cpp 第一件事就是调用 db_init它负责打开数据库文件、建表、开启关键参数任何一步失败都直接返回 false 退出。// src/db.cpp #include db.h #include sqlite3.h #include iostream sqlite3* g_db nullptr; bool db_init(const char* db_path) { if (sqlite3_open(db_path, g_db) ! SQLITE_OK) { std::cerr 打开数据库失败: sqlite3_errmsg(g_db) std::endl; return false; } // 开启外键约束SQLite 默认关闭不开启则级联保护不生效 sqlite3_exec(g_db, PRAGMA foreign_keys ON;, nullptr, nullptr, nullptr); const char* ddl R( CREATE TABLE IF NOT EXISTS student ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, gender TEXT CHECK(gender IN (男,女)), age INTEGER CHECK(age 0 AND age 100), major TEXT, class_name TEXT, phone TEXT UNIQUE ); CREATE TABLE IF NOT EXISTS course ( course_id INTEGER PRIMARY KEY AUTOINCREMENT, course_name TEXT NOT NULL, credit REAL, teacher TEXT, semester TEXT ); CREATE TABLE IF NOT EXISTS score ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER NOT NULL, course_id INTEGER NOT NULL, score REAL CHECK(score 0 AND score 100), exam_date TEXT, FOREIGN KEY (student_id) REFERENCES student(id), FOREIGN KEY (course_id) REFERENCES course(id) ); ); char* err_msg nullptr; if (sqlite3_exec(g_db, ddl, nullptr, nullptr, err_msg) ! SQLITE_OK) { std::cerr 建表失败: err_msg std::endl; sqlite3_free(err_msg); return false; } return true; }逻辑说明sqlite3_open 成功后会返回一个数据库句柄它贯穿整个程序生命周期后面所有模块的查询都依赖这个全局指针。PRAGMA foreign_keys ON 这行特别容易被漏掉——SQLite 的外键约束默认是关闭的不开启的话 score 表的外键形同虚设删学生时不会触发任何约束保护成绩数据就成了悬空引用。参数说明db_init 接收一个路径字符串调用时传 data/student.db。sqlite3_open 有个特性如果文件不存在它会在该路径创建空库但前提是所在目录必须存在它不会自动帮你建 data 目录。这一点需要在 main.cpp 里用 std::filesystem::create_directories 提前创建也是新手最容易踩的第一个坑。3.2 学生信息增删改查预处理语句与输入校验学生模块是全部业务的基础增删改查四个动作分别对应四个函数。这里以新增为例重点讲预处理语句的用法。// src/student.cpp —— 新增学生 bool student_add(const Student stu) { const char* sql INSERT INTO student(name, gender, age, major, class_name, phone) VALUES(?, ?, ?, ?, ?, ?); sqlite3_stmt* stmt nullptr; if (sqlite3_prepare_v2(g_db, sql, -1, stmt, nullptr) ! SQLITE_OK) { std::cerr SQL 预处理失败: sqlite3_errmsg(g_db) std::endl; return false; } sqlite3_bind_text(stmt, 1, stu.name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 2, stu.gender.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_int(stmt, 3, stu.age); sqlite3_bind_text(stmt, 4, stu.major.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 5, stu.class_name.c_str(), -1, SQLITE_TRANSIENT); sqlite3_bind_text(stmt, 6, stu.phone.c_str(), -1, SQLITE_TRANSIENT); bool ok (sqlite3_step(stmt) SQLITE_DONE); if (!ok) { std::cerr 插入失败: sqlite3_errmsg(g_db) std::endl; } sqlite3_finalize(stmt); return ok; }逻辑说明prepare_v2 把带 ? 占位符的 SQL 编译成语句对象之后再用 bind 系列函数绑定参数。这样做的第一个好处是防注入——如果直接拼字符串用户输入一段恶意 SQL 就能把表删了用占位符则输入永远被当成数据而不是命令第二个好处是同一句 SQL 可以只 prepare 一次、绑定多次批量插入时性能差别很大。SQLITE_TRANSIENT 表示 SQLite 会在绑定瞬间拷贝字符串内容不用担心传入的 std::string 生命周期结束导致悬空指针。参数说明bind 函数的序号从 1 开始和 SQL 里 ? 从左到右的顺序一一对应。绑定类型必须和表字段类型匹配age 是 INTEGER 就用 bind_intscore 是 REAL 就用 bind_double字符串一律 bind_text。一个常见误用是把 int 用 bind_text 绑进去SQLite 虽然会做类型转换但转换失败时不会报错而是静默存成 NULL后面查出来全是空值排查起来很头疼。修改操作套路相同只是 SQL 换成 UPDATE 加 WHERE id ?。需要注意的点是UPDATE 之前最好先查一次该 id 是否存在否则 sqlite3_step 返回 SQLITE_DONE但实际影响行数为 0用户会以为改成功了其实什么都没发生。我一般在执行后调用 sqlite3_changes(g_db) 检查受影响行数为 0 就提示「没有找到该学号」这个细节很能体现代码的完成度。3.3 成绩统计与跨表查询JOIN 和聚合函数的落地成绩模块是这套系统里最有技术含量的一块涉及跨表查询和聚合统计不再是无脑的单表操作。查某个学生的成绩单时score 表里只有 student_id 和 course_id必须 join 两张表才能拼出「张三 数学 95」这种可读结果。// src/score.cpp —— 查询指定学生的成绩单 void score_query_by_student(int student_id) { const char* sql R( SELECT s.name, c.course_name, sc.score, sc.exam_date FROM score sc JOIN student s ON sc.student_id s.id JOIN course c ON sc.course_id c.course_id WHERE sc.student_id ? ORDER BY sc.score DESC ); sqlite3_stmt* stmt nullptr; if (sqlite3_prepare_v2(g_db, sql, -1, stmt, nullptr) ! SQLITE_OK) { std::cerr 查询失败: sqlite3_errmsg(g_db) std::endl; return; } sqlite3_bind_int(stmt, 1, student_id); printf(%-10s %-20s %-6s %s\n, 姓名, 课程, 分数, 考试日期); while (sqlite3_step(stmt) SQLITE_ROW) { const char* name (const char*)sqlite3_column_text(stmt, 0); const char* course (const char*)sqlite3_column_text(stmt, 1); double score sqlite3_column_double(stmt, 2); const char* date (const char*)sqlite3_column_text(stmt, 3); printf(%-10s %-20s %-6.1f %s\n, name, course, score, date); } sqlite3_finalize(stmt); }逻辑说明JOIN 的连接条件写在 ON 后面score 表的 student_id 关联 student 的 idcourse_id 关联 course 的 course_idWHERE 再按传入的学号过滤ORDER BY score DESC 实现分数从高到低排序。sqlite3_step 每调用一次返回一行返回 SQLITE_ROW 就取一列循环到返回 SQLITE_DONE 表示取完最后 finalize 释放语句对象。这个「prepare → bind → step 循环 → finalize」四步曲是 SQLite C API 的核心套路这套工程里所有查询都是这个模板。统计功能则交给 SQL 聚合函数。比如算每门课的选课人数、平均分、最高最低分SELECT c.course_name, COUNT(sc.id) AS 人数, AVG(sc.score) AS 平均分, MAX(sc.score) AS 最高分, MIN(sc.score) AS 最低分 FROM score sc JOIN course c ON sc.course_id c.course_id GROUP BY c.course_id;这条 SQL 在 C 里的执行过程和上面完全一样只是结果列从具体数值换成了聚合值while 循环里用 sqlite3_column_text / sqlite3_column_double 取出来打印。把统计逻辑写进 SQL 而不是拉回内存里用 C 算数据量小的时候看不出差别数据量一大差距就明显了——数据库的索引和聚合是优化过的自己在内存里排序纯属重复造轮子还容易写出 O(n²) 的排序。删除学生时的事务处理也是关键。直接 DELETE FROM student 会被外键约束拦下正确做法是先删关联成绩再删学生两步包在事务里bool student_delete(int id) { sqlite3_exec(g_db, BEGIN;, nullptr, nullptr, nullptr); const char* sql1 DELETE FROM score WHERE student_id ?; const char* sql2 DELETE FROM student WHERE id ?; // 依次执行 sql1、sql2任何一步失败就 ROLLBACK if (!exec_stmt(sql1, id) || !exec_stmt(sql2, id)) { sqlite3_exec(g_db, ROLLBACK;, nullptr, nullptr, nullptr); return false; } sqlite3_exec(g_db, COMMIT;, nullptr, nullptr, nullptr); return true; }逻辑说明BEGIN 开启事务后两条 DELETE 要么都成功要么都回滚。如果不包事务可能出现成绩删了学生没删掉的情况数据处于半更新状态。这里把 prepare-bind-step-finalize 封装成了 exec_stmt业务层只传 SQL 和参数也是后面避坑章节要展开的一个习惯。注意外键约束报错不是 bug是保护机制在工作。别为了「省事」把 PRAGMA foreign_keys 关掉正确做法是先删子表记录再删主表记录。4. 编译运行全流程从源码到菜单跑起来的十五分钟4.1 环境准备编译器与 SQLite 依赖编译这套工程需要两样东西一个支持 C17 的编译器、一个 SQLite 开发库。Linux / macOS 上随手装Debian/Ubuntu 系执行 apt install libsqlite3-devmacOS 用 brew install sqlite3编译器用系统自带的 g / clang 即可。Windows 上有两条路线一是 MinGW-w64 加 sqlite3 的 dev 包二是 Visual Studio 里把 sqlite3.c 和 sqlite3.h 直接拖进工程编译。我的建议是能用 Linux 就用 Linux少踩一半的坑。别的不说光是控制台中文编码在 Windows 下就要折腾一阵这个在避坑章节会细讲。装好之后先验证环境g --version ls /usr/include/sqlite3.h第一条命令确认编译器版本第二条确认 SQLite 头文件在位。如果 ls 报错说明开发库没装全装完再试一次。这一步花两分钟能省掉后面编译报错时的半小时排查。4.2 编译命令与 Makefile一条命令构建整个工程源码是多个文件不能指望 g 一个文件搞定。手动编译需要把所有 .cpp 一次性传给编译器链接阶段再把 SQLite 库挂上g -stdc17 -Wall -Iinclude \ src/main.cpp src/db.cpp src/student.cpp src/course.cpp src/score.cpp src/menu.cpp \ -lsqlite3 -o studentms参数说明-Iinclude 告诉编译器头文件在 include 目录-lsqlite3 链接 SQLite 动态库注意这个参数必须放在源文件后面因为链接器按顺序解析符号-Wall 开启常见警告建议保留编译期能拦下一批隐蔽的类型问题。-o studentms 指定输出文件名Linux / macOS 下是带可执行权限的二进制Windows 下自动生成 studentms.exe。每次都敲这么长一串很累所以工程里带了一个 MakefileCXX g CXXFLAGS -stdc17 -Wall -Iinclude LDFLAGS -lsqlite3 SRCS src/main.cpp src/db.cpp src/student.cpp src/course.cpp src/score.cpp src/menu.cpp OBJS $(SRCS:.cpp.o) TARGET studentms $(TARGET): $(OBJS) $(CXX) $(OBJS) -o $(TARGET) $(LDFLAGS) %.o: %.cpp $(CXX) $(CXXFLAGS) -c $ -o $ clean: rm -f $(OBJS) $(TARGET)之后构建就一行 make清理就一行 make clean。Makefile 会自动判断哪些 .cpp 改过、只重新编译对应的 .o 文件改一个模块不用全量重编。第一次跑 make 如果报 undefined reference to sqlite3_open九成是 -lsqlite3 没加上或者开发库没装回到 4.1 检查即可。4.3 运行与菜单操作把整套流程走一遍构建成功后运行 ./studentms首先看到的是主菜单 学生信息管理系统 1. 添加学生 2. 修改学生信息 3. 删除学生 4. 查询学生 5. 浏览全部学生 6. 添加课程 7. 录入成绩 8. 查询成绩单 9. 成绩统计 0. 退出 请选择:菜单实现是一个 while 循环加 switch 分发每次读取用户输入的数字对应调用 student_add / student_update / score_query 等函数函数执行完提示按任意键回菜单选择 0 时 break 退出循环并调用 sqlite3_close 关闭数据库连接。这个「循环 switch」的模式虽然简单但负责分发的主循环和真正干活的业务函数是分离的后续要加菜单项只需在 switch 里加一个 case不用动任何业务代码。建议第一次跑的时候按这个顺序验证先添加三个学生再添加两门课然后给每个学生录两三条成绩最后分别看「查询单个学生成绩单」和「成绩统计」。这样走一遍增删改查、外键关联、聚合查询就全验证过了。中途故意留一个学生不录成绩再去删他会看到外键约束报错——这不是 bug是保护机制在工作正好验证 3.3 里的事务删除逻辑是对的。5. 常见问题与避坑编译链接、中文乱码、路径依赖的翻车现场5.1 链接报错 undefined reference to sqlite3_open现象编译阶段 .cpp 全部通过最后链接时报一堆 undefined reference to sqlite3_open / sqlite3_prepare_v2程序出不来。原因编译器找到了头文件所以语法检查过了但链接阶段找不到 SQLite 的库实现。常见两种-lsqlite3 没写在编译命令里或者写在了源文件之前——链接器按顺序解析库必须放在对象文件后面另一种是 Linux 下只装了运行时库没装 libsqlite3-dev 开发包缺头文件和链接用的 .so。解决按「源文件 → 库」的顺序组织命令-lsqlite3 放到最后Debian/Ubuntu 执行 sudo apt install libsqlite3-dev 后重编。查库是否可见用 ldconfig -p | grep sqlite3能看到输出就说明库在系统里问题只出在链接顺序上。5.2 控制台中文乱码现象程序跑起来菜单和提示信息全是乱码英文和数字正常中文全变成「銆?鏂?」之类的符号。原因源码文件是 UTF-8 编码Linux 终端默认 UTF-8本不该乱但在 Windows 控制台默认代码页是 GBK936UTF-8 的中文字节被按 GBK 解码自然乱成一团。这是 C 控制台程序在 Windows 下最常见的翻车点几乎每个接手的人都会撞上一次。解决Windows 下在 main.cpp 最前面加一句 SetConsoleOutputCP(CP_UTF8)并 include windows.h让控制台输出代码页切到 UTF-8。Linux / macOS 不需要这一步可以用条件编译包一下#ifdef _WIN32 #include windows.h #endif int main() { #ifdef _WIN32 SetConsoleOutputCP(CP_UTF8); #endif // ... }这样跨平台编译不会报错Windows 下运行也不乱码。血的教训是别在源码里把中文改成 GBK 编码来迁就控制台那样换到 Linux 上就全乱了正确做法是统一 UTF-8 源码加运行时切代码页。5.3 数据库文件路径找不到现象程序第一次跑正常换一个工作目录启动报「打开数据库失败: unable to open database file」。原因db_init 用的是相对路径 data/student.db。Linux 下相对路径是相对于「当前工作目录」而不是「可执行文件所在目录」。你 cd 到别的目录再执行 ../build/studentms数据文件就写到当前目录的 data/ 下了如果那里没有 data 目录SQLite 直接打不开。很多人把锅甩给 SQLite其实是路径语义没搞清。解决两个方案。一是约定始终在工程根目录运行简单但不防手滑二是在 main.cpp 里用 std::filesystem::create_directories(data) 先确保目录存在再调 db_init。更稳妥的做法是把路径改成相对可执行文件的绝对路径用 argv[0] 推导所在目录再拼上 data/student.db这套工程为了保持课设的简单性用了方案二够用。5.4 删除学生报外键约束错误现象删除一个已经录过成绩的学生程序报 SQLITE_CONSTRAINT删除失败界面只显示一句「数据库错误」。原因score 表的外键引用了 student.id而 db_init 里显式开启了 PRAGMA foreign_keys ON。开启后删除被引用的学生行会被数据库拒绝这是完整性保护——你删了学生成绩表里就出现指向不存在记录的悬空引用。显示层没做错误分类把约束错误和其他 SQL 错误混在一起提示用户根本看不懂。解决两层都要改。业务层按 3.3 的做法先删 score 里该学生的所有成绩再删 student包事务显示层把 sqlite3_errcode 的返回值分类SQLITE_CONSTRAINT 单独提示「该学生存在成绩记录请确认是否连同成绩一起删除」而不是笼统报数据库错误。这个分类提示在答辩时很加分说明你理解数据库约束的含义。5.5 查询结果重复或者内存持续上涨现象同一个查询在循环里多次执行程序内存一路涨或者第二次查询结果用了上一次的参数数据出现奇怪重复。原因sqlite3_stmt 每次 prepare 之后没有 finalize语句对象和内部缓存一直占着内存。循环里反复 prepare 而不释放就是教科书级的内存泄漏。另一种情况是复用同一个 stmt 时没重置绑定值直接 bind 新值就 step导致参数混乱。解决养成「prepare 和 finalize 成对出现」的习惯复用语句时用 sqlite3_reset 加 sqlite3_clear_bindings 重置。我一般在工程里把 prepare-bind-step-finalize 封装成一个模板函数业务代码里只传 SQL 和参数从根上杜绝忘了释放的可能。踩过一次内存泄漏的坑之后我就不再手写裸的 sqlite3 调用序列了这个封装习惯一直带到了后面的项目里。6. 二次开发改造从课设到能拿去用的系统还差几步6.1 改造方向先把菜单分发换成函数指针表拿到这套工程第一件值得做的事是把 menu.cpp 里的 switch 分发改成函数指针表。现在十个 case 还能撑住再加功能 switch 会越来越长而且每个 case 都要写重复的「调用函数、等待按键、清屏」三段式代码。用 std::vectorstd::functionvoid() 存菜单项每个菜单项是一个 lambda遍历打印、按索引调用std::vectorstd::functionvoid() items { [] { student_add(); }, [] { student_update(); }, [] { student_delete(); }, [] { score_query(); } };新增菜单只需要往 vector 里 push_back 一个 lambda不用碰任何已有代码。这个改动半小时能完成但对后续扩展的价值是实打实的也是从「课设代码」向「可维护工程」过渡的第一步。6.2 加登录鉴权密码别存明文用户管理是「能拿去用」的第一道门槛。建一张 user 表id, username, password_hash, role密码不能存明文至少用 SHA-256 哈希后落库。C 里可以用 OpenSSL 的 EVP 接口算哈希登录时把输入密码哈希后和库里比对。role 字段区分管理员和普通用户管理员能增删改普通用户只读。改动涉及 db.cpp 加表、新增 auth.cpp、menu 加登录入口整体不超过两天工作量。别看它简单这是把系统从「能跑」变成「敢用」的分水岭。6.3 导出 CSV让数据能交出去答辩时老师常问「数据怎么导出来」。加一个导出功能查询全部学生成绩按 CSV 格式写到文件。字段用逗号分隔记录用换行分隔文件开头写 UTF-8 BOM否则表格软件打开会乱码。核心代码也就二十来行std::ofstream out(export.csv); out \xEF\xBB\xBF; // UTF-8 BOM表格软件识别中文的关键 out 姓名,课程,分数,考试日期\n; // 复用 3.3 的成绩查询把 printf 换成 out 写入 out.close();这个功能虽然小但把「系统里的数据能带走」这件事解决了。很多课设就差这么一点「闭环感」——数据录进去能查、能算、能导出整个链路完整了答辩时的说服力完全不一样。最后的建议改完任何功能先把 4.3 的完整流程跑一遍验证基线再补一个异常场景测试比如输入超长姓名、删除不存在的学生、重复导入同一个手机号。我从那次陪学弟调试到凌晨之后每次接手这类工程都强制自己先跑通原始版本、记录基线行为再动手改代码——改挂了至少知道该回滚到哪。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →