目录
[1. 数据结构选择](#1. 数据结构选择)
[2. 功能模块划分](#2. 功能模块划分)
[1. 文件存储与加载](#1. 文件存储与加载)
[2. 核心数据操作](#2. 核心数据操作)
[3. 数据展示](#3. 数据展示)
[1. 字符串分割技术](#1. 字符串分割技术)
[2. map的使用技巧](#2. map的使用技巧)
[3. 文件格式设计](#3. 文件格式设计)
[4. 错误处理机制](#4. 错误处理机制)
源代码:
cpp
/*
* 头文件部分:包含程序所需的所有标准库头文件
* <iostream> - 输入输出流
* <map> - 映射容器
* <string> - 字符串处理
* <iomanip> - 输入输出格式化
* <climits> - 整数类型限制
* <fstream> - 文件流操作
* <sstream> - 字符串流操作
* <vector> - 动态数组容器
*/
#include <iostream>
#include <map>
#include <string>
#include <iomanip>
#include <climits>
#include <fstream>
#include <sstream>
#include <vector>
using namespace std; // 使用标准命名空间,避免重复写std::
/*
* 学生结构体定义
* 包含学生的基本信息:姓名、性别、年龄、成绩和电话
* 结构体(struct)是一种自定义数据类型,可以包含多个不同类型的数据成员
*/
struct student{
string name; // 学生姓名,使用string类型存储
string sex; // 学生性别
int age; // 学生年龄,整型
int score; // 学生成绩,整型
string tele; // 电话号码,使用string类型存储
};
/*
* 菜单显示函数
* 功能:在控制台显示系统的主菜单界面
* cout是标准输出流对象,endl表示换行并刷新缓冲区
* \n是换行符,=====用于美化菜单显示
*/
void menu(){
cout << "\n===== 学生成绩管理系统 =====" << endl;
cout << "1. 添加学生成绩" << endl;
cout << "2. 查询学生成绩" << endl;
cout << "3. 修改学生成绩" << endl;
cout << "4. 删除学生记录" << endl;
cout << "5. 显示所有学生成绩" << endl;
cout << "6. 统计分析" << endl;
cout << "0. 退出系统" << endl;
cout << "请输入你的选择: ";
}
/*
* 添加学生信息函数
* 参数:student_map - 学生信息的map容器引用
* map是C++中的关联容器,存储键值对(key-value),这里key是学生ID,value是student结构体
* emplace()方法直接在容器内构造元素,比insert更高效
*/
void add(map<int,student>& student_map){
cout<<"请输入学生相关信息:"<<endl;
int id; // 学生ID,作为map的键
student s; // 临时student对象,用于存储输入的数据
cout<<"ID:";
cin>>id; // 从标准输入读取学生ID
cout<<"姓名:";
cin>>s.name; // 读取学生姓名
cout<<"性别:";
cin>>s.sex; // 读取学生性别
cout<<"年龄:";
cin>>s.age; // 读取学生年龄
cout<<"成绩:";
cin>>s.score; // 读取学生成绩
cout<<"电话:";
cin>>s.tele; // 读取学生电话
student_map.emplace(id,s); // 将学生信息插入map容器
cout<<"添加成功!"<<endl; // 操作反馈
}
/*
* 修改学生信息函数
* 参数:student_map - 学生信息的map容器引用
* count()方法检查map中是否存在指定键
* map[key]返回对应值的引用,可以直接修改
*/
void mod(map<int,student>& student_map){
int a; // 用于存储用户输入的要修改的学生ID
if (student_map.empty()) { // empty()检查map是否为空
cout << "暂无学生记录!" << endl;
return;
}
cout<<"请输入ID:";
cin>>a;
if(student_map.count(a)==0){ // count()返回键出现的次数(0或1)
cout << "暂无该ID记录!" << endl;
return;
}else{
student& s=student_map[a]; // 获取该学生的引用
// 显示当前信息
cout << "\n===== 当前学生信息 =====" << endl;
cout << "ID:" << a << endl;
cout << "姓名:" << s.name << endl;
cout << "性别:" << s.sex << endl;
cout << "年龄:" << s.age << endl;
cout << "成绩:" << s.score << endl;
cout << "电话:" << s.tele << endl;
// 输入新信息
cout<<"请修改学生相关信息:"<<endl;
cout<<"姓名:";
cin>>s.name; // 直接修改原数据
cout<<"性别:";
cin>>s.sex;
cout<<"年龄:";
cin>>s.age;
cout<<"成绩:";
cin>>s.score;
cout<<"电话:";
cin>>s.tele;
cout<<"修改成功!"<<endl;
}
}
/*
* 查询学生信息函数
* 功能:根据ID查询并显示学生信息
* 使用了与修改函数类似的查找逻辑
*/
void find(map<int,student>& student_map){
int a; // 存储要查询的学生ID
if (student_map.empty()) {
cout << "暂无学生记录!" << endl;
return;
}
cout<<"请输入ID:";
cin>>a;
if(student_map.count(a)==0){
cout << "暂无该ID记录!" << endl;
return;
}else{
student& s=student_map[a]; // 获取学生引用
// 显示查询结果
cout << "\n===== 学生信息 =====" << endl;
cout << "ID:" << a << endl;
cout << "姓名:" << s.name << endl;
cout << "性别:" << s.sex << endl;
cout << "年龄:" << s.age << endl;
cout << "成绩:" << s.score << endl;
cout << "电话:" << s.tele << endl;
}
}
/*
* 删除学生记录函数
* 功能:根据ID删除学生记录
* erase()方法从map中删除指定键的元素
*/
void del(map<int,student>& student_map){
int a; // 存储要删除的学生ID
if (student_map.empty()) {
cout << "暂无学生记录!" << endl;
return;
}
cout<<"请输入ID:";
cin>>a;
if(student_map.count(a)==0){
cout << "暂无该ID记录!" << endl;
return;
}else{
student& s=student_map[a]; // 获取学生引用
// 显示将被删除的信息
cout << "\n===== 当前学生信息 =====" << endl;
cout << "ID:" << a << endl;
cout << "姓名:" << s.name << endl;
cout << "性别:" << s.sex << endl;
cout << "年龄:" << s.age << endl;
cout << "成绩:" << s.score << endl;
cout << "电话:" << s.tele << endl;
student_map.erase(a); // 从map中删除该学生
cout << "删除成功!" << endl;
}
}
/*
* 显示所有学生信息函数
* 功能:格式化输出所有学生信息
* setw()设置输出字段宽度,实现对齐效果
* 使用结构化绑定(C++17特性)遍历map
*/
void display(map<int,student>& student_map){
if (student_map.empty()) {
cout << "暂无学生记录!" << endl;
return;
}
cout << "\n===== 所有学生列表 =====" << endl;
// 输出表头,setw设置列宽
cout<<setw(5)<<"ID"<<setw(10)<<"姓名"<<setw(5)<<"性别"
<<setw(5)<<"年龄"<<setw(5)<<"成绩"<<setw(15)<<"电话"<<endl;
// 遍历map中的所有元素
// const auto& [id,s] - C++17结构化绑定,将pair解包为id和s
for(const auto& [id,s]:student_map){
// 格式化输出每个字段
cout<<setw(5)<<id // ID,5字符宽
<<setw(10)<<s.name // 姓名,10字符宽
<<setw(5)<<s.sex // 性别,5字符宽
<<setw(5)<<s.age // 年龄
<<setw(5)<<s.score // 成绩
<<setw(15)<<s.tele // 电话,15字符宽
<<endl; // 换行
}
}
/*
* 统计分析函数
* 功能:计算并显示成绩统计信息
* INT_MIN和INT_MAX是<climits>中定义的整型极限值
*/
void statistics(map<int,student>& student_map){
if (student_map.empty()) {
cout << "暂无学生记录!" << endl;
return;
}
float sum=0; // 成绩总和
int count=0; // 学生人数
int max_score = INT_MIN; // 初始化为最小整数
int min_score = INT_MAX; // 初始化为最大整数
int max_id = -1, min_id = -1; // 最高/最低分学生ID
// 遍历所有学生
for(const auto& [id, s] : student_map){
sum += s.score; // 累加成绩
count++; // 计数
// 更新最高分记录
if(s.score > max_score) {
max_score = s.score;
max_id = id;
}
// 更新最低分记录
if(s.score < min_score) {
min_score = s.score;
min_id = id;
}
}
// 输出统计结果
cout << "\n===== 成绩分析 =====" << endl;
cout << "学生总数: " << count << endl;
cout << "平均成绩: " << sum/count << endl; // 注意整数除法问题
cout << "最高分: " << max_score << " (ID: " << max_id << ")" << endl;
cout << "最低分: " << min_score << " (ID: " << min_id << ")" << endl;
}
/*
* 保存数据到文件函数
* 功能:将学生数据保存到文本文件
* ofstream是输出文件流类,用于写入文件
* \t是制表符,用于分隔字段
*/
void save_file(map<int,student>& student_map){
// 1. 创建并打开文件
ofstream outfile("student.txt"); // 默认模式是ios::out
// 2. 检查文件是否成功打开
if(!outfile){
cerr<<"无法打开文件"<<endl; // cerr是标准错误流
return;
}
// 3. 写入表头行
outfile <<"ID\t姓名\t性别\t年龄\t成绩\t电话"<<endl;
// 4. 遍历写入每条记录
for(auto& [id,s]:student_map){
outfile<<id<<"\t"<<s.name<<"\t"<<s.sex<<"\t"
<<s.age<<"\t"<<s.score<<"\t"<<s.tele<<endl;
}
// 5. 关闭文件(析构函数会自动关闭,但显式关闭是好习惯)
outfile.close();
cout<<"数据已经写入文件student.txt"<<endl;
}
/*
* 从文件加载数据函数
* 功能:从文本文件读取学生数据
* ifstream是输入文件流类,用于读取文件
* getline()按行读取,istringstream分割字段
*/
void load_file(map<int,student>& student_map){
// 1. 打开文件
ifstream infile("student.txt");
// 2. 检查文件是否成功打开
if(!infile){
cerr<<"暂时无法打开文件"<<endl;
return;
}
// 3. 清空当前数据
student_map.clear();
// 4. 读取并跳过表头行
string header;
getline(infile,header);
// 5. 逐行读取数据
string line; // 存储每行内容
int line_count=0; // 行计数器
int success_count=0;// 成功加载记录数
while (getline(infile,line)) // 读取一行
{
line_count++;
if(line.empty()){ // 跳过空行
continue;
}
// 5.1 使用字符串流分割字段
istringstream iss(line); // 创建字符串流
vector<string> fields; // 存储分割后的字段
string field; // 临时存储每个字段
// 按制表符分割字段
while(getline(iss,field,'\t')){
fields.push_back(field);
}
// 6. 检查字段数量是否正确(应有6个)
if (fields.size() != 6) {
cerr << "警告:第 " << line_count << " 行字段数量错误 ("
<< fields.size() << " 个字段,应为6个)" << endl;
cerr << "内容: " << line << endl;
continue;
}
try // 尝试解析字段
{
// 解析字段
int id=stoi(fields[0]); // 字符串转整数
student s;
s.name=fields[1];
s.sex=fields[2];
s.age=stoi(fields[3]);
s.score=stoi(fields[4]);
s.tele=fields[5];
// 添加到map
student_map.emplace(id,s);
success_count++;
}
catch(const exception& e) // 捕获异常
{
cerr << "解析第 " << line_count << " 行时出错: " << e.what() << endl;
cerr << "内容: " << line << endl;
}
}
// 7. 关闭文件
infile.close();
// 8. 输出加载结果
if (line_count - success_count > 0) {
cout << "警告:有 " << (line_count - success_count)
<< " 条记录未成功加载" << endl;
}
}
/*
* 主函数
* 功能:程序入口,实现主循环和菜单选择
*/
int main(){
map<int,student> student_map; // 创建学生map容器
// 启动时加载数据
load_file(student_map);
int choice; // 存储用户选择
while(true){ // 主循环
menu(); // 显示菜单
cin >> choice; // 读取用户输入
switch (choice) { // 根据选择调用不同功能
case 0: // 退出
save_file(student_map); // 退出前保存数据
cout <<"感谢使用,再见"<< endl;
return 0; // 结束程序
case 1:
add(student_map);
break;
case 2:
find(student_map);
break;
case 3:
mod(student_map);
break;
case 4:
del(student_map);
break;
case 5:
display(student_map);
break;
case 6:
statistics(student_map);
break;
default:
cout << "无效选项,请重新输入!" << endl;
break;
}
}
return 0;
}
代码详解:
学生成绩管理系统实现详解
下面我将从整体设计思路、关键函数实现原理和核心技术点三个方面详细介绍这个学生成绩管理系统的实现。
一、系统整体设计思路
1. 数据结构选择
-
核心数据结构 :使用
map<int, student>
存储学生数据-
键(key):学生ID(整数),保证唯一性
-
值(value):student结构体,包含完整学生信息
-
-
选择原因:
-
按ID查询效率高(O(log n))
-
自动按ID排序
-
易于实现增删改查
-
2. 功能模块划分
cpp
1. 数据管理核心
- 添加(add)
- 查询(find)
- 修改(mod)
- 删除(del)
2. 数据展示
- 列表显示(display)
- 统计分析(statistics)
3. 持久化存储
- 保存到文件(save_file)
- 从文件加载(load_file)
4. 用户界面
- 菜单系统(menu)
- 主控制循环(main)
二、关键函数实现原理
1. 文件存储与加载
save_file
函数
cpp
void save_file(map<int,student>& student_map){
ofstream outfile("student.txt");
// [错误处理...]
outfile <<"ID\t姓名\t性别\t年龄\t成绩\t电话"<<endl;
for(auto& [id,s]:student_map){
outfile<<id<<"\t"<<s.name<<"\t"<<s.sex<<"\t"
<<s.age<<"\t"<<s.score<<"\t"<<s.tele<<endl;
}
// [关闭文件...]
}
-
实现原理:
-
创建文本文件输出流
-
写入表头行(字段名称)
-
遍历map,将每个学生信息按字段写入,用制表符(
\t
)分隔 -
每行对应一个学生记录
-
-
设计考虑:
-
使用制表符分隔:比逗号更可靠(姓名中可能包含逗号)
-
文本格式:便于人工查看和调试
-
简单直接:不处理复杂转义,假设数据中不包含制表符
-
load_file
函数
cpp
void load_file(map<int,student>& student_map){
ifstream infile("student.txt");
// [错误处理...]
getline(infile,header); // 跳过标题行
while(getline(infile,line)) {
istringstream iss(line);
vector<string> fields;
string field;
while(getline(iss,field,'\t')) {
fields.push_back(field);
}
// [字段解析...]
}
// [关闭文件...]
}
-
实现原理:
-
创建输入文件流
-
跳过标题行
-
逐行读取,使用
istringstream
分割制表符 -
将分割后的字段存入vector
-
解析字段并存入map
-
-
核心技术:
-
字符串分割 :使用
istringstream
+getline
组合-
istringstream
将字符串转为可操作的流 -
getline
的第三个参数指定分隔符(这里是\t
)
-
-
健壮性处理:
-
检查字段数量
-
try-catch捕获转换异常
-
统计成功/失败记录数
-
-
2. 核心数据操作
add
函数
cpp
void add(map<int,student>& student_map){
// [输入ID...]
if(student_map.count(id)>0){
// ID已存在处理
}
student s;
// [输入其他字段...]
student_map.emplace(id,s);
}
-
关键点:
-
先检查ID是否已存在(避免覆盖)
-
使用
emplace
直接构造元素,比insert
更高效 -
未做数据有效性校验(实际应用应添加)
-
mod
函数
cpp
void mod(map<int,student>& student_map){
// [输入ID...]
student& s=student_map[id]; // 获取引用
// [显示当前值...]
// [输入新值...]
// 直接通过引用修改原数据
}
-
关键点:
-
使用引用(
&
)直接修改map中的元素 -
显示当前值让用户知道在修改什么
-
同样缺乏输入验证
-
find
和del
函数
-
共用相同查找逻辑:
cppif(student_map.count(a)==0){ // 不存在处理 } student& s=student_map[a]; // 获取引用
-
设计一致性:所有操作都先检查存在性,保证健壮性
3. 数据展示
display
函数
cpp
if(student_map.count(a)==0){
// 不存在处理
}
student& s=student_map[a]; // 获取引用
-
格式化输出:
-
setw(n)
:设置字段宽度,实现对齐 -
宽度值需要根据实际数据调整
-
适合控制台表格显示
-
statistics
函数
cpp
int max_score = INT_MIN;
int min_score = INT_MAX;
if(s.score > max_score) {
max_score = s.score;
max_id = id;
}
// [类似处理min...]
-
极值查找技巧:
-
初始化max为最小可能值,min为最大可能值
-
确保第一个元素一定会被记录
-
同时记录对应的ID
-
三、核心技术详解
1. 字符串分割技术
为什么这样实现:
cpp
istringstream iss(line);
vector<string> fields;
string field;
while(getline(iss,field,'\t')) {
fields.push_back(field);
}
-
istringstream:
-
将字符串包装成流,可以使用流操作
-
比C风格的
strtok
更安全(不修改原字符串)
-
-
getline分隔:
-
第三个参数指定分隔符(这里是
\t
) -
自动处理连续分隔符
-
不会跳过空字段(如
a\t\tc
会得到["a","","c"])
-
替代方案比较:
方法 | 优点 | 缺点 |
---|---|---|
istringstream+getline | 安全、标准 | 稍慢 |
strtok | 快速 | 不安全、修改原串 |
手动find+substr | 完全控制 | 代码复杂 |
2. map的使用技巧
元素访问方式:
cpp
// 安全访问(不自动插入)
if(student_map.count(id)>0){
student& s = student_map[id];
}
// 直接访问(不存在时会插入)
student& s = student_map[id];
遍历方法:
cpp
// C++17结构化绑定
for(const auto& [id,s] : student_map){}
// 传统方式
for(const auto& pair : student_map){
int id = pair.first;
student s = pair.second;
}
3. 文件格式设计
文本格式选择原因:
-
可读性强:可以直接用文本编辑器查看
-
调试方便:容易发现数据问题
-
跨平台:不同系统都能处理
-
兼容性:其他程序(如Excel)可以直接打开
潜在改进:
-
添加引号处理:
"张三,李四"\t男\t20
-
转义字符:处理字段中的制表符
-
CSV格式:更通用但需要处理逗号
4. 错误处理机制
当前实现:
-
文件打开失败检查
-
字段数量验证
-
stoi转换的try-catch
-
记录加载成功/失败统计
可扩展方向:
-
更详细的错误日志
-
数据校验(年龄范围、电话格式等)
-
恢复机制(跳过错误行继续)
注:该代码是本人自己所写,可能不够好,不够简便,欢迎大家指出我的不足之处。如果遇见看不懂的地方,可以在评论区打出来,进行讨论,或者联系我。上述内容全是我自己理解的,如果你有别的想法,或者认为我的理解不对,欢迎指出!!!如果可以,可以点一个免费的赞支持一下吗?谢谢各位彦祖亦菲!!!!!