vector
里删除元素,最后整体联调,并排查两个典型 bug。
vector
里删除元素,最后整体联调,并排查两个典型 bug。
上节课完成了:struct Student + vector 的数据设计、
菜单循环骨架、添加和显示全部两个功能。
今天要补的三个功能,都建立在同一个基本操作之上—— 先在容器里找到那个人。所以我们先写一个查找的工具函数, 后面查找和删除都靠它。
查找函数返回下标而不是学生本身,因为下标既能用来 读取这个学生,也能用来删除他——一举两得。
那“没找到”怎么表示?合法下标都是 0 或正数,
所以用 -1 这个不可能的下标当作“没找到”的信号:
// 找到就返回下标,找不到返回 -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 i;,函数立刻结束,
循环自然也就停了——不需要另外写 break。
这是第 12 讲“提前 return”的又一个用法。
return -1; 写进循环体里,第一个人对不上就直接返回了,
后面的人根本没机会比——这是初学者常犯的错。
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
可以直接用 == 比较内容是否相同,和比较数字一样自然。
添加用 push_back,删除用 erase。
稍微特别的是,它要的不是下标数字,而是位置,
写法固定为 容器.begin() + 下标:
std::vector<int> v = {10, 20, 30, 40};
v.erase(v.begin() + 1); // 删掉下标 1 的元素(也就是 20)
// 现在 v 变成 {10, 30, 40},长度自动从 4 变成 3
// 注意:原本下标 2 的 30,现在变成了下标 1 students.begin() 表示第一个元素的位置,
加上下标就是目标元素的位置。
背后的原理(迭代器)是第二阶段的内容,现在当成固定搭配记住就行。
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;
} removeStudent 会修改 students,
参数必须写 std::vector<Student>&。
对比上面的 findStudent——两个函数参数写法的差别,
正好反映了它们一个只读、一个要改。
总分、平均分、最高分都能在同一次遍历里算出来,不用遍历三遍。
注意打擂台这里记的是下标 maxIdx 而不是分数——
这样最后还能知道最高分是谁:
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——处理边界情况要成为写函数的本能。
这就是上节课强调的好处:功能各自封装成函数后,接进主流程每个只要一行, 已经写好的代码一个字都不用动:
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 补上,否则用户根本不知道有这些功能。
这是很容易漏掉的一步。
把六个功能全部接好,按下面的剧本从头走一遍:
| 操作 | 期望表现 |
|---|---|
| 一上来就选 5 统计 | 提示还没有学生,不崩溃 |
| 添加小明 90、小红 85、小刚 58 | 每次都提示当前人数 |
| 选 2 显示全部 | 三个人都在 |
| 选 3 查找“小红” | 找到,显示分数 85 |
| 选 3 查找“小美” | 提示查无此人 |
| 选 5 统计 | 总分 233,平均分约 77.67,最高分小明 90 |
| 选 4 删除“小刚” | 提示已删除,当前共 2 名 |
| 选 2 显示全部 | 只剩小明、小红 |
| 选 4 删除“小刚”(再删一次) | 提示查无此人,不崩溃 |
| 选 5 统计 | 按剩下 2 个人重新计算 |
| 选 0 | 正常退出 |
假设想加一个“清除所有不及格学生”的功能,很自然会这样写——但它是错的:
// 想删掉所有不及格的学生——但这样写有 bug
for (int i = 0; i < students.size(); i++) {
if (students[i].score < 60) {
students.erase(students.begin() + i);
}
} 为什么错:假设下标 2 和 3
都是不及格的。删掉下标 2 之后,原来下标 3 的人
前移到了下标 2,但循环紧接着 i++ 变成 3——
那个刚刚前移过来的人被跳过了,永远检查不到。
怎么改:删除成功时不要让 i 前进
(可以在删除后写 i-- 抵消掉 i++),
或者干脆从后往前遍历——从末尾开始删,前移就影响不到还没检查的部分了。
菜单要求输入数字,但用户手滑输了个字母会怎样?
std::cin >> choice 读取失败,choice 拿不到值,
更麻烦的是那个字母还赖在输入缓冲区里,
导致下一轮循环又立刻读取失败——菜单开始疯狂刷屏,停不下来。
怎么办:完整的解决办法要用到
cin.clear() 和 cin.ignore(),属于第二阶段的内容。
现在知道有这个坑就够了——如果测试时不小心触发了刷屏,
按 Ctrl+C 强制结束即可,不是你的逻辑写错了。
cout,把关键变量的值打印出来(比如
idx 到底是多少、size() 变了没有),
让程序自己告诉你哪一步开始不对——这是最朴素也最管用的调试手段。
添加学生时,先用 findIndex 检查这个姓名是否已存在,
已存在就提示“该学生已存在”并放弃添加。
新增菜单项 6. 修改分数:输入姓名找到学生后,
读入新分数并更新。
& 还是 const &?
判断标准永远是那一条:它会不会改动容器里的数据? 新增一个功能,删除所有低于 60 分的学生——正确地处理上面 bug 一说的陷阱。
提示:两种改法任选,从后往前遍历那种更不容易出错。 测试时一定要构造两个连续的不及格学生,否则这个 bug 根本暴露不出来。
在统计功能里再加一段:分别输出 A / B / C / D / E 五个等级各有多少人。
提示:用第 21 讲的 gradeOf 判断等级,
准备五个计数器变量,遍历时对号入座地加一。
| 功能 | 关键技术 |
|---|---|
| 按姓名查找 | 遍历比对 + 返回下标 / -1 |
| 删除学生 | erase(begin() + idx) |
| 成绩统计 | 一次遍历同时累加与打擂台 |
| 健壮性 | 空容器、找不到等边界情况提前拦截 |
你已经独立完成了一个有菜单、有增删查、有统计,并能处理空容器和查无此人等边界情况的完整程序—— 这已经是一个像模像样的小软件了。