← 课程目录
第 23 讲 模块六:进阶与综合

综合项目(二):完成与调试

23 / 24
💡
本节课目标
接着上节课的骨架,补齐查找、删除、统计三个功能, 把六个菜单项全部打通。中间会学一个新工具——从 vector删除元素,最后整体联调,并排查两个典型 bug。
开场

从骨架到成品

📖 理论讲解

上节课留下了什么

上节课完成了:struct Student + vector 的数据设计、 菜单循环骨架、添加显示全部两个功能。

今天要补的三个功能,都建立在同一个基本操作之上—— 先在容器里找到那个人。所以我们先写一个查找的工具函数, 后面查找和删除都靠它。

第一步

工具函数:按姓名找下标

📖 理论讲解

用 -1 表示“没找到”

查找函数返回下标而不是学生本身,因为下标既能用来 读取这个学生,也能用来删除他——一举两得。

那“没找到”怎么表示?合法下标都是 0 或正数, 所以用 -1 这个不可能的下标当作“没找到”的信号:

find_index.cpp CPP
// 找到就返回下标,找不到返回 -1
int findIndex(const std::vector<Student>& students, std::string name) {
    for (int i = 0; i < students.size(); i++) {
        if (students[i].name == name) {
            return i;          // 找到了,立刻交出下标
        }
    }
    return -1;                 // 整个循环走完都没找到
}
return 直接当 break 用
找到之后 return i;,函数立刻结束, 循环自然也就停了——不需要另外写 break。 这是第 12 讲“提前 return”的又一个用法。
💡
为什么 return -1 要写在循环外面
只有整个循环跑完一次都没匹配上,才能断定“没找到”。 如果把 return -1; 写进循环体里,第一个人对不上就直接返回了, 后面的人根本没机会比——这是初学者常犯的错。
第二步

查找功能

📖 理论讲解

有了工具函数,这一步很轻松

find_student.cpp CPP
void findStudent(const std::vector<Student>& students) {
    std::string name;
    std::cout << "请输入要查找的姓名:";
    std::cin >> name;

    int idx = findIndex(students, name);

    if (idx == -1) {
        std::cout << "查无此人。" << std::endl;
    } else {
        std::cout << "找到了:" << students[idx].name
                  << ",分数 " << students[idx].score << std::endl;
    }
}

注意这里只数据,所以参数用 const &

比较字符串直接用 ==
students[i].name == name——std::string 可以直接用 == 比较内容是否相同,和比较数字一样自然。
第三步

新工具:从 vector 删除元素

📖 理论讲解

erase:把某个位置的元素抽走

添加用 push_back,删除用 erase。 稍微特别的是,它要的不是下标数字,而是位置, 写法固定为 容器.begin() + 下标

erase_demo.cpp CPP
std::vector<int> v = {10, 20, 30, 40};

v.erase(v.begin() + 1);   // 删掉下标 1 的元素(也就是 20)

// 现在 v 变成 {10, 30, 40},长度自动从 4 变成 3
// 注意:原本下标 2 的 30,现在变成了下标 1
💡
begin() + 下标,先记住这个写法
students.begin() 表示第一个元素的位置, 加上下标就是目标元素的位置。 背后的原理(迭代器)是第二阶段的内容,现在当成固定搭配记住就行
⚠️
删除会让后面的元素集体前移
删掉一个元素后,它后面的所有元素下标都减 1,容器长度也变短。 这个特性等下会引出一个非常经典的 bug——先记住这句话。
remove_student.cpp CPP
void removeStudent(std::vector<Student>& students) {
    std::string name;
    std::cout << "请输入要删除的姓名:";
    std::cin >> name;

    int idx = findIndex(students, name);

    if (idx == -1) {
        std::cout << "查无此人,无法删除。" << std::endl;
        return;
    }

    students.erase(students.begin() + idx);
    std::cout << "已删除。当前共 " << students.size() << " 名学生。" << std::endl;
}
删除要改容器,所以是 & 不是 const &
removeStudent 会修改 students, 参数必须写 std::vector<Student>&。 对比上面的 findStudent——两个函数参数写法的差别, 正好反映了它们一个只读、一个要改。
第四步

统计功能

📖 理论讲解

第 18 讲的老本行,一次遍历全算完

总分、平均分、最高分都能在同一次遍历里算出来,不用遍历三遍。 注意打擂台这里记的是下标 maxIdx 而不是分数—— 这样最后还能知道最高分是谁

show_stats.cpp CPP
void showStats(const std::vector<Student>& students) {
    if (students.empty()) {
        std::cout << "还没有学生,无法统计。" << std::endl;
        return;
    }

    int sum = 0;
    int maxIdx = 0;          // 打擂台,先假设第一个最高

    for (int i = 0; i < students.size(); i++) {
        sum += students[i].score;
        if (students[i].score > students[maxIdx].score) {
            maxIdx = i;      // 记下标,这样还能知道是谁
        }
    }

    double average = sum / (double)students.size();

    std::cout << "总人数:" << students.size() << std::endl;
    std::cout << "总分:" << sum << std::endl;
    std::cout << "平均分:" << average << std::endl;
    std::cout << "最高分:" << students[maxIdx].name
              << " " << students[maxIdx].score << std::endl;
}
⚠️
空容器一定要先挡住
如果一个学生都没有,students[maxIdx]越界sum / 0 也是灾难。所以开头先 if (students.empty()) 提前 return——处理边界情况要成为写函数的本能
第五步

