实验六
实验 6:文件 I/O 与异常处理
说明
阅读文档
请仔细阅读,明确任务要求、提交方式与截止时间。
实验任务说明
- 任务 1:验证性实验。读懂、运行、理解代码,掌握 I/O 流、异常处理基础操作。
- 任务 2:设计性实验。参照验证性任务,完成设计、编码。
实践建议
- 先独立思考、编码,再查工具;缺少“亲手敲→犯错→调试→改错”环节,训练效果甚微。
- 随文档提供了部分代码。可下载、解压缩后使用。如果你不熟悉 C++ 语法,请手动敲入代码、刻意练习。
- 如有疑问,实验课请及时与老师、同学沟通/讨论。
一、实验目的
- 会用标准 I/O 流 (
iostream/fstream) 完成控制台和文件的读/写,并处理读/写过程中的异常。 - 会用操控符及流成员函数控制数据格式。
- 会用
throw/try/catch及标准库异常类处理异常,并能解释异常处理流程。 - 能综合应用封装、继承、多态及现代 C++ 标准库实现一个小型完整应用,确保代码正确,并注意安全、高效、可移植与可扩展。
二、实验准备
浏览/复习以下教材章节:
- 第 11 章:流类库与输入输出
- 第 12 章:异常处理
三、实验内容
1. 实验任务 1
说明
验证性实验。综合应用运算符重载、文件 I/O、异常处理、标准库实现一个简单综合应用。运行、理解代码,回答问题。
问题场景描述
校级 ACM 集训队选拔赛后,将学员信息(学号、姓名、专业)与成绩(解题数、总罚时)存入 data.txt。

要求:
从文件加载参赛选手信息,按照 ACM 排序规则处理后,输出到屏幕,同时保存到文件。
代码组织
contestant.hpp: 结构体 Contestant 定义及其重载运算符函数>>和<<实现utils.hpp: 工具函数实现(排序函数、数据读/写)task1.cpp: 应用代码 + main
代码
contestant.hpp
#pragma once
#include <iomanip>
#include <iostream>
#include <string>
struct Contestant {
std::string id; // 学号
std::string name; // 姓名
std::string major; // 专业
int solved; // 解题数
int penalty; // 总罚时
};
// 重载 <<
// 要求: 姓名/专业里不含空白符
inline std::ostream& operator<<(std::ostream& out, const Contestant& c) {
out << std::left;
out << std::setw(15) << c.id
<< std::setw(15) << c.name
<< std::setw(15) << c.major
<< std::setw(10) << c.solved
<< std::setw(10) << c.penalty;
return out;
}
// 重载 >>
inline std::istream& operator>>(std::istream& in, Contestant& c) {
in >> c.id >> c.name >> c.major >> c.solved >> c.penalty;
return in;
}
utils.hpp
#pragma once
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>
#include "contestant.hpp"
// ACM 排序规则: 先按解题数降序, 再按罚时升序
inline bool cmp_by_solve(const Contestant& a, const Contestant& b) {
if (a.solved != b.solved)
return a.solved > b.solved;
return a.penalty < b.penalty;
}
// 将结果写至任意输出流
inline void write(std::ostream& os, const std::vector<Contestant>& v) {
for (const auto& x : v)
os << x << '\n';
}
// 将结果打印到屏幕
inline void print(const std::vector<Contestant>& v) {
write(std::cout, v);
}
// 将结果保存到文件
inline void save(const std::string& filename, const std::vector<Contestant>& v) {
std::ofstream os(filename);
if (!os)
throw std::runtime_error("fail to open " + filename);
write(os, v);
}
// 从文件读取信息 (跳过标题行)
inline std::vector<Contestant> load(const std::string& filename) {
std::ifstream is(filename);
if (!is)
throw std::runtime_error("fail to open " + filename);
std::string line;
std::getline(is, line); // 跳过标题
std::vector<Contestant> v;
Contestant t;
int seq;
while (is >> seq >> t)
v.push_back(t);
return v;
}
task1.cpp
#include <algorithm>
#include <iostream>
#include <stdexcept>
#include <vector>
#include "contestant.hpp"
#include "utils.hpp"
const std::string in_file = "./data.txt";
const std::string out_file = "./ans.txt";
void app() {
std::vector<Contestant> contestants;
try {
contestants = load(in_file);
std::sort(contestants.begin(), contestants.end(), cmp_by_solve);
print(contestants);
save(out_file, contestants);
} catch (const std::exception& e) {
std::cerr << e.what() << '\n';
return;
}
}
int main() {
app();
}
运行结果
读取正确数据的文件:

读取有错误数据的文件:

