文件流:ifstream、ofstream、fstream、文件读写模式、二进制文件操作
文件操作,说白了就是让程序和磁盘上的文件「对话」。我刚开始学C++时,总觉得文件读写不就是几个函数的事吗?直到我在项目中处理一个几十GB的日志文件,程序直接崩溃了……嗯,从那以后我再也不敢小看文件流了。
STL提供的文件流三剑客——ifstream、ofstream、fstream,覆盖了读、写、读写三种场景。今天我们就把它彻底讲透。
1. 三剑客:谁负责什么?
先看一张图,理清它们的关系:
简单来说:
- ifstream:只管读,不能写。适合读取配置文件、日志分析等场景。
- ofstream:只管写,不能读。适合输出结果、保存数据。
- fstream:既能读又能写。但要注意,它默认是读写模式,需要配合正确的打开方式。
我的习惯:能用 ifstream 或 ofstream 就别用 fstream。为什么?接口更清晰,不容易误操作。我在代码审查时见过有人用 fstream 只读打开文件,结果不小心写了脏数据进去……这种 bug 很难查。
2. 文件打开模式:别让模式坑了你
文件打开模式是文件流最容易被忽视的地方。我见过太多人因为模式用错,导致数据丢失或读取失败。
| 模式 | 含义 | 典型场景 |
|---|---|---|
ios::in |
打开文件用于读取 | ifstream 默认模式 |
ios::out |
打开文件用于写入(会清空原有内容) | ofstream 默认模式 |
ios::app |
追加模式,写入位置在文件末尾 | 日志文件追加 |
ios::ate |
打开后定位到文件末尾 | 需要知道文件大小时 |
ios::binary |
以二进制模式打开 | 图片、音频、序列化数据 |
ios::trunc |
如果文件存在,清空内容 | 配合 out 使用 |
我曾经踩过的坑:用 ofstream 写日志,忘了加
ios::app,结果每次运行程序都把之前的日志清空了。排查了半天,最后发现是模式问题。记住:追加日志一定要用 ios::app!
3. 文本文件读写:最基础的操作
文本文件读写是日常开发中最常用的。来看一个完整的例子:
#include <iostream>
#include <fstream>
#include <string>
// 写入文本文件
void writeTextFile(const std::string& filename) {
std::ofstream outFile(filename);
if (!outFile.is_open()) {
std::cerr << "文件打开失败!" << std::endl;
return;
}
outFile << "姓名: 张三" << std::endl;
outFile << "年龄: 28" << std::endl;
outFile << "城市: 北京" << std::endl;
outFile.close(); // 记得关闭
}
// 读取文本文件
void readTextFile(const std::string& filename) {
std::ifstream inFile(filename);
if (!inFile) { // 也可以这样判断
std::cerr << "文件不存在或无法打开!" << std::endl;
return;
}
std::string line;
while (std::getline(inFile, line)) {
std::cout << line << std::endl;
}
inFile.close();
}
int main() {
writeTextFile("info.txt");
readTextFile("info.txt");
return 0;
}
这里有几个细节要注意:
is_open()和operator bool都可以判断文件是否打开成功。我个人习惯用if (!inFile),更简洁。getline()会读取一行,但不包含换行符。如果你需要保留换行符,可以用get()逐字符读取。- 文件用完一定要
close()。虽然析构函数会自动关闭,但显式关闭是良好的编程习惯。
4. 二进制文件操作:处理非文本数据
二进制文件操作是很多初学者的盲区。你想想看,图片、音频、视频、序列化数据,这些都不是纯文本,必须用二进制模式处理。
二进制读写的核心是两个函数:read() 和 write()。它们的签名是:
// 从文件流读取 count 个字节到 buffer
istream& read(char* buffer, streamsize count);
// 将 buffer 中的 count 个字节写入文件流
ostream& write(const char* buffer, streamsize count);
来看一个实际例子:保存和读取结构体数据。
#include <iostream>
#include <fstream>
#include <cstring>
struct Student {
char name[32];
int age;
double score;
};
// 写入二进制文件
void writeBinary(const std::string& filename) {
std::ofstream outFile(filename, std::ios::binary);
if (!outFile) {
std::cerr << "打开文件失败" << std::endl;
return;
}
Student stu;
std::strcpy(stu.name, "李四");
stu.age = 22;
stu.score = 95.5;
// 直接写入结构体内存
outFile.write(reinterpret_cast<const char*>(&stu), sizeof(Student));
outFile.close();
}
// 读取二进制文件
void readBinary(const std::string& filename) {
std::ifstream inFile(filename, std::ios::binary);
if (!inFile) {
std::cerr << "打开文件失败" << std::endl;
return;
}
Student stu;
inFile.read(reinterpret_cast<char*>(&stu), sizeof(Student));
std::cout << "姓名: " << stu.name << std::endl;
std::cout << "年龄: " << stu.age << std::endl;
std::cout << "成绩: " << stu.score << std::endl;
inFile.close();
}
int main() {
writeBinary("student.dat");
readBinary("student.dat");
return 0;
}
重要提醒:二进制文件在不同平台、不同编译器下可能不兼容。比如结构体对齐、字节序(大端/小端)都会影响数据。跨平台传输二进制数据时,建议使用序列化库(如 Protocol Buffers、Boost.Serialization)。
5. 文件流的状态与错误处理
文件操作不可能永远一帆风顺。磁盘满了、权限不够、文件被占用……这些情况都要处理。
文件流提供了几个状态标志:
std::ifstream file("test.txt");
// 检查文件流状态
if (file.good()) {
// 一切正常
}
if (file.eof()) {
// 到达文件末尾
}
if (file.fail()) {
// 上次操作失败(比如类型不匹配)
}
if (file.bad()) {
// 发生严重错误(比如磁盘损坏)
}
// 重置状态
file.clear(); // 清除所有错误标志
我在项目中处理过一个问题:读取一个 CSV 文件时,某一行数据格式不对,failbit 被置位,导致后续所有读取都失败。解决方案就是每次读取后检查状态,必要时 clear() 并跳过错误行。
6. 文件定位:随机访问
有时候我们需要跳到文件的某个位置读写,而不是从头开始。这就用到了文件定位:
std::ifstream file("data.bin", std::ios::binary);
// 获取当前读取位置
std::streampos pos = file.tellg();
// 跳转到第100个字节
file.seekg(100, std::ios::beg);
// 从当前位置向后跳20个字节
file.seekg(20, std::ios::cur);
// 从文件末尾向前跳50个字节
file.seekg(-50, std::ios::end);
// 对于 ofstream,使用 tellp() 和 seekp()
实用技巧:用
seekg(0, std::ios::end) 配合 tellg() 可以快速获取文件大小。这在处理二进制文件时非常有用。
7. 实战建议:文件操作的黄金法则
最后,分享几条我在实战中总结的经验:
- 总是检查文件是否打开成功。不要假设文件一定存在。
- 优先使用 RAII 方式。让文件流在作用域结束时自动关闭,避免忘记 close()。
- 文本文件用 getline() 逐行读取,二进制文件用 read()/write() 按块操作。
- 跨平台时注意路径分隔符。Windows 用反斜杠,Linux/macOS 用正斜杠。建议用正斜杠,或者用
std::filesystem::path。 - 大文件不要一次性读入内存。用缓冲区逐块处理,否则内存会爆。
文件流看似简单,但用好了能解决很多实际问题。我当年做一个数据采集系统,每天要处理上百个日志文件,就是靠文件流配合多线程搞定的。记住:工具不在多,在于用得精。
公众号:蓝海资料掘金营,微信deep3321