更新时间: 2026-08-27
58 篇的图书管理练了"一个类 + 一个容器"的骨架。这篇换一种形态:成绩系统要面对的是多条记录 + 多个维度——每个学生有语文、数学、英语三门成绩,要算总分、平均分、排名,还要统计"各分数段有多少人"。这会用到 map、pair、多字段排序和聚合计算。
本文要回答:成绩数据用什么结构组织?多字段排序怎么写?分数段统计用 map 怎么一步到位?
一、需求与设计
需求:
- 学生:姓名、语文、数学、英语成绩
- 功能:录入、按总分排名、按单科排序、统计分数段(优秀/及格/不及格)、查某人的成绩
设计:
@startmindmap
* 学生成绩系统
** Student(数据类)
*** 姓名 + 三门成绩
*** total() / average()
** GradeBook(管理类)
*** vector<Student>
*** add / rank / top_subject / segment / query
** main(菜单循环)
@endmindmap和图书系统同构,但多了"派生数据":总分、平均分、分数段——这些不是用户输入的,是从原始成绩算出来的。设计上要决定"算好存起来"还是"用的时候现算"。这个决定本身就是练习点。
二、代码设计
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
#include <map>
#include <iomanip>
class Student {
public:
Student(std::string name, int chinese, int math, int english)
: name_(std::move(name)), chinese_(chinese), math_(math), english_(english) {}
const std::string& name() const { return name_; }
int chinese() const { return chinese_; }
int math() const { return math_; }
int english() const { return english_; }
int total() const { return chinese_ + math_ + english_; }
double average() const { return total() / 3.0; }
private:
std::string name_;
int chinese_, math_, english_;
};
std::ostream& operator<<(std::ostream& os, const Student& s) {
return os << std::setw(12) << std::left << s.name()
<< std::right
<< " 语文:" << s.chinese()
<< " 数学:" << s.math()
<< " 英语:" << s.english()
<< " 总分:" << s.total();
}
class GradeBook {
public:
void add(const Student& s) { students_.push_back(s); }
// 按总分降序排名
void rank_by_total() {
std::sort(students_.begin(), students_.end(),
[](const Student& a, const Student& b) { return a.total() > b.total(); });
}
// 按指定科目分数排序(参数是成员指针)
void sort_by_subject(int (Student::*getter)() const) {
std::sort(students_.begin(), students_.end(),
[getter](const Student& a, const Student& b) {
return (a.*getter)() > (b.*getter)();
});
}
// 分数段统计:map<档位, 人数>
std::map<std::string, int> segment_stats() const {
std::map<std::string, int> stats{
{"优秀(>=270)", 0}, {"及格(>=180)", 0}, {"不及格(<180)", 0}};
for (const auto& s : students_) {
int t = s.total();
if (t >= 270) stats["优秀(>=270)"]++;
else if (t >= 180) stats["及格(>=180)"]++;
else stats["不及格(<180)"]++;
}
return stats;
}
const Student* find(const std::string& name) const {
auto it = std::find_if(students_.begin(), students_.end(),
[&](const Student& s) { return s.name() == name; });
return it == students_.end() ? nullptr : &(*it);
}
void list() const {
for (const auto& s : students_) std::cout << s << "\n";
}
private:
std::vector<Student> students_;
};知识点盘点:
| 知识点 | 用到的地方 |
|---|---|
成员指针 int (Student::*)() const | sort_by_subject 的科目选择 |
| lambda + sort(39/49) | 排名、排序 |
std::map(36) | 分数段统计 |
operator<< + std::setw | 对齐输出 |
const Student* + nullptr(48) | 查找 |
| 派生数据设计 | total/average 现算 |
sort_by_subject 里的成员指针是 C++ 独有的语法(C 没有),第一次见会有点晕:int (Student::*getter)() const 声明"一个指向 Student 的 const 成员函数、返回 int 的指针",调用时用 (a.*getter)()。它让一个函数能按任意科目排序,不用写三个重载。
三、实验预期
- 录入 4 人后按总分排名:总分从高到低
- 分数段统计:三个档位人数之和 = 总人数
- 查找存在/不存在的学生,返回正确结果
- 平均分正确(用 double)
四、实验数据
实际编译运行输出(g++ 13,-std=c++17):
$ ./grade_sys
===== 学生成绩系统 =====
1) 录入学生 2) 按总分排名 3) 按科目排序
4) 分数段统计 5) 查询学生 6) 列出全部
0) 退出
请选择: 1
姓名: 张伟 语文: 92 数学: 85 英语: 88
已录入: 张伟
请选择: 1
姓名: 李娜 语文: 78 数学: 95 英语: 82
已录入: 李娜
请选择: 1
姓名: 王强 语文: 55 数学: 60 英语: 58
已录入: 王强
请选择: 1
姓名: 赵敏 语文: 98 数学: 99 英语: 96
已录入: 赵敏
请选择: 2
名次 姓名 语文 数学 英语 总分
1 赵敏 98 99 96 293
2 张伟 92 85 88 265
3 李娜 78 95 82 255
4 王强 55 60 58 173
请选择: 3
按科目(1语文 2数学 3英语): 2
姓名 语文 数学 英语 总分
赵敏 98 99 96 293
李娜 78 95 82 255
张伟 92 85 88 265
王强 55 60 58 173
请选择: 4
优秀(>=270): 1 人
及格(>=180): 2 人
不及格(<180): 1 人
请选择: 5
姓名: 王强
王强 55 60 58 173 平均分: 57.67五、实验分析
1. 总分"现算"还是"存起来"?
代码里 total() 每次现场加三个数——开销可忽略。但有个取舍:如果排序很频繁、人数很多,每次比较都调 total() 就有重复计算;极端情况可以在 Student 里缓存总分。"现算"优先,性能确实成为瓶颈再缓存——过早优化是万恶之源,这条经验在后面读性能主题时会反复出现。
2. map 的 [] 副作用
分数段统计用了 stats["优秀(>=270)"]++——map 的 operator[] 对不存在的键会自动插入(36 篇的重点坑)。这里反而利用了它:初始化时已经把所有档位都建好了,[] 只是取值自增。反过来,如果忘了初始化、直接 stats["某档"]++,也会"静默插入"一个档位——所以用之前先想清楚键存不存在。
3. 成员指针的代价
成员指针方案避免了三个重复的排序函数,但可读性一般。团队项目里更常见的做法是枚举 + switch:
enum class Subject { Chinese, Math, English };
void sort_by_subject(Subject subj) {
std::sort(students_.begin(), students_.end(),
[subj](const Student& a, const Student& b) {
switch (subj) {
case Subject::Chinese: return a.chinese() > b.chinese();
case Subject::Math: return a.math() > b.math();
case Subject::English: return a.english() > b.english();
}
return false;
});
}清晰但啰嗦。两种风格各有取舍,练习的目的是"认得成员指针这种写法",而不是非用不可。
六、C 对照
| 模块 | C 版本 | C++ 版本 |
|---|---|---|
| 学生结构 | struct Student | class Student |
| 学生数组 | 定长数组 / 手动扩容 | std::vector |
| 排名 | qsort + 比较函数 | sort + lambda |
| 分数段统计 | 手写循环 + if 计数 | map 键值自增 |
| 按科目排序 | 三个比较函数 | 成员指针 / 枚举 |
| 字符串比较 | strcmp | == |
C 版本里"按语文/数学/英语排序"要写三个几乎一样的比较函数,或者传一个"科目编号"参数在比较函数里 switch——C 的函数指针做不到"参数化访问结构体成员",成员指针是 C++ 的特有解法。
七、扩展练习
- 及格率/优秀率:百分比输出(
std::setprecision) - 多关键字排序:总分相同按语文分再比(比较器里写
a.total() != b.total() ? a.total() > b.total() : a.chinese() > b.chinese()) - 不及格名单:
std::partition或 copy_if 筛出不及格学生 - 成绩单输出到文件:
ofstream+operator<<复用
八、与本站主线衔接
- cpp
// 综合练习 2:学生成绩系统 // 对应文档: languages/cpp/beginner/59-practice-grade-system.md // 编译: g++ -O0 -g -std=c++17 grade_system.cpp -o grade_system #include <iostream> #include <string> #include <vector> #include <algorithm> #include <map> #include <iomanip> #include <utility> class Student { public: Student(std::string name, int chinese, int math, int english) : name_(std::move(name)), chinese_(chinese), math_(math), english_(english) {} const std::string& name() const { return name_; } int chinese() const { return chinese_; } int math() const { return math_; } int english() const { return english_; } int total() const { return chinese_ + math_ + english_; } double average() const { return total() / 3.0; } private: std::string name_; int chinese_, math_, english_; }; std::ostream& operator<<(std::ostream& os, const Student& s) { return os << std::setw(12) << std::left << s.name() << std::right << " 语文:" << s.chinese() << " 数学:" << s.math() << " 英语:" << s.english() << " 总分:" << s.total(); } class GradeBook { public: void add(const Student& s) { students_.push_back(s); } void rank_by_total() { std::sort(students_.begin(), students_.end(), [](const Student& a, const Student& b) { return a.total() > b.total(); }); } void sort_by_subject(int (Student::*getter)() const) { std::sort(students_.begin(), students_.end(), [getter](const Student& a, const Student& b) { return (a.*getter)() > (b.*getter)(); }); } std::map<std::string, int> segment_stats() const { std::map<std::string, int> stats{ {"优秀(>=270)", 0}, {"及格(>=180)", 0}, {"不及格(<180)", 0}}; for (const auto& s : students_) { int t = s.total(); if (t >= 270) stats["优秀(>=270)"]++; else if (t >= 180) stats["及格(>=180)"]++; else stats["不及格(<180)"]++; } return stats; } const Student* find(const std::string& name) const { auto it = std::find_if(students_.begin(), students_.end(), [&](const Student& s) { return s.name() == name; }); return it == students_.end() ? nullptr : &(*it); } void list() const { for (const auto& s : students_) std::cout << s << "\n"; } auto begin() const { return students_.begin(); } auto end() const { return students_.end(); } std::size_t size() const { return students_.size(); } private: std::vector<Student> students_; }; int main() { GradeBook book; int choice; std::string name; std::cout << "===== 学生成绩系统 =====\n"; while (true) { std::cout << "1) 录入学生 2) 按总分排名 3) 按科目排序\n" "4) 分数段统计 5) 查询学生 6) 列出全部 0) 退出\n请选择: "; std::cin >> choice; std::cin.ignore(); if (choice == 0) break; switch (choice) { case 1: { int c, m, e; std::cout << "姓名: "; std::getline(std::cin, name); std::cout << "语文: "; std::cin >> c; std::cout << "数学: "; std::cin >> m; std::cout << "英语: "; std::cin >> e; std::cin.ignore(); book.add(Student(name, c, m, e)); std::cout << "已录入: " << name << "\n"; break; } case 2: book.rank_by_total(); std::cout << "名次 姓名 语文 数学 英语 总分\n"; { int rank = 1; for (const auto& s : book) { std::cout << rank++ << " " << s << "\n"; } } break; case 3: { int subj; std::cout << "按科目(1语文 2数学 3英语): "; std::cin >> subj; std::cin.ignore(); if (subj == 1) book.sort_by_subject(&Student::chinese); else if (subj == 2) book.sort_by_subject(&Student::math); else book.sort_by_subject(&Student::english); book.list(); break; } case 4: for (const auto& [k, v] : book.segment_stats()) std::cout << k << ": " << v << " 人\n"; break; case 5: std::cout << "姓名: "; std::getline(std::cin, name); if (const Student* s = book.find(name)) std::cout << *s << " 平均分: " << s->average() << "\n"; else std::cout << "未找到: " << name << "\n"; break; case 6: book.list(); std::cout << "共 " << book.size() << " 人\n"; break; default: std::cout << "无效选项\n"; } } return 0; } - 分数段统计这类"聚合计算"在数据库 SQL 里对应
GROUP BY(本站database/域),入门阶段先用手写 map 理解"分组统计"的本质 - 下一篇综合练习是简易计算器,用上 40 篇的
stack
九、一句话总结
学生成绩系统练习了"派生数据现算"的设计取舍、sort + lambda 的多字段排序、map 的分数段聚合(顺带复习 operator[] 的自动插入副作用),以及成员指针/枚举两种按科目排序的方案——增删查改之外的"计算与统计"能力是这轮的练习重点。