回答问题
问题 1:流操作与代码复用
观察 print() 与 save() 的实现,均在内部调用 write():
write()的参数类型是std::ostream&,它为什么能同时接受std::cout和std::ofstream对象作为实参?- 如果要把结果写到其他设备,只要该设备也提供
std::ostream接口,还需改动write()吗?
答:(1)因为std::cout的类型就是std::ostream,std::ofstream是std::ostream的派生类,他们在标准库中的定义分别如下:
_EXPORT_STD extern "C++" __PURE_APPDOMAIN_GLOBAL _CRTDATA2_IMPORT ostream cout;
_EXPORT_STD using ostream = basic_ostream<char, char_traits<char>>;
_EXPORT_STD using ofstream = basic_ofstream<char, char_traits<char>>;
class basic_ofstream : public basic_ostream<_Elem, _Traits>
(2)不需要,因为write()调用的是std::ostream,用的是这个接口,而与具体设备无关。
问题 2:异常处理与捕获
在代码中找到两处 throw 语句,说明:
- 什么情况下会抛出异常;
- 异常被谁捕获、做了哪些处理。
答:(1)在 utils.hpp 的 save() 和 load() 函数中对无法打开文件时均会抛出异常。(2)异常会被task1.cpp中的catch捕获,捕获后通过std::cerr输出报错信息,然后直接返回,避免程序崩溃。
问题 3:替代写法
函数 app 中 std::sort 的参数 cmp_by_solve 换成下面的 lambda 表达式是否可以?功能、性能、结果是否一致?
[](const Contestant& a, const Contestant& b) {
return a.solved != b.solved ? a.solved > b.solved : a.penalty < b.penalty;
}
答:可以,功能完全一致,性能可能略高,因为使用 lambda 表达式时,编译器通常能更容易地把函数内联展开,从而减少函数调用的开销,结果一致。
问题 4:数据完整性与代码健壮性
把 in_file 改成 ./data_bad.txt (内含空白行或字段缺失),重新编译运行:
- 观察运行结果有什么问题?给出测试截图并分析原因。
- 思考:如何修改函数
load,使其提示出错行号并跳过有问题的数据行,同时让程序继续执行?

答:(1)测试图如运行结果第二张图片所示。格式不对的时候会使ifstream流进入错误状态,也就是(is>>file)的返回值会为false,while循环就结束了,所以后续不会继续读入。
(2)while循环的判断条件改为用std::getline()来判断是否读完,中间的判断过程可以用如果读到了并且正确的话就写入内存,如果不满足要求就不写入,大致如下:
int count_line = 0;
while (std::getline(f, line)) {
++count_line;
if(line.empty()){
continue;
}
if(is >> seq >> t){
v.push_back(t);
}
else{
std::cerr<<"读取第"<<count_line<<"行发生错误";
}
}
2. 实验任务 2
说明
设计性实验。用面向对象编程实现成绩处理。
问题场景描述
某艺术院系学员专业课成绩信息保存在文件 data.txt 中。格式如下:

要求:
用面向对象分析、设计,综合运用封装、I/O 流、异常处理编程实现成绩处理。
- 必做:
- 用户通过菜单选择操作。支持:加载文件、排序、打印到屏幕、保存到文件。
- 排序规则:专业按字典序、专业内按成绩降序。
- 打开文件读/写时,如打开失败,要求提供异常处理。
- 选做*:
- 为数据完整性、有效性(如文件字段是否有缺失、成绩值是否合法)提供异常处理。
预期效果

程序应提供如下菜单:

代码组织
student.hpp: 学员类 Student 及其重载运算符函数声明student.cpp: 学员类 Student 及其重载运算符函数实现(待实现)stumgr.hpp: 学员成绩管理类 Stumgr 声明stumgr.cpp: 学员成绩管理类 Stumgr 实现(待实现)task2.cpp: 应用代码(已提供)
代码
student.hpp
#pragma once
#include <iostream>
#include <string>
class Student {
public:
Student() = default;
~Student() = default;
const std::string get_major() const;
int get_grade() const;
friend std::ostream& operator<<(std::ostream& os, const Student& s);
friend std::istream& operator>>(std::istream& is, Student& s);
private:
int id;
std::string name;
std::string major;
int grade; // 0-100
};
stumgr.hpp
#pragma once
#include <string>
#include <vector>
#include "student.hpp"
class Stumgr {
public:
void load(const std::string& file); // 加载数据文件(空格分隔)
void sort(); // 排序: 按专业字典序升序、同专业分数降序
void print() const; // 打印到屏幕
void save(const std::string& file) const; // 保存到文件
private:
void write(std::ostream &os) const; // 把数据写到任意输出流
private:
std::vector<Student> students;
};
task2.cpp
#include <iostream>
#include <limits>
#include <string>
#include "stumgr.hpp"
const std::string in_file = "./data.txt";
const std::string out_file = "./ans.txt";
void menu() {
std::cout << "\n**********简易应用**********\n"
<< "1.加载文件\n"
<< "2.排序\n"
<< "3.打印到屏幕\n"
<< "4.保存到文件\n"
<< "5.退出\n"
<< "请选择: ";
}
void app() {
Stumgr mgr;
while(true) {
menu();
int choice;
std::cin >> choice;
try {
switch (choice) {
case 1: mgr.load(in_file); std::cout << "加载成功\n"; break;
case 2: mgr.sort(); std::cout << "排序已完成\n"; break;
case 3: mgr.print(); std::cout << "打印已完成\n"; break;
case 4: mgr.save(out_file); std::cout << "导出成功\n"; break;
case 5: return;
default: std::cout << "不合法输入\n";
}
} catch (const std::exception& e) {
std::cout << "Error: " << e.what() << '\n';
}
}
}
int main() {
app();
}
student.cpp
#include "student.hpp"
#include <iomanip>
const std::string Student::get_major() const{
return major;
}
int Student::get_grade() const{
return grade;
}
std::ostream& operator<<(std::ostream& os, const Student& s){
os<<std::left<<std::setw(8)<<s.id<<std::setw(8)<<s.name<<std::setw(8)
<<s.major<<std::setw(8)<<s.grade<<std::endl;
return os;
}
std::istream& operator>>(std::istream& is, Student& s){
if (!(is >> s.id)) {
std::cerr << "读取id发生错误:";
return is;
}
if (!(is >> s.name)) {
std::cerr << "读取name发生错误:";
return is;
}
if (!(is >> s.major)) {
std::cerr<<"读取major发生错误:";
return is;
}
if (!(is >> s.grade)) {
std::cerr << "读取grade发生错误:";
}
return is;
}
stumgr.cpp
#include "stumgr.hpp"
#include <iomanip>
#include <fstream>
#include <sstream>
#include <iostream>
#include <stdexcept>
#include <algorithm>
void StuMgr::load(const std::string& file){
std::ifstream f(file);
if(!f){
throw std::runtime_error("打开文件:" + file + "失败,请检查路径是否正确");
}
std::string line;
std::getline(f, line);
int count_line = 0;
Student s;
while (std::getline(f, line)) {
++count_line;
if (line.empty()) {
continue;
}
std::istringstream iss(line);
std::string t;
if (!(iss >> s)) {
std::cerr << "错误发生在第:" << count_line << "行:" <<
iss.str() << std::endl;
continue;
}
if (s.get_grade() > 100) {
std::cerr<<"不允许的grade" << "错误发生在第:" << count_line << "行:" <<
iss.str() << std::endl;
continue;
}
students.push_back(s);
}
}
void StuMgr::sort(){
if(students.size()){
std::sort(students.begin(), students.end(), [](Student& s1,
Student& s2)->bool{
if(s1.get_major() != s2.get_major())
return s1.get_major() < s2.get_major();
else{
return s1.get_grade() > s2.get_grade();
}
});
}
else{
std::cerr<<"暂无有效数据,无法排序"<<std::endl;
}
}
void StuMgr::print() const{
if(!students.size()){
std::cerr<<"暂无有效数据"<<std::endl;
}
std::cout<<std::left<<std::setw(8)<<"学号"<<std::setw(8)<<"姓名"<<
std::setw(8)<<"专业"<<std::setw(8)<<"排名"<<std::endl;
for(const auto& stu:students){
std::cout<<stu;
}
}
void StuMgr::save(const std::string& file) const{
std::ofstream o(file);
if(!o){
throw std::runtime_error("无法打开文件:" + file);
}
write(o);
}
void StuMgr::write(std::ostream& os) const{
for(const auto& s:students){
os<<s<<'\n';
}
}
运行结果
(两个都是GIF,如果不动的话就右键打开新标页签查看)
读取正确数据的情况

读取错误数据的情况

Student类的函数逻辑非常简单,单纯的输入输出流就行,不过为了对报错信息显示的更加详细,所以在输入流中对每个变量进行单独输入,只要有不匹配的就从这里断开,然后返回带错误信息的is。在StuMgr类中,最核心的load函数的流程大致如下,首先用std::getline()来表示是否读取到了尾部,然后将数据读入字符串流中(因为之前用了std::getline(),已经移动了文件指针,用f>>s的话就会出错,而读取为字符串流也方便进行模式匹配之类的操作),然后将字符串流中的数据输入到s中,其流程在Student的重载>>里,各种报错都会有提示。排序的话直接一个lambda函数按规则返回,其他写入文件的操作与实验一基本一样。
四、实验结论
两个实验主要内容:
异常处理:使用 try-catch 捕获运行时错误(如文件打开失败),提升程序的容错能力 。
输入输出流(文件操作):主要是std::ostream, std::istream, std::ifstream, std::ofstream的应用。
运算符重载:重载 << 和 >>,使自定义类(如 Student)能直接通过标准流进行读写 。

浙公网安备 33010602011771号