接进菜单

📖 理论讲解

main 只加三行

这就是上节课强调的好处:功能各自封装成函数后,接进主流程每个只要一行, 已经写好的代码一个字都不用动:

main_final.cpp CPP
int main() {
    std::vector<Student> students;

    while (true) {
        printMenu();

        int choice;
        std::cin >> choice;

        if (choice == 1) {
            addStudent(students);
        } else if (choice == 2) {
            printAll(students);
        } else if (choice == 3) {
            findStudent(students);
        } else if (choice == 4) {
            removeStudent(students);
        } else if (choice == 5) {
            showStats(students);
        } else if (choice == 0) {
            std::cout << "再见!" << std::endl;
            break;
        } else {
            std::cout << "无效选项,请重新输入。" << std::endl;
        }
    }

    return 0;
}
⚠️
别忘了同步更新菜单文字
printMenu 里还只印着三个选项。功能接上了, 菜单也要把 3、4、5 补上,否则用户根本不知道有这些功能。 这是很容易漏掉的一步。
联调

整体测试

⚡ 动手实操 约 20 分钟

完整走一遍流程

把六个功能全部接好,按下面的剧本从头走一遍:

操作期望表现
一上来就选 5 统计提示还没有学生,不崩溃
添加小明 90、小红 85、小刚 58每次都提示当前人数
选 2 显示全部三个人都在
选 3 查找“小红”找到,显示分数 85
选 3 查找“小美”提示查无此人
选 5 统计总分 233,平均分约 77.67,最高分小明 90
选 4 删除“小刚”提示已删除,当前共 2 名
选 2 显示全部只剩小明、小红
选 4 删除“小刚”(再删一次)提示查无此人,不崩溃
选 5 统计按剩下 2 个人重新计算
选 0正常退出
💡
重点看这三条
空容器统计查找不存在的人删除已经删过的人——这三条都是“意外情况”, 也是最容易让程序崩溃的地方。能全部平稳通过,说明程序的健壮性合格了。
调试

两个经典 bug

📖 理论讲解

bug 一:边遍历边删除

假设想加一个“清除所有不及格学生”的功能,很自然会这样写——但它是错的:

bug_delete.cpp CPP
// 想删掉所有不及格的学生——但这样写有 bug
for (int i = 0; i < students.size(); i++) {
    if (students[i].score < 60) {
        students.erase(students.begin() + i);
    }
}

为什么错:假设下标 23 都是不及格的。删掉下标 2 之后,原来下标 3 的人 前移到了下标 2,但循环紧接着 i++ 变成 3—— 那个刚刚前移过来的人被跳过了,永远检查不到。

怎么改:删除成功时不要让 i 前进 (可以在删除后写 i-- 抵消掉 i++), 或者干脆从后往前遍历——从末尾开始删,前移就影响不到还没检查的部分了。

📖 理论讲解

bug 二:输入类型不匹配

菜单要求输入数字,但用户手滑输了个字母会怎样? std::cin >> choice 读取失败,choice 拿不到值, 更麻烦的是那个字母还赖在输入缓冲区里, 导致下一轮循环又立刻读取失败——菜单开始疯狂刷屏,停不下来

怎么办:完整的解决办法要用到 cin.clear()cin.ignore(),属于第二阶段的内容。 现在知道有这个坑就够了——如果测试时不小心触发了刷屏, 按 Ctrl+C 强制结束即可,不是你的逻辑写错了。

调试的通用思路
程序行为不对时,别盯着代码硬看。在可疑的地方插几行 cout,把关键变量的值打印出来(比如 idx 到底是多少、size() 变了没有), 让程序自己告诉你哪一步开始不对——这是最朴素也最管用的调试手段。
拓展

让系统更完善

练习 23-1

拒绝重名

添加学生时,先用 findIndex 检查这个姓名是否已存在, 已存在就提示“该学生已存在”并放弃添加。

练习 23-2

修改分数

新增菜单项 6. 修改分数:输入姓名找到学生后, 读入新分数并更新。

💡
想一想
这个函数该用 & 还是 const &? 判断标准永远是那一条:它会不会改动容器里的数据?
练习 23-3

清除所有不及格

新增一个功能,删除所有低于 60 分的学生——正确地处理上面 bug 一说的陷阱。

提示:两种改法任选,从后往前遍历那种更不容易出错。 测试时一定要构造两个连续的不及格学生,否则这个 bug 根本暴露不出来。

练习 23-4

按等级统计人数

在统计功能里再加一段:分别输出 A / B / C / D / E 五个等级各有多少人

提示:用第 21 讲的 gradeOf 判断等级, 准备五个计数器变量,遍历时对号入座地加一。

收尾

小结

📖 理论讲解

项目完成了

功能关键技术
按姓名查找遍历比对 + 返回下标 / -1
删除学生erase(begin() + idx)
成绩统计一次遍历同时累加与打擂台
健壮性空容器、找不到等边界情况提前拦截

你已经独立完成了一个有菜单、有增删查、有统计,并能处理空容器和查无此人等边界情况的完整程序—— 这已经是一个像模像样的小软件了。

💡
下节课预告
最后一讲:展示你的作品,一起把第一阶段学过的所有知识画成一张知识地图, 再聊聊第二阶段会学什么。