第十章:文件操作与异常处理——让程序学会读写与容错
文件操作和异常处理,说白了就是让程序能跟外部世界打交道,并且出了岔子还能优雅地处理。我刚开始学C++那会儿,总觉得文件读写挺简单的,不就是打开、读写、关闭嘛。直到有一次我写了个日志系统,程序跑了一周突然崩溃,一查才发现是文件句柄没释放……嗯,从那以后我再也不敢小看文件操作了。
10.1 文件流基础:fstream 三兄弟
C++ 标准库提供了三个文件流类,它们都继承自 iostream:
| 类名 | 用途 | 基类 |
|---|---|---|
ifstream |
输入文件流(读文件) | istream |
ofstream |
输出文件流(写文件) | ostream |
fstream |
文件流(读写均可) | iostream |
我个人习惯用 fstream,因为它最灵活。但如果你只需要读或写,用 ifstream 或 ofstream 更明确,代码可读性也更好。
10.2 文本文件读写
文本文件是最常见的文件格式。我们来看一个完整的例子:
#include <iostream>
#include <fstream>
#include <string>
int main() {
// 写文件
std::ofstream outFile("data.txt");
if (!outFile.is_open()) {
std::cerr << "文件打开失败!" << std::endl;
return -1;
}
outFile << "姓名: 张三" << std::endl;
outFile << "年龄: 25" << std::endl;
outFile << "分数: 92.5" << std::endl;
outFile.close();
// 读文件
std::ifstream inFile("data.txt");
std::string line;
while (std::getline(inFile, line)) {
std::cout << line << std::endl;
}
inFile.close();
return 0;
}
std::getline() 读取整行,比用 >> 操作符更安全。后者遇到空格就停了,容易出问题。
10.3 二进制文件读写
二进制文件读写,我在项目中用得最多的是保存游戏存档和配置文件。二进制文件的好处是:读写速度快,数据紧凑,没有文本格式转换的开销。
#include <iostream>
#include <fstream>
struct Student {
char name[32];
int age;
double score;
};
int main() {
Student stu = {"李四", 22, 88.5};
// 写入二进制文件
std::ofstream outFile("student.dat", std::ios::binary);
outFile.write(reinterpret_cast<char*>(&stu), sizeof(Student));
outFile.close();
// 读取二进制文件
Student readStu;
std::ifstream inFile("student.dat", std::ios::binary);
inFile.read(reinterpret_cast<char*>(&readStu), sizeof(Student));
inFile.close();
std::cout << "姓名: " << readStu.name << std::endl;
std::cout << "年龄: " << readStu.age << std::endl;
std::cout << "分数: " << readStu.score << std::endl;
return 0;
}
10.4 文件打开模式
文件流打开时可以指定模式,用位标志组合:
| 模式 | 含义 |
|---|---|
std::ios::in |
打开用于读取 |
std::ios::out |
打开用于写入(默认会清空文件) |
std::ios::app |
追加模式,写入位置在文件末尾 |
std::ios::binary |
以二进制模式打开 |
std::ios::ate |
打开后定位到文件末尾 |
std::ios::trunc |
如果文件存在,清空内容 |
举个例子:
// 追加写入,不清空原有内容
std::ofstream logFile("log.txt", std::ios::app);
// 读写二进制文件
std::fstream file("data.bin", std::ios::in | std::ios::out | std::ios::binary);
10.5 异常机制:try/catch/throw
文件操作最容易出问题——文件不存在、权限不够、磁盘满了……所以异常处理是必须的。C++ 的异常机制用三个关键字:
try:包裹可能出错的代码catch:捕获并处理异常throw:抛出异常
来看个实际例子:
#include <iostream>
#include <fstream>
#include <stdexcept>
double readScoreFromFile(const std::string& filename) {
std::ifstream file(filename);
if (!file.is_open()) {
throw std::runtime_error("无法打开文件: " + filename);
}
double score;
file >> score;
if (file.fail()) {
throw std::invalid_argument("文件内容格式错误,无法读取分数");
}
return score;
}
int main() {
try {
double score = readScoreFromFile("score.txt");
std::cout << "分数: " << score << std::endl;
}
catch (const std::exception& e) {
std::cerr << "错误: " << e.what() << std::endl;
}
return 0;
}
10.6 标准异常类
C++ 标准库提供了一套异常类体系,都继承自 std::exception:
| 异常类 | 头文件 | 用途 |
|---|---|---|
std::runtime_error |
<stdexcept> |
运行时错误(文件打不开等) |
std::logic_error |
<stdexcept> |
逻辑错误(参数无效等) |
std::invalid_argument |
<stdexcept> |
无效参数 |
std::out_of_range |
<stdexcept> |
越界访问 |
std::bad_alloc |
<new> |
内存分配失败 |
我个人建议:永远用 catch (const std::exception& e) 来捕获所有标准异常。不要用 catch (...),那会连系统异常都吞掉,调试起来很痛苦。
10.7 异常安全与资源管理
文件操作中,最怕的就是异常导致资源泄漏。比如文件打开了,但中间抛了异常,close() 就没执行到。
解决方案有两个:
- RAII 思想:文件流对象的析构函数会自动关闭文件。所以只要在
try块里创建对象,出了作用域就会自动释放。 - 手动管理:在
catch块里关闭文件。但这样代码容易变得臃肿。
我推荐用 RAII,C++ 的设计哲学就是「资源获取即初始化」。你想想看,如果每个文件操作都要手动 close(),那代码得多难看?
// RAII 方式:自动关闭文件
void processFile() {
std::ifstream file("data.txt"); // 构造时打开
if (!file) {
throw std::runtime_error("打开文件失败");
}
// 处理文件...
// 离开作用域时,file 析构,自动关闭
}
10.8 本章知识体系
下面这张图帮你理清文件操作与异常处理的核心脉络:
10.9 避坑指南
最后分享几个我踩过的坑:
- 文件路径问题:Windows 用反斜杠
\,但 C++ 字符串里要写成\\。我建议统一用正斜杠/,跨平台兼容。 - 忘记检查文件是否打开:
if (!file)或if (!file.is_open())一定要写。我曾经偷懒没写,结果程序读了个空文件,查了半天 bug。 - 二进制文件的结构体对齐:不同编译器、不同平台的结构体大小可能不一样。跨平台时用
#pragma pack(push, 1)强制 1 字节对齐。 - 异常不要滥用:频繁抛异常会影响性能。高频调用的函数里,用返回值判断错误更合适。
好了,文件操作和异常处理就讲到这里。记住一句话:写文件要想着「如果写一半断电了怎么办」,读文件要想着「如果文件被删了怎么办」。带着这种心态写代码,你的程序会健壮很多。
公众号:蓝海资料掘金营,微信deep3321