13、文件与异常处理:文件流(fstream)、文本文件与二进制文件读写、异常处理(try/catch/throw)
大家好,我是老李。今天咱们聊聊C++里两个特别实在的话题——文件读写和异常处理。说实话,这两个东西在真实项目中几乎是天天见。你写个工具要保存配置吧?得用文件。你写个数据处理程序,用户给个坏文件怎么办?得用异常处理兜底。
我刚开始学的时候,总觉得文件操作就是打开、读写、关闭,没啥技术含量。直到有一次,我写的一个日志模块在线上跑了三天,突然把硬盘写满了……嗯,从那以后我再也不敢小看文件流了。
13.1 文件流三兄弟:fstream、ifstream、ofstream
C++ 标准库给我们准备了三个文件流类,都在 <fstream> 头文件里。说白了就是:
- ifstream:只管读文件(input file stream)
- ofstream:只管写文件(output file stream)
- fstream:既能读又能写(file stream)
我个人习惯是,能明确读写方向就用 ifstream/ofstream,只有确实需要同时读写时才用 fstream。为啥?因为接口更清晰,不容易搞混。
if (file.is_open()) 或者 if (!file) 判断一下,花不了半秒钟。
13.2 文本文件读写
文本文件,说白了就是你能用记事本打开看的那种。读写方式最直观,用 << 和 >> 就行。
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
// 写入文本文件
ofstream outFile("data.txt");
if (!outFile) {
cerr << "文件打开失败!" << endl;
return 1;
}
outFile << "姓名:张三" << endl;
outFile << "年龄:25" << endl;
outFile << "分数:92.5" << endl;
outFile.close();
// 读取文本文件
ifstream inFile("data.txt");
string line;
while (getline(inFile, line)) {
cout << line << endl;
}
inFile.close();
return 0;
}
这里有个坑,我踩过好几次——用 >> 读取时,遇到空格就停了。比如读取 "Hello World",只能读到 "Hello"。所以读整行一定要用 getline()。
13.3 二进制文件读写
二进制文件,说白了就是按内存中的原始字节来读写。适合存结构体、图片、音频这些数据。读写用 read() 和 write() 成员函数。
#include <iostream>
#include <fstream>
#include <cstring>
using namespace std;
struct Student {
char name[32];
int age;
double score;
};
int main() {
Student stu1 = {"李四", 22, 88.5};
// 写入二进制文件
ofstream outFile("student.dat", ios::binary);
if (!outFile) {
cerr << "打开文件失败" << endl;
return 1;
}
outFile.write(reinterpret_cast<const char*>(&stu1), sizeof(Student));
outFile.close();
// 读取二进制文件
Student stu2;
ifstream inFile("student.dat", ios::binary);
if (!inFile) {
cerr << "打开文件失败" << endl;
return 1;
}
inFile.read(reinterpret_cast<char*>(&stu2), sizeof(Student));
inFile.close();
cout << "姓名:" << stu2.name << endl;
cout << "年龄:" << stu2.age << endl;
cout << "分数:" << stu2.score << endl;
return 0;
}
看到 reinterpret_cast<char*> 别慌。这是 C++ 风格的类型转换,意思就是「把这块内存当字节数组看」。我刚开始也觉得这写法别扭,但用多了就习惯了——它比 C 风格的强制转换安全得多。
- 文本文件:人类可读,跨平台性好,但占空间大,读写慢
- 二进制文件:紧凑高效,读写快,但不可读,跨平台要注意字节序
我的经验是:配置文件、日志用文本;大数据、序列化用二进制。
13.4 文件状态与定位
文件流有几个状态标志位,你得知道:
| 状态函数 | 说明 |
|---|---|
good() |
一切正常,没有错误也没有到达文件尾 |
eof() |
到达文件末尾 |
fail() |
上次操作失败(比如类型不匹配) |
bad() |
发生严重错误(比如磁盘损坏) |
clear() |
重置所有状态标志 |
文件定位也很常用。比如你要读取一个二进制文件的第 N 条记录:
ifstream file("data.bin", ios::binary);
// 跳到第5条记录(假设每条记录100字节)
file.seekg(4 * 100, ios::beg);
// 读取记录
char record[100];
file.read(record, 100);
seekg 是给输入流用的,seekp 是给输出流用的。我当年搞混过一次,debug 了一下午才发现……
13.5 异常处理:try / catch / throw
好了,文件读写说完了,咱们聊聊异常处理。为啥要把这两个放一起?因为文件操作太容易出错了——文件不存在、权限不够、磁盘满了……没有异常处理,程序动不动就崩。
C++ 的异常处理机制就三个关键字:
- try:把可能出错的代码包起来
- catch:捕获并处理异常
- throw:抛出异常(可以抛任何类型)
#include <iostream>
#include <fstream>
#include <stdexcept>
using namespace std;
void readConfig(const string& filename) {
ifstream file(filename);
if (!file) {
throw runtime_error("无法打开配置文件: " + filename);
}
string line;
while (getline(file, line)) {
// 处理配置...
cout << "读取配置: " << line << endl;
}
}
int main() {
try {
readConfig("config.ini");
}
catch (const runtime_error& e) {
cerr << "错误: " << e.what() << endl;
return 1;
}
catch (...) {
cerr << "发生了未知错误" << endl;
return 1;
}
return 0;
}
catch (...)。它就像个垃圾桶,啥都往里扔,但你不知道扔了啥。我一般用它做最后的兜底。
13.6 自定义异常类
有时候标准异常不够用,你可以自己定义。比如文件操作中,我想区分「文件不存在」和「权限不足」:
#include <stdexcept>
#include <string>
using namespace std;
class FileException : public runtime_error {
public:
FileException(const string& msg, int errCode)
: runtime_error(msg), m_errCode(errCode) {}
int getErrorCode() const { return m_errCode; }
private:
int m_errCode;
};
// 使用示例
void openFile(const string& path) {
ifstream file(path);
if (!file) {
if (errno == EACCES) {
throw FileException("权限不足", 13);
}
else {
throw FileException("文件打开失败", errno);
}
}
}
为什么要自定义异常?说白了,就是让调用方能根据不同的错误码做不同的处理。比如权限不足可以提示用户,文件不存在可以自动创建。
13.7 综合实战:带异常处理的配置文件读写
最后来个综合例子,把今天学的串起来:
#include <iostream>
#include <fstream>
#include <string>
#include <map>
#include <stdexcept>
using namespace std;
class ConfigManager {
public:
void load(const string& filename) {
ifstream file(filename);
if (!file) {
throw runtime_error("无法打开配置文件: " + filename);
}
m_data.clear();
string line;
while (getline(file, line)) {
if (line.empty() || line[0] == '#') continue;
size_t pos = line.find('=');
if (pos == string::npos) {
throw runtime_error("配置格式错误: " + line);
}
string key = line.substr(0, pos);
string value = line.substr(pos + 1);
m_data[key] = value;
}
}
string get(const string& key, const string& defaultVal = "") {
auto it = m_data.find(key);
if (it != m_data.end()) {
return it->second;
}
return defaultVal;
}
private:
map<string, string> m_data;
};
int main() {
ConfigManager config;
try {
config.load("app.conf");
cout << "数据库地址: " << config.get("db_host", "localhost") << endl;
cout << "端口: " << config.get("db_port", "3306") << endl;
}
catch (const runtime_error& e) {
cerr << "配置加载失败: " << e.what() << endl;
return 1;
}
return 0;
}
这个例子我实际项目中改过好几版。一开始没加异常处理,用户给个格式不对的配置文件,程序直接崩。后来加了 try/catch,至少能给出友好的错误提示了。
好了,文件与异常处理就聊到这儿。记住一句话:文件操作必检查,异常处理要具体。这两样用好了,你的程序会健壮很多。