☰
C++课程设计学生选课系统:面向对象增删改查与文件持久化实战
2026/10/28 4:28:15 网站建设 项目流程

简介:这份面向计算机科学与技术专业学生的C++课程设计文档,围绕学生选修课程系统展开,适合正在完成面向对象课程设计、需要参考完整项目流程与报告写法的学习者。内容以青岛理工大学课程设计报告为样例,覆盖需求分析、模块图与流程图设计,以及学生登录、课程浏览、选课与退选、冲突检查、个人信息管理、成绩查询和数据统计等功能设计;同时给出用户认证、课程信息管理、选课逻辑与数据持久化等关键代码思路,并包含调试分析、上机问题与解决方案、可改进和可扩展功能、用户手册及设计总结,能帮助读者理解从需求到实现再到测试文档的完整链路。资源包为1个doc文件,约259KB,便于直接查阅和整理报告结构。已有1442人学习,适合需要搭建选课系统框架、补齐课程设计文档与排错思路的读者参考。

1. 从机房报告到可运行系统:c++课程设计里最容易被低估的选课项目

学生选修课程系统设计 c++ 是 c++课程设计 里很典型、也很容易做成“报告画得很满、代码只跑到菜单”的题目。这个资源把教师端维护学生和课程、学生端浏览课程并选课、对象数组增删改查、文件读写和选课统计都落到了代码里,技术栈就是标准 C++ 面向对象加控制台菜单,配 fstream 文本文件和一段二进制选课记录。它适合刚接触课程设计、想照着跑通一套完整增删改查闭环的新手,也适合已经写过课设、想看看类职责怎么拆、文件持久化怎么不翻车的人。别拿企业级教务系统的标准要求它,它的价值在流程完整、代码能对照、改起来有抓手。

2. 整体骨架拆解:四个类、两个端点、三份数据文件怎么串起来

2.1 目录与运行环境:CodeBlocks 下一个控制台工程的最小组织

这个项目原始形态更接近单文件课设:类定义、功能函数和主菜单都放在一个 cpp 里,运行时依赖当前目录下的数据文件。先在单文件形态下跑通,比一上来拆成十几个文件更稳,因为文件路径、输入输出格式、菜单分支都还没验证,拆早了只会把问题摊薄。运行环境用常见的 C++ IDE 即可,原文环境是 CodeBlocks;命令行编译也可以,重点是编译出来的可执行文件要和数据文件放在同一个工作目录。

文件/目录作用运行注意
main.cpp放 student、course、xuanke、manage 以及 main 菜单先保证能独立编译
course_information.txt课程记录,空格分隔路径相对当前工作目录
student_information.txt学生记录,空格分隔字符数组字段不能带空格
xuanke_information.txt选课记录,二进制追加统计时按 sizeof(xuanke) 计算条数
拆分后的 student.h/course.h/xuanke.h后期模块化使用不建议在跑通前拆

命令行编译可以这样起步:

g++ main.cpp -std=c++11 -o course_system ./course_system

-std=c++11不是原项目硬性要求,但后面把char[]换成std::string、把固定数组换成std::vector时会更顺手;-o course_system指定输出文件名,Windows 下可以写成-o course_system.exe。第一次运行如果数据文件不存在,原代码的读取函数会提示“打开失败”然后返回,程序一般不会直接崩,但你会看到空列表,这是正常现象。常见做法是在程序启动时用ofstream以追加模式碰一下文件,把文件先创建出来,再进入读取流程,避免新手误以为程序坏了。

主菜单一定要有循环,否则教师端做完一次添加就退出,体验会很差。原文调试记录里也提到用while调节菜单,这是控制台课设的血泪经验。主循环骨架可以写成这样:

