☰
C++课程设计:学生选课系统对象数组与文件持久化避坑
2026/10/28 4:28:15 网站建设 项目流程

简介:这份C++课程设计文档面向计算机相关专业学生及C++面向对象编程学习者,围绕“学生选修课程系统”提供一份较完整的课程设计报告范例,可用于选题论证、报告撰写和项目实现对照。压缩包共1个doc文件,约259KB,按需求分析、内容设计、调试分析、用户手册、设计总结与附录等章节展开。内容覆盖学生登录、课程浏览、选课与退选、冲突检查、个人信息管理、成绩查询和数据统计等功能梳理,并给出用户管理、课程信息、选课等模块划分及登录、选课流程说明;关键代码部分涉及身份验证、课程数据管理、选课逻辑与数据持久化。文档还记录了上机问题及解决方案、性能分析、可改进点和功能扩展构想,附有用户手册与参考文献。已有1442人学习下载,适合需要完成C++课程设计、理解面向对象建模与报告规范的学生参考。

1. 验收时最容易被问倒的一句话:退出重进,数据还在吗

学生选修课程系统设计几乎是 C++ 课程设计里出镜率最高的一道题,但真正能拿得出手的版本不多。大多数提交上去的程序,菜单画得挺漂亮,增删改查也能跑,可一按退出再重新进入,刚敲进去的那几门课就全没了。这份资源是一套完整的控制台实现:教师端负责学生、课程记录的增删改查与统计,学生端负责浏览课程和选课,数据靠对象数组和本地文件撑着,四个类student、course、xuanke、manage分工明确。它适合两类人:一类是刚学完类和对象、继承、文件流,需要一份能逐行读懂、能直接跑通的完整参照,用来应付 C++ 课程设计这道作业;另一类是手上已经有半成品,但卡在“保存不生效”“修改后原记录被覆盖”这些具体毛病上,想找个对照着改的底子。

2. 类结构拆解:三个数据类挂一个管理器

这套代码的骨架其实很简单,三个类只管存数据,第四个类只管调功能。看懂这一层,后面所有函数的写法都能自己推导出来。

2.1 student 与 course:字段怎么定,坑就埋在怎么定

学生类和课程类是最典型的“纯数据 + 输入输出方法”结构,没有构造函数、没有私有封装,所有成员都是public,这也是课程设计阶段最常见的写法。

