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:选课系统核心数据流

下面这张图,是我画的核心数据流转逻辑。你看一眼就能明白整个系统是怎么串起来的。

管理员 添加/删除课程 学生 选课/退课/查询 vector<Course> 课程列表(随机访问) 支持按索引快速遍历 map<int, Student> 学号 → 学生信息 O(log n) 快速查找 map<int, set<int>> 学生ID → 课程ID集合 set自动去重+排序 set<TimeSlot> 冲突检测 利用set自动排序,O(log n)判断时间重叠 选课成功 / 冲突提示 / 名额已满

关键代码实现

好,咱们直接上代码。我挑几个最核心的模块来讲。

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::findset::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