实验六

实验 6:文件 I/O 与异常处理

说明

阅读文档
请仔细阅读,明确任务要求、提交方式与截止时间。

实验任务说明

  • 任务 1:验证性实验。读懂、运行、理解代码,掌握 I/O 流、异常处理基础操作。
  • 任务 2:设计性实验。参照验证性任务,完成设计、编码。

实践建议

  • 先独立思考、编码,再查工具;缺少“亲手敲→犯错→调试→改错”环节,训练效果甚微。
  • 随文档提供了部分代码。可下载、解压缩后使用。如果你不熟悉 C++ 语法,请手动敲入代码、刻意练习。
  • 如有疑问,实验课请及时与老师、同学沟通/讨论。

一、实验目的

  1. 会用标准 I/O 流 (iostream/fstream) 完成控制台和文件的读/写,并处理读/写过程中的异常。
  2. 会用操控符及流成员函数控制数据格式。
  3. 会用 throw/try/catch 及标准库异常类处理异常,并能解释异常处理流程。
  4. 能综合应用封装、继承、多态及现代 C++ 标准库实现一个小型完整应用,确保代码正确,并注意安全、高效、可移植与可扩展。

二、实验准备

浏览/复习以下教材章节:

  • 第 11 章:流类库与输入输出
  • 第 12 章:异常处理

三、实验内容

1. 实验任务 1

说明

验证性实验。综合应用运算符重载、文件 I/O、异常处理、标准库实现一个简单综合应用。运行、理解代码,回答问题。

问题场景描述

校级 ACM 集训队选拔赛后,将学员信息(学号、姓名、专业)与成绩(解题数、总罚时)存入 data.txt
image

要求:

从文件加载参赛选手信息,按照 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();
}

运行结果

读取正确数据的文件:
1success
读取有错误数据的文件:
1error

回答问题

问题 1:流操作与代码复用
观察 print()save() 的实现,均在内部调用 write()

  1. write() 的参数类型是 std::ostream&,它为什么能同时接受 std::coutstd::ofstream 对象作为实参?
  2. 如果要把结果写到其他设备,只要该设备也提供 std::ostream 接口,还需改动 write() 吗?

答:(1)因为std::cout的类型就是std::ostreamstd::ofstreamstd::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. 什么情况下会抛出异常;
  2. 异常被谁捕获、做了哪些处理。

答:(1)在 utils.hpp 的 save()load() 函数中对无法打开文件时均会抛出异常。(2)异常会被task1.cpp中的catch捕获,捕获后通过std::cerr输出报错信息,然后直接返回,避免程序崩溃。

问题 3:替代写法
函数 appstd::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 (内含空白行或字段缺失),重新编译运行:

  1. 观察运行结果有什么问题?给出测试截图并分析原因。
  2. 思考:如何修改函数 load,使其提示出错行号并跳过有问题的数据行,同时让程序继续执行?
    image

答:(1)测试图如运行结果第二张图片所示。格式不对的时候会使ifstream流进入错误状态,也就是(is>>file)的返回值会为falsewhile循环就结束了,所以后续不会继续读入。

(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 中。格式如下:
image

要求:

用面向对象分析、设计,综合运用封装、I/O 流、异常处理编程实现成绩处理。

  • 必做
    • 用户通过菜单选择操作。支持:加载文件、排序、打印到屏幕、保存到文件。
    • 排序规则:专业按字典序、专业内按成绩降序。
    • 打开文件读/写时,如打开失败,要求提供异常处理。
  • 选做*
    • 为数据完整性、有效性(如文件字段是否有缺失、成绩值是否合法)提供异常处理。
预期效果

image

程序应提供如下菜单:

image

代码组织
  • 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,如果不动的话就右键打开新标页签查看)
读取正确数据的情况
2success
读取错误数据的情况
2error_compressed

Student类的函数逻辑非常简单,单纯的输入输出流就行,不过为了对报错信息显示的更加详细,所以在输入流中对每个变量进行单独输入,只要有不匹配的就从这里断开,然后返回带错误信息的is。在StuMgr类中,最核心的load函数的流程大致如下,首先用std::getline()来表示是否读取到了尾部,然后将数据读入字符串流中(因为之前用了std::getline(),已经移动了文件指针,用f>>s的话就会出错,而读取为字符串流也方便进行模式匹配之类的操作),然后将字符串流中的数据输入到s中,其流程在Student的重载>>里,各种报错都会有提示。排序的话直接一个lambda函数按规则返回,其他写入文件的操作与实验一基本一样。


四、实验结论

两个实验主要内容:
异常处理:使用 try-catch 捕获运行时错误(如文件打开失败),提升程序的容错能力 。
输入输出流(文件操作):主要是std::ostream, std::istream, std::ifstream, std::ofstream的应用。
运算符重载:重载 <<>>,使自定义类(如 Student)能直接通过标准流进行读写 。

posted @ 2025-12-17 22:02  cuupe  阅读(4)  评论(0)    收藏  举报