class student { public: // 数据成员 int SID; // 学号,唯一标识 char name[10]; // 姓名 char sex; // 性别,单字符 M/F int age; // 年龄 char SDEPT[10]; // 系别 char grade[10]; // 班级 char TEL[10]; // 联系方式 public: // 函数成员 void put_student() { cout << "请输入学生的 学号、姓名、性别、年龄、系别、班级、联系方式:" << endl; cin >> SID >> name >> sex >> age >> SDEPT >> grade >> TEL; } void display() { cout << SID << "\t" << name << "\t" << sex << "\t" << age << "\t" << SDEPT << "\t" << grade << "\t" << TEL << endl; } };

这段代码的关键点不在put_student,而在那几个char数组的长度。name[10]看起来够用,但 UTF-8 编码下一个汉字占 3 个字节,10 个字节实际只能装下三个汉字,四个字的名字就会写越界,cin >> name不会报错,它会安安静静地把后面的内存踩掉,然后在某个不相干的地方崩溃——这就是典型的玄学 bug。我的做法是直接把char name[10]换成string name,cin >> string不受长度限制,ofstream <<写出去也还是文本,改动量只有几行,但能省掉一整轮排查。

课程类结构一样,只是字段换成了课程代码、名称、性质、总学时、学分、开课学期:

class course { public: int c_code; // 课程代码,唯一标识 char c_name[10]; // 课程名称 char c_nature[10]; // 课程性质(必修/选修) int c_hour; // 总学时 int c_grade; // 学分 char c_semester[10]; // 开课学期 public: void put_course() { cout << "请输入课程代码、课程名称、课程性质、总学时、学分、开课学期" << endl; cin >> c_code >> c_name >> c_nature >> c_hour >> c_grade >> c_semester; } void display() { cout << c_code << "\t" << c_name << "\t" << c_nature << "\t" << c_hour << "\t" << c_grade << "\t" << c_semester << endl; } };

原始报告里course还多写了一个int vis[90];,这个字段从头到尾没有任何地方用到,属于设计阶段想“标记这门课有没有被选”但最后没落地留下的残骸。二次开发时它可以派上用场,但如果只是照着跑通,删掉更干净。

2.2 xuanke 与 manage:关系类和管理类的分工

选课关系单独抽了一个类,里面只有两个int:

class xuanke { public: int c_num; // 课程代码 int s_num; // 学生学号 public: void get() { cout << "请输入课程代码和学生学号:" << endl; cin >> c_num >> s_num; } };

这个设计是整套代码里最值得肯定的地方。选课本质上是学生和课程之间的多对多关系,硬塞进student里加一个“已选课程数组”,或者塞进course里加一个“选课学生数组”,都会让后续的统计和退选变得非常难写。抽成独立的三元组(这里是二元组)之后,选课记录可以单独存一个文件,统计某门课有多少人选,只要扫一遍这个文件就行。

承载所有功能的是manage类:

class manage : public student, public course, public xuanke { private: student st[maxn]; // 学生对象数组 course co[maxn]; // 课程对象数组 xuanke xk[maxn]; // 选课记录数组 public: void c_read(); void s_read(); void add_course(); void add_student(); void c_search(); void s_search(); void c_display(); void s_display(); void c_delete(); void s_delete(); void c_edit(); void s_edit(); void xuank(); void cnt(); void save(); };

这里有个需要说清楚的取舍:manage用公有继承同时继承了三个数据类,但真正干活的成员变量是自己内部的st、co、xk三个数组。继承在这里没带来任何好处,manage并不会因为继承了student就自动变成学生。更合理的写法是去掉继承,改成组合——manage里持有三个数组就够了。这不是语法错误,程序照样跑,但答辩时老师问一句“你为什么用多重继承”,很难答得圆。想改的话,把类声明改成class manage就行,其余代码一行不用动,因为所有访问走的都是st[i]、co[i]。

2.3 全局计数与数组上限:maxn 定成 100 的代价

文件头部两个全局变量加上一个常量,是整套程序能运转的基础,也是最脆弱的地方:

const int maxn = 100; // 数组上限,学生/课程各最多 100 条 int c_cnt = 0, s_cnt = 0; // 当前已存在的课程数、学生数

c_cnt和s_cnt承担了三重职责:既是数组下标(新记录写进co[c_cnt]),又是循环上界(for(int i = 0; i < c_cnt; i++)),还是统计口径(学生人数直接输出s_cnt)。写起来确实省事,但代价是:所有函数都必须保证这两个数在每条路径上都被正确维护,少加一次、多加一次,后面全是连锁反应。

实际风险在于add_course()里那句co[c_cnt++] = c;完全没有边界判断。第 101 门课会被写到数组外面去,而maxn是 100,这个错误在课程设计的演示数据量下永远不会暴露,一旦老师让你多导几条测试数据,程序就当场翻车。稳妥的补法是加一行守卫:

if (c_cnt >= maxn) { cout << "记录数已达上限 " << maxn << ",请先删除部分记录" << endl; return; }

另一个隐患是这两个计数器是全局的,而不是manage的成员。表面上没问题,因为整个程序只有一个manage对象;但只要哪天想开两个实例做对比演示,数据立刻串味。改成manage的成员变量并配套一个init()在读文件前清零,是成本很低的一次加固。

3. 菜单与增删改查:一个 while 串起两端

控制台程序的骨架就是“打印菜单 → 读一个数字 → switch 分发 → 回到菜单”。这套代码在这一点上没有花活,但每一类操作的实现细节里都藏着值得抄的做法和需要躲的坑。

3.1 main 的骨架:为什么要用 while 包住整个菜单

原始报告的调试部分专门记了一条:“进入教师端时如果选择某项操作,操作完成后就会退出系统,解决方法是加一个 while 语句”。这个毛病几乎所有初学控制台程序的人都踩过,因为最初的main往往写成了顺序结构:

int main() { manage m; m.c_read(); // 先把课程文件读进对象数组 m.s_read(); // 再把学生文件读进对象数组 int a; while (true) // 三层循环:主菜单 / 学生端 / 教师端 { cout << "==== 学生选课系统 ====" << endl; cout << " 1. 学生端 2. 教师端 3. 退出" << endl; cin >> a; if (a == 3) break; // 只有主动选 3 才退出 if (a == 1) // ---------- 学生端 ---------- { int b; while (true) { cout << " 1. 显示课程 2. 选课 3. 返回" << endl; cin >> b; if (b == 3) break; if (b == 1) m.c_display(); if (b == 2) m.xuank(); } } else if (a == 2) // ---------- 教师端 ---------- { int c; while (true) { cout << " 1. 添加 2. 查询 3. 显示 4. 删除" << endl; cout << " 5. 修改 6. 统计 7. 保存 8. 返回" << endl; cin >> c; if (c == 8) break; switch (c) { case 1: { int t; cout << " 1. 添加课程 2. 添加学生" << endl; cin >> t; if (t == 1) m.add_course(); else m.add_student(); break; } case 2: /* 查询同理分课程/学生 */ break; case 3: /* 显示 */ break; case 4: /* 删除 */ break; case 5: /* 修改 */ break; case 6: m.cnt(); break; case 7: m.save(); break; } } } } m.save(); // 退出前兜底保存一次 return 0; }

这段骨架有三处值得说明。第一,c_read()和s_read()放在循环外面,只在程序启动时读一次,之后所有操作都作用在内存中的对象数组上,只有save()才回写文件。这个“内存优先、一次性落盘”的思路是对的,比每个函数都开关文件清晰得多。第二,break只跳出当前这一层while,所以学生端选 3 是回到主菜单,主菜单选 3 才是退出程序,层级别搞混。第三,末尾那句兜底save()很关键——原始版本没有它,于是出现“这次改完直接关窗口,下次进来记录全没了”的情况,学生自己调试时百思不得其解。

提示:把所有cin >>的结果都当成可能失败来处理。用户输入一个字母而不是数字时,cin会进入失败状态,后面所有读取全部失效,菜单会疯狂刷屏。稳妥做法是在每次cin >>后加cin.clear(); cin.ignore(numeric_limits<streamsize>::max(), '\n');。

3.2 添加:先查重,再落位

添加课程是最能体现“唯一性由程序保证”的一段。编号重复不是靠用户自觉,而是靠一次线性扫描兜住:

void add_course() { course c; c.put_course(); // 先读入待添加的完整信息 cout << endl; for (int i = 0; i < c_cnt; i++) // 查重:扫一遍已存在的课程 { if (co[i].c_code == c.c_code) { cout << "有重复,添加失败!" << endl; return; // 直接返回,不写入数组 } } co[c_cnt++] = c; // 下标即当前数量,写完自增 cout << "添加成功!" << endl; }

这里的关键顺序是“先把整条记录读完,再做校验”。如果改成一边读字段一边查重,一旦发现重复,用户已经输了一半,体验反而更糟。co[c_cnt++] = c这一句是对象整体赋值,编译器生成的默认赋值运算符会逐成员拷贝,对于只含基本类型和char数组的类完全够用。

学生端的add_student()逻辑完全对称,只是把co换成st、c_code换成SID、c_cnt换成s_cnt。这种成对出现的函数非常适合抽模板,把关键的比较字段抽成参数,用同一个函数处理两类记录,代码量能砍掉将近一半。不过课程设计阶段建议先保持两套,等跑通了再重构,改坏了不好定位。

3.3 查询、显示与删除:线性查找加上数组前移

查询和显示是一对,前者按编号找到一条,后者遍历输出全部:

void c_search() { int c_num, i; cin >> c_num; // 读入要查的课程代码 for (i = 0; i < c_cnt; i++) { if (co[i].c_code == c_num) { cout << "课程代码、课程名称、课程性质、总学时、学分、开课学期:" << endl; co[i].display(); // 命中就输出这一条 break; } } if (i == c_cnt) // 循环走完仍未命中 cout << "没有找到相关课程,查询失败!" << endl; }

判断“没找到”用的是i == c_cnt,而不是额外设一个flag。因为break之后i停在命中位置,循环正常结束则i必然等于c_cnt,这个技巧在后面的删除和修改里反复出现,值得记住。

删除的难点不在查找,而在删完之后数组要保持连续:

void c_delete() { int c_num, i; cin >> c_num; for (i = 0; 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; }

这个前移循环的上界必须是c_cnt - 1,因为循环体里访问的是co[j+1],如果写成j < c_cnt,最后一次会读到co[c_cnt],那是紧接着数组尾部的一块未初始化内存,拷贝出来的对象再拿去显示就是一堆乱码。这是我最常见的翻车点之一,编译不报错、大部分时候也不崩,但会莫名其妙印出垃圾数据。

3.4 修改:先备份再覆盖的回滚写法

修改函数是整套代码里最讲究的一段。朴素写法是“找到记录 → 直接让用户重新输入 → 若编号重复就报错”,问题在于:用户输入的那一刻,原记录已经被新内容覆盖掉了,这时候再报“修改失败”其实已经晚了,原数据没了。原始报告里专门记了这个坑,解法是先存一份备份:

void c_edit() { int c_num, i; course backup; // 备份原记录 cout << "请输入要修改课程的代码:" << endl; cin >> c_num; for (i = 0; i < c_cnt; i++) if (co[i].c_code == c_num) { backup = co[i]; break; } // 找到并备份 if (i == c_cnt) { cout << "没有此项记录!" << endl; return; } co[i].display(); // 先展示原值,方便对照 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] = backup; // 撞车就回滚 cout << "存在相同编号的课程,修改失败!" << endl; return; } } cout << "修改成功!" << endl; }

这里的backup就是后悔药。j == i那一句不能省,否则只要用户没改编号,自己和自己比较一定失败,等于所有不带编号变化的修改都改不了。同样的结构在学生端s_edit()上照搬,只是比较字段换成SID。

参数上唯一需要注意的仍然是编号类型:课程代码和学号都用int,意味着用户输错一个字符(比如打了字母),cin会失败并让后面全部停摆。对这个规模的程序,最简单的防护是在读之前先cout明确提示“只能输入数字”,出错后靠cin.clear()复位。

4. 文件持久化与选课统计:两套 IO 并存怎么用

程序退出前数据要落地,下次启动要能读回来,这部分用了两套不同的 IO:学生和课程走 C++ 的fstream文本流,选课记录走 C 的FILE二进制。混用不是好设计,但搞懂它们各自的特点,能解释很多看起来莫名其妙的现象。

4.1 文本流读写:三行代码完成存取

学生和课程的存取都用ifstream/ofstream,写出的是纯文本,用记事本直接打开就能看:

void 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(); }

字段之间用空格分隔,每条记录一行,这是文本格式最省事的做法,代价是字段内容里不能含空格——char c_nature[10]里如果输入“专业选修”这种带空格的词,读回来时整行就会错位。真要支持空格,得换成分隔符,比如|,然后读的时候用getline配合stringstream切分。课程设计的数据量下用空格够了,但心里要有这根弦。

读取就是写出的逆向操作,循环里把六个字段依次读进当前下标的对象:

void c_read() { ifstream in("course_information.txt", ios::in); if (!in) { cout << "打开失败!" << endl; return; } while (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(); }

while (in >> ...)这一句的妙处在于:读到文件末尾时流会进入失败状态,条件判定为假,循环自然结束,不需要事先知道记录条数。所有字段都读成功才自增,所以文件末尾多出半行残缺数据也不会污染计数。

必须强调一点:这段代码依赖c_cnt从 0 开始。如果程序里读了一次文件、操作半天、又调了一次c_read(),第二次的数据会接在第一次后面,同一批记录读进来两遍。正规做法是在读取前加一句c_cnt = 0;,或者在函数开头明确写出“本函数会追加,不覆盖”。

4.2 选课记录为什么用 FILE 而不是 fstream

选课这块用的是fopen+fwrite:

void xuank() { FILE *fp; xuanke xk; xk.get(); // 读入课程代码 + 学号 if ((fp = fopen("xuanke_information.txt", "ab+")) == NULL) // 追加模式 if ((fp = fopen("xuanke_information.txt", "wb+")) == NULL) { cout << "不能建立 xuanke_information.txt 文件" << endl; exit(0); } fseek(fp, 0, SEEK_END); // 指针移到末尾 fwrite(&xk, sizeof(xuanke), 1, fp); // 整块结构体二进制写入 fclose(fp); cout << "选课成功!" << endl; }

选这个方案的理由不难猜:选课记录只有两个int,没有字符串,用二进制整块写最省事,一条记录固定 8 字节,统计时可以靠文件长度反推条数。而学生和课程里有char数组,文本格式更好调试,于是同一套程序里出现了两种风格。这在课程设计里能接受,但两层 IO 混用会带来一个真实问题:二进制文件的打开模式写错了,读出来的全是乱码。

另外要说明一点:"ab+"模式本身在文件不存在时就会自动创建,所以外层那个if基本是死代码,真正需要"wb+"的情况极少出现。这不是错,只是说明作者对fopen模式的理解还不够透。改的时候把这段简化成一次fopen(..., "ab+")加一次判空即可,逻辑更清楚。

4.3 统计函数的三种口径

统计功能用一个switch分了三支:学生人数、课程门数、某门课的选课名单。第一支和第二支直接输出计数器:

void cnt() { int a; cin >> a; switch (a) { case 1: // 统计学生人数 cout << "学生人数为:" << s_cnt << endl; break; case 2: // 统计课程门数 cout << "课程门数为:" << c_cnt << endl; break; case 3: { // 统计某门课的选课学生 FILE *fp = fopen("xuanke_information.txt", "rb+"); if (fp == NULL) { cout << "文件打开失败!" << endl; return; } int c_num, n, cnt = 0; int A[60]; // 存命中学生的学号 cin >> c_num; fseek(fp, 0, SEEK_END); n = ftell(fp) / sizeof(xuanke); // 用文件长度反推记录条数 rewind(fp); xuanke xk1; for (int i = 0; i < n; i++) { fread(&xk1, sizeof(xuanke), 1, fp); if (xk1.c_num == c_num) A[cnt++] = xk1.s_num; // 命中就收集 } fclose(fp); cout << "选修该课程的人数为:" << cnt << endl; cout << "学生的学号分别为:" << endl; for (int i = 0; i < cnt; i++) cout << A[i] << " "; break; } } }

n = ftell(fp) / sizeof(xuanke)是这段的核心技巧,也是它的软肋。它能成立的前提是文件里全是完整的xuanke记录、没有任何额外字节、并且写入端和读取端用的是同一个结构体定义。只要换一个编译器,结构体对齐方式一变,sizeof(xuanke)就可能从 8 变成 16,条数直接算错一半。原始报告里int A[60]也是一个硬上限,超过 60 人选同一门课就越界了。想要稳,把A换成vector<int>,条数改用while (fread(&xk1, sizeof(xuanke), 1, fp) == 1)循环里现数,两个隐患一起消掉。

5. 避坑与常见问题排查

这套代码能跑起来之后,剩下的时间基本都花在跟下面这几个问题上较劲。每一条我都给出实际现象、根因和改法。

5.1 现象:这次改完,下次进来记录全没了

原因通常不是save()写错了,而是根本没被调用。原始版本里保存是教师端的一个菜单项,用户改完数据直接关窗口,程序走到return 0就结束,内存里的修改随之蒸发。更隐蔽的一种是保存函数里out.close()漏写,缓冲区没刷新,文件里只留了半截。

解决有两个层次:短期在main的末尾、return 0之前补一句m.save();,保证任何退出路径都会落盘;长期在增删改三个函数内部,操作成功后立刻调用一次save(),做到“改一条存一次”。课程设计的数据量下,多写几次文件的开销完全可以忽略。

5.2 现象:修改课程信息后,发现编号和别人撞了,报错提示出来了,但原记录已经被改坏

这就是 3.4 节讲的回滚缺失。根因是函数直接对co[i]调用了put_course(),用户输入完毕时原值已经被覆盖,之后才发现重复,此时没有东西可以恢复。解决办法是修改前先把co[i]赋值给一个局部的course backup,查重失败就co[i] = backup还原。判断重复的循环里记得if (j == i) continue;,否则自己和自己比永远重。

5.3 现象:选统计功能,选“1 查学生人数”输出的却是课程门数

根因很直接,switch的两个分支把变量写反了:case 1里输出的是c_cnt,case 2里输出的是s_cnt。这类错误不影响编译,跑起来也“有数字”,所以特别容易漏掉。修复就是把两个变量对调。更稳妥的做法是给计数器改个名,student_count和course_count,让写错的时候一看就知道。

5.4 现象:第二次选课,之前选的记录全被清空

看到文件操作就去找模式下错。原始代码用的是"ab+"追加模式,本身是对的;但如果中途有人把它改成了"wb+",每次打开都会把文件截断重写,历史记录就没了。另一个来源是统计函数里的fopen(..., "rb+"),读模式配r+在文件不存在时会返回NULL,用户第一次选课之前点统计,程序直接报“文件打开失败”就退出了,看起来像是记录没了。

解决:写入端固定"ab+",读取端固定"rb",文件不存在时不要exit(0),而是提示“暂无选课记录”后正常返回。

5.5 现象:添加了好几门课,显示的时候只出来一部分

根因是计数器没有跟着自增,或者自增的位置不对。典型写法是co[c_cnt] = c;而漏掉了后面的c_cnt++;,于是新记录确实写进了数组,但c_cnt没变,显示循环的范围到不了它,看起来就像加进去了又没加进去。另一种是add_course里查重成功走的return分支之后又写了自增,导致失败的添加也让计数加一,数组尾部多出一个空对象。

解决:所有写数组的地方统一收口到一个append辅助函数里,先判上限、再赋值、再自增,三件事绑在一起,不给漏写的机会。

另外还有个只在编译期暴露的问题值得记一笔:原始报告里类名xuanke和成员函数名xuanke()撞了。C++ 里类名和函数名在同一作用域重名,编译器会报出一堆很难懂的错。改法是把成员函数改名为select_course()、xuank()之类的不同名,类名保持不变,一处改名,所有调用点跟着改即可。

6. 二次开发:把课程作业改成能用的东西

这套代码真正值钱的地方不是它能跑,而是它把“对象数组 + 文件 + 菜单”这条最小闭环走通了,往上加东西的路很宽。我给几个改造成本低、效果明显的方向。

第一个是把定长数组换成vector。student st[maxn]改成vector<student> st;,c_cnt、s_cnt全部删掉,改用st.size()。这一步能顺手干掉两件事:100 条上限没了,计数漏加少加的坑也一并消失。改动集中在声明处和所有c_cnt的引用处,半小时能完成,是我认为性价比最高的一次重构。

第二个是登录与端点拆分。摘要里提到的“学号加密码验证”在现有代码里没有,但加起来的成本很低:给student加一个char pwd[16]字段,学生端入口先读学号、比对密码,通过后才允许选课;教师端入口单独设一个固定口令。注意别把口令硬编码进源码里当作安全措施,这只是课程作业级别的演示,真要上线得换成哈希存储。

第三个是自动保存。把save()的调用从菜单项挪到add_*、delete_*、edit_*的末尾,用户就不需要记得手动保存了。代价是每次操作多一次文件写入,测试时可以把save()包一层开关,需要调试就关掉。

改完之后怎么验证没过改坏?我一般跑一套固定的冒烟流程:添加三条课程、两条学生 → 显示确认条数对 → 修改其中一条的编号成已存在的编号,确认报错且原值未变 → 删除中间那条,确认后面那条顶上来、总量减一 → 选课两次、统计确认两条记录 → 退出重进,逐项核对。这套流程跑通,基本就说明数据结构没有被我改乱。

改造点改动位置风险收益
数组换 vector类声明 + 所有c_cnt引用低去上限、去计数 bug
char[]换stringstudent/course字段声明低根除越界与截断
加登录校验端点入口 +student字段中贴合真实选课场景
自动保存增删改函数末尾低消除“忘了保存”

还有个细节值得单独说:改char name[10]成string的时候,save()里out << st[i].name不用动,但c_read()里in >> st[s_cnt].name也一样不用动,string和>>天然兼容。真正需要检查的是所有把字段当char*用的地方,比如strcmp、strcpy、fwrite。这套代码里course和student都是文本流,没有strcpy,所以改起来很干净;但xuanke走的是二进制fwrite,一旦给它加string字段,fwrite写出去的就是指针值,下次读回来直接悬空访问。所以那两个类要分开改,别图省事一起动。

注意:在manage里把st、co、xk从定长数组改成vector之后,save()里的for (int i = 0; i < c_cnt; i++)必须同步换成for (size_t i = 0; i < co.size(); i++),漏改一处就是编译期报错,反而比运行期出错好排查。

这套代码我前后改过三遍,第一遍只求能在验收时跑通,第二遍把数组和字符串的隐患清掉,第三遍才加上登录和自动保存。三次里最耗时间的不是写新功能,而是查那些“编译通过、运行不报错、结果就是不对”的问题。从那以后我每次动这种以数组下标当主键的程序,都强制先把增、删、改、查四条路径各跑一遍再往下加东西。希望帮到你。

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

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

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

立即咨询