14. C++项目实战:学生选课系统(STL容器应用)
说实话,很多初学者学完STL容器后,最大的困惑就是——这些东西到底能干嘛?vector、map、set学了一堆,真到写项目时,脑子里还是一团浆糊。我个人习惯是,每学一个知识点,就立刻找个真实场景去套用。今天这个学生选课系统,就是STL容器最经典的落地场景之一。
你想想看,选课系统里有什么?学生信息、课程信息、选课记录、冲突检测……这些数据天然就适合用不同的容器来管理。咱们今天就把这个项目拆开揉碎了讲清楚。
项目需求概览
先说说这个系统要干什么:
- 管理员可以添加课程、查看选课情况
- 学生可以查看课程列表、选课、退课
- 系统要自动检测选课冲突(时间重叠)
- 每个学生最多选3门课
嗯,需求不复杂,但足够把STL容器玩明白。
核心数据结构设计
我设计这个系统时,第一件事就是确定用什么容器来存什么数据。这里有个经验:选容器不是凭感觉,而是看你要怎么操作数据。
| 数据 | 容器 | 理由 |
|---|---|---|
| 课程信息 | vector<Course> | 课程数量固定,需要随机访问 |
| 学生信息 | map<int, Student> | 学号作为key,快速查找 |
| 选课记录 | map<int, set<int>> | 学生ID映射到已选课程ID集合 |
| 时间冲突检测 | set<TimeSlot> | 自动排序,快速判断重叠 |
看到没?map和set在这里发挥了关键作用。map负责快速查找,set负责去重和排序。我在项目中遇到过有人用vector来存选课记录,结果每次查冲突都要O(n)遍历,数据一多就卡成PPT。
SVG:选课系统核心数据流
下面这张图,是我画的核心数据流转逻辑。你看一眼就能明白整个系统是怎么串起来的。
关键代码实现
好,咱们直接上代码。我挑几个最核心的模块来讲。
1. 课程类与容器初始化
class Course {
public:
int id;
string name;
int capacity; // 课程容量
int enrolled; // 已选人数
string timeSlot; // 时间段,如 "周一 10:00-12:00"
bool isFull() const { return enrolled >= capacity; }
};
// 课程容器
vector<Course> courses;
// 初始化课程
void initCourses() {
courses.push_back({1, "C++程序设计", 30, 0, "周一 10:00-12:00"});
courses.push_back({2, "数据结构", 25, 0, "周一 14:00-16:00"});
courses.push_back({3, "操作系统", 20, 0, "周二 10:00-12:00"});
courses.push_back({4, "计算机网络", 30, 0, "周三 08:00-10:00"});
}
这里有个小细节:push_back用的是花括号初始化,C++11以后都支持。我见过有人还在用push_back(Course(1, "...")),其实没必要那么啰嗦。
2. 学生选课——核心逻辑
// 选课记录:学生ID → 已选课程ID集合
map<int, set<int>> studentCourses;
// 选课函数
bool enrollCourse(int studentId, int courseId) {
// 检查课程是否存在
if (courseId < 1 || courseId > courses.size()) {
cout << "课程不存在!" << endl;
return false;
}
Course& course = courses[courseId - 1];
// 检查课程是否已满
if (course.isFull()) {
cout << "课程已满!" << endl;
return false;
}
// 检查是否已选过这门课
auto& enrolled = studentCourses[studentId];
if (enrolled.find(courseId) != enrolled.end()) {
cout << "你已经选过这门课了!" << endl;
return false;
}
// 检查选课数量限制(最多3门)
if (enrolled.size() >= 3) {
cout << "最多只能选3门课!" << endl;
return false;
}
// 检查时间冲突
for (int cid : enrolled) {
if (courses[cid - 1].timeSlot == course.timeSlot) {
cout << "时间冲突!与课程" << courses[cid - 1].name << "重叠" << endl;
return false;
}
}
// 选课成功
enrolled.insert(courseId);
course.enrolled++;
cout << "选课成功!" << endl;
return true;
}
这段代码里,set::find和set::insert都是O(log n)操作。你想想看,如果用vector来存已选课程,每次查重都要遍历一遍,数据量大了之后性能差距非常明显。
重点提示:STL容器的选择直接决定了代码的效率和可读性。map+set的组合,在需要「快速查找+自动去重」的场景下,几乎是无可替代的。
3. 退课操作
bool dropCourse(int studentId, int courseId) {
auto it = studentCourses.find(studentId);
if (it == studentCourses.end()) {
cout << "该学生没有选课记录!" << endl;
return false;
}
auto& enrolled = it->second;
if (enrolled.erase(courseId) == 0) {
cout << "你没有选这门课!" << endl;
return false;
}
courses[courseId - 1].enrolled--;
cout << "退课成功!" << endl;
return true;
}
注意erase的返回值——它返回删除的元素个数。如果返回0,说明根本没选过这门课。这个细节我曾经在项目里吃过亏,当时没检查返回值,结果退课失败也没提示,用户一脸懵。
避坑指南
我曾经踩过的坑,你千万别再踩:
- 容器迭代器失效:在遍历map/set时,如果调用了erase,迭代器会失效。正确做法是用
it = container.erase(it)来获取下一个有效迭代器。 - map的[]操作符:
studentCourses[studentId]如果key不存在,会自动插入一个空set。这既是优点也是陷阱——不小心就会创建出空记录。 - vector下标越界:用
courses[courseId - 1]前,一定要先检查courseId是否合法。我建议用at()函数,它会抛出异常而不是静默崩溃。
完整运行示例
int main() {
initCourses();
// 学生1选课
enrollCourse(1001, 1); // 选C++程序设计 → 成功
enrollCourse(1001, 2); // 选数据结构 → 成功
enrollCourse(1001, 3); // 选操作系统 → 成功
enrollCourse(1001, 4); // 再选一门 → 失败(超过3门)
// 学生2选课
enrollCourse(1002, 1); // 选C++程序设计 → 成功
enrollCourse(1002, 1); // 再选同一门 → 失败(已选过)
// 查看学生1的选课情况
cout << "学生1001的选课:";
for (int cid : studentCourses[1001]) {
cout << courses[cid - 1].name << " ";
}
cout << endl;
// 退课
dropCourse(1001, 2); // 退掉数据结构
enrollCourse(1001, 4); // 现在可以选网络了 → 成功
return 0;
}
小技巧:如果你想让选课记录持久化,可以把studentCourses序列化成JSON或二进制文件。我个人习惯用nlohmann/json库,几行代码就能搞定读写。
总结一下
这个项目虽然小,但把STL容器的核心用法都串起来了:
- vector:适合存储数量固定、需要随机访问的数据
- map:适合键值对快速查找
- set:适合自动去重和排序
- 容器组合:map套set,解决复杂关联关系
说白了,STL容器就是C++给咱们准备好的工具箱。你不需要自己造轮子,但得知道什么场景该用哪个工具。这个选课系统你如果能自己手写一遍,STL容器这块基本就稳了。
嗯,今天就聊到这儿。代码都在上面了,建议你打开IDE敲一遍。遇到问题别慌,STL的调试其实有套路——打印容器内容、检查迭代器、确认边界条件,三板斧下去,90%的bug都能定位。
公众号:蓝海资料掘金营,微信deep3321