int main() { manage m; m.c_read(); // 先读课程文件到 co 数组 m.s_read(); // 再读学生文件到 st 数组 int a = 0; while (true) { cout << "1.教师端 2.学生端 3.退出" << endl; cin >> a; if (a == 1) { m.teacher_menu(); // 教师端内部再套一层菜单循环 } else if (a == 2) { m.student_menu(); // 学生端显示课程、选课 } else if (a == 3) { m.save(); // 退出前落盘,减少数据丢失 break; } else { cout << "输入有误,请重新选择" << endl; } } return 0; }

这段代码的关键参数是a,它只负责一级菜单分支;m.c_read()和m.s_read()放在循环外,保证整个程序对内存中的对象数组操作,而不是每个功能函数都重新打开文件。m.save()放在退出分支,对应原文“运行完程序没有保存导致下次看不到修改”的踩坑记录。教师端teacher_menu()内部也要有while,否则添加完一门课就回不到主菜单。student_menu()至少要有显示课程和选课两个入口,和原文学生端功能保持一致。

2.2 类关系和职责边界:student、course、xuanke、manage 各自管什么

原文定义了四个类:student管学号、姓名、性别、年龄、系别、班级、联系方式;course管课程代码、课程名称、课程性质、总学时、学分、开课学期;xuanke只保存课程编号和学生学号;manage聚合学生数组、课程数组、选课数组以及计数变量,并承载添加、查询、显示、删除、修改、统计、保存、读取这些功能。这个划分对课设来说够用,职责边界也清楚:实体类只负责自己的字段和输入输出,管理类负责集合操作和文件读写。

类名关键字段关键方法设计注意
studentSID、name、sex、age、SDEPT、grade、TELput_student、display学号用 int,姓名等用 char[]
coursec_code、c_name、c_nature、c_hour、c_grade、c_semesterput_course、display课程代码是唯一键
xuankec_num、s_numget只记录课程号和学生学号
managest[maxn]、co[maxn]、xk[maxn]、c_cnt、s_cnt增删改查、统计、save/read计数器和数组要同步维护

原文里的manage写成了多继承:class manage : public student, public course, public xuanke。从教学演示角度看,它想表达“管理类拥有学生、课程、选课的能力”,但实际运行并不需要继承实体类,因为manage内部已经用st[]、co[]、xk[]聚合了对象。多继承还会带来同名display()的二义性隐患,后面扩展时容易踩坑。更稳的写法是组合:

class manage { private: student st[maxn]; course co[maxn]; xuanke xk[maxn]; int c_cnt; // 当前课程条数,也是 co 的下一个写入位置 int s_cnt; // 当前学生条数 int xk_cnt; // 当前选课条数,内存统计时可用 public: manage() : c_cnt(0), s_cnt(0), xk_cnt(0) {} void c_read(); void s_read(); void add_course(); void add_student(); void c_search(); void s_search(); void c_delete(); void s_delete(); void c_edit(); void s_edit(); void xuank(); void cnt(); void save(); };

这里把c_cnt、s_cnt从全局变量挪进manage,参数含义更清楚:c_cnt既是课程数量,也是下一次添加课程时的数组下标;s_cnt同理。maxn是固定容量,原文取值 100,适合课设演示,但超过容量就会越界。xk_cnt在原代码里没有充分使用,因为选课直接追加到二进制文件;把它放进管理类,是为了后续做退选、按课程统计时不必每次都重读文件。字段类型保持原样能减少迁移成本,等跑通后再把char name[10]改成std::string,才不会在输入带空格姓名时翻车。

2.3 数据流:读文件、对象数组、菜单操作、保存回文件

这个项目的数据流不是“每次操作都直接读写文件”,而是启动时把课程和学生记录读进对象数组,菜单操作只改内存,退出或手动保存时再写回文本文件;选课记录则走另一条路,直接以二进制追加到xuanke_information.txt,统计时再按记录大小读出来。两条持久化路径并存,是理解这个项目的关键,也是后面排查问题的重点。

启动阶段先调用c_read()和s_read(),用while(in >> ...)逐条读入,读到一条就c_cnt++或s_cnt++。进入教师端后,添加课程只改co[c_cnt]并让c_cnt自增;删除课程把后面的对象依次前移,再让c_cnt减一;修改课程先定位下标,再覆盖字段。学生端选课不经过co[]数组,而是构造一个xuanke对象,用fwrite追加到二进制文件。退出前save()把co[]和st[]覆盖写回两个文本文件。这个流程的好处是查询和显示快,缺点是内存和文件可能不同步,所以保存动作必须明确。

// 启动时读入,后续菜单只操作内存数组 manage m; m.c_read(); m.s_read(); // 教师端添加/删除/修改只改 co[]、st[] 和计数器 // 学生端选课直接追加 xuanke_information.txt // 退出前统一保存课程和学生文本文件 m.save();

这段伪流程没有新语法,但参数关系要盯住:c_cnt和s_cnt必须跟数组实际条数一致,添加时先写co[c_cnt]再c_cnt++,删除时先前移再c_cnt--。如果只改数组不改计数器,显示函数会少显示一条;如果只改计数器不改数组,查询会读到脏数据。选课文件是二进制,不要用记事本直接改,否则fread读出来的c_num、s_num会错位。常见做法是把选课也改成文本文件,三份数据统一格式,但原项目这样分开写,正好可以拿来理解“文本流”和“二进制记录”的差别。

3. 核心增删改查:编号唯一性、数组前移和回滚怎么守住

3.1 添加课程/学生:先查重再入数组

添加功能的第一原则是编号唯一。课程用c_code做唯一键,学生用SID做唯一键。原文的做法很直接:先调用put_course()或put_student()从键盘读入一条记录,然后遍历已有数组,如果发现相同编号就提示“有重复,添加失败”,否则把新对象放到数组末尾并让计数器加一。这个顺序不能反,反了就要在失败时回滚,课设里很容易漏。

void manage::add_course() { course c; c.put_course(); // 读入课程代码、名称、性质、学时、学分、学期 int i = 0; for (; i < c_cnt; ++i) { if (co[i].c_code == c.c_code) { // 课程代码唯一 break; } } if (i < c_cnt) { cout << "有重复,添加失败!" << endl; return; } if (c_cnt >= maxn) { // 固定数组必须先判容量 cout << "课程数量已达上限,添加失败!" << endl; return; } co[c_cnt++] = c; // 先写入当前位置,再让计数加一 cout << "添加成功!" << endl; }

这段代码里c是临时对象,c_cnt是当前课程数量,maxn是数组容量。co[c_cnt++] = c的执行顺序是先把c赋给co[c_cnt],再让c_cnt自增,所以下一次添加会写到新位置。put_course()里的cin >> c_name遇到空格会截断,课程名如果有空格就会读错;这是char[]输入的老问题,后面换std::string并用getline才能解决。学生添加同理,只是唯一键换成SID,容量判断换成s_cnt >= maxn。原文调试记录里提到“添加记录时计数值没有跟着增加,导致显示不完全”,根子就在c_cnt++或s_cnt++这一句有没有跟数组写入绑定。

3.2 查询与显示:按编号定位,找不到要有回退提示

查询课程按课程代码找,查询学生按学号找。原文用for遍历,找到就break,循环结束后判断i == c_cnt或i == s_cnt表示没找到。这个写法简单,但要注意break只跳出当前循环,后面的“未找到”判断必须放在循环外。显示功能则是遍历整个数组,每条记录一行;数组为空时提示“文件中无记录”,不要直接访问co[0]。

void manage::c_search() { int c_num = 0; cout << "请输入要查询的课程代码:"; cin >> c_num; int i = 0; for (; i < c_cnt; ++i) { if (co[i].c_code == c_num) { cout << "课程代码、名称、性质、总学时、学分、开课学期:" << endl; co[i].display(); break; } } if (i == c_cnt) { // 循环走到末尾仍未 break cout << "没有找到相关课程,查询失败!" << endl; } } void manage::c_display() { if (c_cnt == 0) { cout << "文件中无记录!" << endl; return; } for (int i = 0; i < c_cnt; ++i) { co[i].display(); // 每条记录一行 } }

c_num是用户输入的课程代码,类型要和c_code一致,原文都用int。i == c_cnt是“没找到”的判定条件,因为找到时break会让i停在匹配下标上,只有遍历完才会等于c_cnt。显示函数先判c_cnt == 0,避免空数组循环和后续误访问。查询学生时把co换成st、c_code换成SID、c_cnt换成s_cnt即可。这里有个常见误用:把“未找到”写在for循环里面,每比较一条不匹配就打印一次失败,最后找到时也会刷一串失败提示;正确做法是循环外只判一次。

3.3 删除与修改:数组前移和“先备份再覆盖”

删除课程时,先按课程代码找到下标i,如果没找到就提示“没有该项记录”并返回;找到后把i后面的对象依次前移一位,最后c_cnt--。修改课程更容易翻车:原文先把旧对象备份到c = co[i],然后让用户输入新内容覆盖co[i],再遍历数组检查新编号是否和其他记录重复,如果重复就把备份赋回去。这个“先备份再覆盖”的顺序是修改功能的关键,否则旧值已经被覆盖,回滚不了。

void manage::c_delete() { int c_num = 0; cout << "请输入要删除课程的代码:"; cin >> c_num; int i = 0; for (; i < c_cnt; ++i) { if (co[i].c_code == c_num) break; } if (i == c_cnt) { cout << "没有该项记录!" << endl; return; } for (int j = i; j < c_cnt - 1; ++j) { co[j] = co[j + 1]; // 后面元素整体前移 } c_cnt--; // 计数减一,尾元素不再参与访问 cout << "记录删除成功!" << endl; } void manage::c_edit() { int c_num = 0; cout << "请输入要修改课程的代码:"; cin >> c_num; int i = 0; for (; i < c_cnt; ++i) { if (co[i].c_code == c_num) break; } if (i == c_cnt) { cout << "没有此项记录!" << endl; return; } course old = co[i]; // 先备份旧记录 co[i].put_course(); // 再读入新记录 for (int j = 0; j < c_cnt; ++j) { if (j == i) continue; // 跳过自己 if (co[j].c_code == co[i].c_code) { co[i] = old; // 编号冲突,回滚 cout << "存在相同编号的课程,修改失败!" << endl; return; } } cout << "修改成功!" << endl; }

删除里的j < c_cnt - 1是为了让j + 1不越界,c_cnt--之后最后一个位置虽然还留着旧对象,但显示和查询只走到c_cnt之前,不会访问它。修改里的old是备份对象,j == i必须跳过,否则自己和自己比较永远重复。原文调试记录里提到“修改后编号重复仍然修改成功”,原因就是先覆盖再查重且没有回滚;加上old备份和co[i] = old就能把后悔药补上。学生删除和修改同理,把co换成st、c_code换成SID、c_cnt换成s_cnt。这两个函数建议在真正写入前再判一次数组边界,避免maxn满了之后还继续读入。

4. 文件持久化与选课统计:fstream 文本流和二进制记录怎么配合

4.1 读取课程/学生文件:为什么先读入对象数组

读取函数负责把文本文件里的记录装进对象数组,并恢复计数器。原文c_read()用ifstream in; in.open("course_information.txt", ios::in);,打开失败就提示并返回;读取循环用while(in >> co[c_cnt].c_code >> co[c_cnt].c_name >> ...),每读成功一条就让c_cnt++。学生读取同理。这个设计的价值在于后续查询、显示、删除、修改都只面对内存数组,不用每个函数都打开文件,代码职责更清楚。

void manage::c_read() { ifstream in("course_information.txt", ios::in); if (!in) { cout << "打开失败!" << endl; return; } c_cnt = 0; // 重读前先清零 while (c_cnt < maxn && in >> co[c_cnt].c_code >> co[c_cnt].c_name >> co[c_cnt].c_nature >> co[c_cnt].c_hour >> co[c_cnt].c_grade >> co[c_cnt].c_semester) { c_cnt++; // 每成功读一条,计数加一 } in.close(); // 读文件也要关闭 }

in是输入文件流,c_cnt在读取前清零,避免重复调用时越加越多。while里先判c_cnt < maxn,再执行流读取,防止文件记录超过数组容量导致越界。in >> co[c_cnt].c_name按空白分隔读取,所以课程名、姓名里不能有空格;如果有空格,读到一半就会错位。原文调试记录里提到“文件确实存在但读取失败”,常见原因是写文件后没有close(),缓冲区没刷盘或句柄被占用;读取函数和保存函数都要成对关闭文件。学生读取把文件名换成student_information.txt,字段换成SID、name、sex、age、SDEPT、grade、TEL,计数器换成s_cnt。

4.2 保存文件:用 fstream 覆盖写避免二进制乱码

保存函数负责把对象数组写回文本文件。原文一开始用 C 的fwrite(),结果打开文件看到乱码,因为fwrite写的是二进制字节;后来改用 C++ 的fstream,用<<输出文本,记事本能直接看。这个取舍对课设很实用:课程和学生数据量小,文本格式方便调试,也方便手动造测试数据。保存用ios::out覆盖写,每次保存都重写整个文件,不做追加,避免重复记录越积越多。

void manage::save() { ofstream out("course_information.txt", ios::out); if (!out) { cout << "打开失败!" << endl; return; } for (int i = 0; i < c_cnt; ++i) { out << co[i].c_code << " " << co[i].c_name << " " << co[i].c_nature << " " << co[i].c_hour << " " << co[i].c_grade << " " << co[i].c_semester << endl; } out.close(); // 关闭课程文件 out.open("student_information.txt", ios::out); if (!out) { cout << "打开失败!" << endl; return; } for (int i = 0; i < s_cnt; ++i) { out << st[i].SID << " " << st[i].name << " " << st[i].sex << " " << st[i].age << " " << st[i].SDEPT << " " << st[i].grade << " " << st[i].TEL << endl; } out.close(); // 关闭学生文件 }

ofstream out以ios::out打开,文件不存在会创建,存在会截断重写;c_cnt和s_cnt决定写多少条,所以保存前必须保证计数器准确。字段之间用空格分隔,和读取函数的>>格式保持一致;如果改成逗号分隔,读取也要同步改。out.close()不能省,原文踩过“写完没关文件,下次读取失败”的坑。保存可以放在退出菜单里自动调用,也可以在教师端每次增删改之后调用一次,后者更稳但会频繁写盘;课设数据量不大,自动保存更省心。注意不要在save()里再调用c_read(),否则会把刚写的内容又读一遍,计数器容易乱。

4.3 选课与统计:xuanke 记录用二进制追加,统计时 ftell 计算条数

选课功能和其他增删改查不一样,它没有走对象数组,而是用FILE*以ab+模式打开xuanke_information.txt,用fwrite把xuanke对象追加进去。统计某门课程的选修人数时,用fseek(fp, 0, 2)移到文件末尾,ftell(fp)得到总字节数,再除以sizeof(xuanke)得到记录条数,rewind(fp)回到开头逐条fread,匹配课程编号后把学生学号收集起来。这个做法能跑,但混合了 C 风格文件操作和 C++ 流,排查时要分清。

void manage::xuank() { FILE* fp = fopen("xuanke_information.txt", "ab+"); if (fp == NULL) { fp = fopen("xuanke_information.txt", "wb+"); if (fp == NULL) { cout << "不能建立选课文件" << endl; return; } } xuanke xk; xk.get(); // 读入 c_num 和 s_num fseek(fp, 0, 2); // 移到末尾,准备追加 fwrite(&xk, sizeof(xuanke), 1, fp); fclose(fp); cout << "选课成功!" << endl; } void manage::cnt() { int a = 0; cin >> a; switch (a) { case 1: cout << "学生人数为:" << s_cnt << endl; // 原代码这里写成了 c_cnt break; case 2: cout << "课程门数为:" << c_cnt << endl; // 原代码这里写成了 s_cnt break; case 3: { FILE* fp = fopen("xuanke_information.txt", "rb+"); if (fp == NULL) { cout << "选课文件打开失败!" << endl; return; } int c_num = 0; cin >> c_num; fseek(fp, 0, 2); long n = ftell(fp) / sizeof(xuanke); // 记录条数 rewind(fp); int ids[maxn]; int cnt = 0; xuanke xk1; for (long i = 0; i < n && cnt < maxn; ++i) { fread(&xk1, sizeof(xuanke), 1, fp); if (xk1.c_num == c_num) { ids[cnt++] = xk1.s_num; // 收集选该课的学生学号 } } fclose(fp); cout << "选修该课程的人数为:" << cnt << endl; for (int i = 0; i < cnt; ++i) { cout << ids[i] << " "; } cout << endl; break; } default: cout << "输入有误" << endl; } }

fopen("xuanke_information.txt", "ab+")表示以追加读写模式打开二进制文件,文件不存在时创建;如果失败再尝试wb+新建。fseek(fp, 0, 2)的第三个参数2表示文件末尾,ftell返回当前字节偏移,除以sizeof(xuanke)就是记录条数。fread每次读一个xuanke,所以文件里不能混入文本内容,否则记录会错位。ids[maxn]是学生学号收集数组,cnt既做数量也做下标,循环里要加cnt < maxn防止越界。原文统计分支把学生人数和课程门数写反了,case 1应该输出s_cnt,case 2应该输出c_cnt;这种小错不影响编译,但统计结果会让人怀疑人生,跑通后一定要拿已知数据核对一遍。选课记录目前不检查重复选课和课程是否存在,二次开发时可以补上。

5. 避坑与排查:控制台课设最容易翻车的五个现场

5.1 文件与保存类:保存了但下次运行没数据

现象是教师端添加完课程,退出程序再进来,课程列表又空了,或者修改后的字段还是旧值。原因通常有两个:一是退出前没有调用save(),只改了内存数组;二是save()里文件没有close(),或者程序异常结束导致缓冲没刷盘。原项目调试记录里专门写了“运行完程序没有保存,下次读取发现上次修改没有显示”,这不是玄学,就是落盘动作漏了。

解决方式是把保存做成明确动作:退出主菜单时调用一次save(),教师端每次添加、删除、修改成功后也可以调用一次。save()内部两个文件都要close(),并且保存后可以立刻用c_read()、s_read()读一遍做自检,确认计数器和文件条数一致。排查时先看当前工作目录是不是你以為的目录,IDE 的运行目录和可执行文件目录可能不同;再看文件修改时间有没有变化。常见做法是把数据文件路径打印出来,或者用绝对路径做一次对照,定位到底是没写还是写到了别处。

5.2 数组与菜单类:显示不全、删除越界、操作一次就退出

现象是新添加的记录不显示,或者删除最后一条时程序崩溃,或者教师端做完一个操作就回到主菜单。新记录不显示,原因多半是添加时写了数组但忘了c_cnt++或s_cnt++,显示循环只走到旧计数;删除越界,原因多半是前移循环写成了j < c_cnt,访问了co[j + 1]的越界位置;操作一次就退出,原因是教师端没有套while菜单循环,执行完一个case就自然结束。

解决方式是把计数器和数组写入绑在同一个函数里,添加用co[c_cnt++] = c,删除用for (j = i; j < c_cnt - 1; ++j) co[j] = co[j + 1]; c_cnt--;,改完马上显示一遍验证。菜单循环用while (true)包住switch,每个分支执行完break回到菜单,只有选退出才return或break外层。排查时可以在添加、删除后打印c_cnt、s_cnt,看计数有没有跟着变;数组上限maxn也要在添加前判断,别等越界了再找原因。

5.3 编号与选课类:修改冲突回滚失败、选课文件读成乱码

现象是修改课程编号后和其他课程重复,程序仍然提示修改成功;或者用记事本打开xuanke_information.txt看到乱码,统计时人数不对。修改冲突回滚失败,原因通常是先co[i].put_course()覆盖了旧值,再查重,发现重复时旧值已经没了;选课文件乱码,原因是fwrite写的是二进制结构体,不是文本,记事本按文本解释当然乱。

解决方式是在修改前备份course old = co[i];,查重时跳过自己,发现重复立即co[i] = old;回滚。选课文件不要用记事本改,统计时坚持用fread按sizeof(xuanke)读;如果确实想手改,就把选课也改成文本格式,三份文件统一用fstream。另外统计分支要核对case 1输出学生人数s_cnt、case 2输出课程门数c_cnt,原文这里写反过。选课目前不检查同一学生是否重复选同一门课,二次开发时可以在写入前扫一遍二进制文件,或者把选课记录也读进xk[]数组统一判重。排查选课问题时,先打印ftell得到的字节数和sizeof(xuanke),看记录条数是否合理,再逐条读出来对照。

5.4 输入与字符数组类:姓名带空格、输入类型不匹配

现象是输入学生姓名“张 三”后,后面的性别、年龄全部错位,或者输入字母导致菜单死循环。原因是cin >> name按空白截断,char name[10]只能接第一个词;输入类型不匹配时,cin进入失败状态,后面的读取全部失效。课设演示数据通常用单字姓名或不带空格的拼音,所以这个问题容易藏到答辩才爆。

解决方式短期可以把姓名输入改成不带空格,或者在put_student()里用cin >>读完整行前先清理缓冲区;长期把char[]换成std::string,用getline(cin, name)读整行。菜单输入int a前要判断cin.fail(),失败就cin.clear(); cin.ignore(...);清掉错误状态。排查时在每次输入后打印读到的字段,看是从哪一步开始错位;char name[10]还要防超长,输入超过 9 个字符会越界,这类问题在机房环境不一定立刻崩,但属于埋雷。

5.5 文件路径与模式类:文本流和二进制流混用

现象是课程文件能读,选课统计却读不出;或者用fstream打开选课文件后fread读到的数据不对。原因是课程和学生用ifstream/ofstream文本模式,选课用FILE*二进制模式,两套 API 和两种格式混在一个项目里。文本模式会做换行转换,二进制模式按字节原样读写,混用就会错位。

解决方式是把边界划清楚:课程和学生坚持文本格式,用fstream的>>和<<;选课坚持二进制格式,用fopen/fwrite/fread,并且打开模式带b。不要让fstream去读fwrite写的文件,也不要让fread去读文本文件。排查时先看文件大小能不能被sizeof(xuanke)整除,不能整除就说明混入了文本或写坏了;再看fopen的返回值,失败时打印路径。常见做法是把三份数据文件统一放到data/目录,启动时检查目录存在,路径用相对路径但固定工作目录,减少“文件到底在哪”的来回折腾。

6. 二次开发:把单文件课设改成可展示的模块化工程

这个项目跑通之后,最值得做的不是加花哨界面,而是把单文件拆成能复用的模块。第一步拆头文件和源文件:student.h、course.h、xuanke.h只放类定义,manage.h放管理类声明,manage.cpp放增删改查和文件读写,main.cpp只留菜单。第二步把manage的多继承改成组合,字段用std::string替代char[],固定数组st[maxn]换成std::vector<student>,计数器直接用size(),从根上消掉越界和计数不同步。第三步把保存动作挂到每次增删改之后,再加一个“添加-保存-重启-显示-删除-再保存”的自检入口。

// student.h:实体类只留数据,输入输出放实现文件 #pragma once #include <string> struct student { int SID = 0; std::string name; char sex = 'M'; int age = 0; std::string SDEPT; std::string grade; std::string TEL; };

这段头文件把char name[10]换成std::string,姓名带空格不会再截断;SID给默认值,避免未初始化。拆分后编译命令要带上所有 cpp,例如g++ main.cpp manage.cpp -std=c++11 -o course_system。改造验证不要靠感觉,按表格逐项核对:

改造点做法验证方法
实体类拆分student/course/xuanke 独立头文件单独包含头文件能编译
组合替代多继承manage 内聚合 vector不再出现 display 二义性
自动保存增删改后调用 save重启后数据还在
选课退选二进制记录按 c_num+s_num 删除统计算法人数减一
输入校验检查 cin.fail 和空字符串输入字母菜单不死循环

我一般还会补一个最小测试数据文件,放两门课、两个学生、一条选课记录,每次改完先跑这个固定数据集,看统计人数和列表条数对不对得上。选课退选可以在xuanke里加一个按c_num和s_num匹配的删除函数,读二进制到临时数组,跳过目标记录再重写文件;成绩管理、登录密码这些属于扩展功能,等增删改查和持久化稳定后再加,不然排查面会铺得太大。从那以后我每次把这类课设交给别人前,都强制走一遍“添加-保存-关闭-重启-显示-删除-再保存”的闭环,确认文件、计数器和菜单循环三者一致才收手,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询