第21章 文件输入输出
第21章 文件输入输出
- 使用fstream进行文本和二进制文件的读写
- 理解并应用文件打开模式与状态检测
- 掌握序列化与反序列化的基本方法
程序运行时的数据都存储在内存里,断电就消失。文件就像程序的“笔记本”,可以把计算结果、用户配置、游戏存档等数据永久保存到硬盘上。在C++中,我们通过fstream(文件流)这个工具来读写文件,它把文件抽象成一个可以顺序读写的字节流。
文件流类比
类比: 文件流就像连接水龙头和水池的水管,水龙头是程序,水池是硬盘上的文件,水就是数据。技术定义: fstream是C++标准库中用于文件操作的类,它继承自iostream,提供了与cin/cout类似的操作符(<<, >>)和方法,用于在程序和外部文件之间传输数据。
文件流家族与打开模式
首先,你需要包含头文件<fstream>。C++提供了三种主要的文件流类,对应不同的操作意图。
| 流类 | 用途 | 继承自 |
|---|---|---|
std::ifstream | 输入文件流,只用于从文件读取数据。 | std::istream |
std::ofstream | 输出文件流,只用于向文件写入数据。 | std::ostream |
std::fstream | 文件流,既可读又可写。 | std::iostream |
创建流对象时,可以指定文件名和打开模式。打开模式决定了文件如何被处理,这是文件操作中第一个容易踩坑的地方。
关键点:文件打开模式
打开模式是std::ios类的静态常量,用位或操作符|组合使用。最常用的组合有:
std::ios::out | std::ios::trunc:写入,如果文件存在则清空内容(默认ofstream行为)。std::ios::out | std::ios::app:写入,追加到文件末尾。std::ios::in:读取(默认ifstream行为)。std::ios::in | std::ios::out:读写,文件必须存在。std::ios::binary:以二进制模式打开,处理非文本数据时必须加上。
我在项目中踩过这个坑:用std::ios::out模式打开一个不存在的文件进行写入,程序会创建它。但如果你用std::ios::in | std::ios::out模式打开一个不存在的文件,流会进入失败状态,后续所有操作都会无效。
文本文件读写实战
文本文件读写最简单,直接使用<<和>>操作符,就像用cout和cin一样。但要注意,>>操作符以空白字符(空格、换行、制表符)作为分隔。
// 示例:写入和读取一个简单的配置文件
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 1. 写入文本文件
std::ofstream outFile("config.txt");
if (!outFile.is_open()) {
std::cerr << "无法打开文件用于写入!" << std::endl;
return 1;
}
outFile << "PlayerName = " << "Alice" << "\n";
outFile << "Level = " << 42 << "\n";
outFile << "Resolution = " << 1920 << "x" << 1080 << std::endl;
outFile.close(); // 显式关闭是好习惯
// 2. 读取文本文件
std::ifstream inFile("config.txt");
std::string key, equalSign, value;
while (inFile >> key >> equalSign >> value) {
std::cout << "Key: " << key << ", Value: " << value << std::endl;
}
// 注意:这里“1920x1080”会被“>>”拆分成两个值
return 0;
}
常见误区
- 不检查文件是否成功打开: 直接进行读写操作,如果文件不存在或权限不足,程序会静默失败。务必使用
is_open()或直接判断流状态(if (inFile))。 - 混淆
>>和getline:operator>>会跳过空白符,不适合读取包含空格的整行文本。读取一行应用std::getline(stream, string_var)。 - 忘记关闭文件或依赖析构: 虽然流对象析构时会自动关闭文件,但在长时间运行或频繁打开文件的程序中,显式调用
close()能及时释放系统资源,避免句柄泄漏。
二进制文件与序列化
当需要保存一个类的对象、一个结构体数组或任何非纯文本数据时,必须使用二进制模式。这个过程称为序列化(把内存对象变成字节流)和反序列化(从字节流重建对象)。
序列化类比
类比: 序列化就像把乐高模型拆成零件清单和拼装图打包进盒子;反序列化就是按照清单和图纸把零件重新拼成模型。技术定义: 序列化是将数据结构或对象状态转换为可以存储或传输的格式(通常是字节序列)的过程,反序列化是其逆过程。
二进制读写使用流对象的read()和write()成员函数。它们的参数是内存地址(char*)和字节数。
struct GameSave {
char playerName[20];
int level;
float health;
// 注意:结构体内不能有string、vector等动态容器!
};
// 二进制写入
GameSave save = {"Bob", 10, 85.5f};
std::ofstream binOut("save.dat", std::ios::binary);
binOut.write(reinterpret_cast<char*>(&save), sizeof(GameSave));
// 二进制读取
GameSave loadedSave;
std::ifstream binIn("save.dat", std::ios::binary);
binIn.read(reinterpret_cast<char*>(&loadedSave), sizeof(GameSave));
编者提示: 上面代码中使用的reinterpret_cast是一种危险的类型转换,它仅仅重新解释内存中的比特位。关于C++中四种类型转换操作符(static_cast, dynamic_cast, const_cast, reinterpret_cast)的安全用法和区别,我们将在第24章《类型转换操作符》中详细剖析。
实际开发中,大部分团队不会直接用二进制写整个结构体,因为存在字节序(Endianness)、内存对齐(Alignment)和指针失效三大问题。对于包含动态数据的类,需要设计自定义的序列化函数。
文件定位与随机访问
文件流内部维护一个“当前位置”指针。对于顺序读写,指针自动后移。但有时我们需要跳转到文件特定位置进行读写,这就是随机访问。
读取大文件中间部分
- 跳过文件头部的元数据。
- 直接读取中间某个数据块。
- 避免加载整个文件到内存。
修改文件特定记录
- 在数据库式文件中定位。
- 读取-修改-写回固定长度记录。
- 要求记录长度固定。
追加日志
- 快速定位到文件末尾。
- 写入新的日志条目。
- 使用
ios::app模式更简单。
控制文件位置指针使用seekg()(用于输入流定位读位置)和seekp()(用于输出流定位写位置)。获取当前位置使用tellg()和tellp()。
std::fstream file("data.bin", std::ios::in | std::ios::out | std::ios::binary);
// 将读指针移动到距文件开头第1024字节处
file.seekg(1024, std::ios::beg);
// 将写指针移动到文件末尾
file.seekp(0, std::ios::end);
long endPos = file.tellp(); // 获取文件总大小
seek的第二个参数是定位基准,有三个选项:std::ios::beg(文件开头),std::ios::cur(当前位置),std::ios::end(文件末尾)。如果你看到程序读取的数据全是乱码,大概率是文件指针定位错了地方,或者忘记以二进制模式打开文件。
动手试一试
创建一个简单的通讯录程序,功能如下:
- 定义
Contact结构体,包含name(字符串)、phone(字符串)、id(整数)成员。 - 实现“添加联系人”功能:将一个新的
Contact对象以文本格式追加写入到contacts.txt文件中,每行存储一个联系人,字段用逗号分隔(例如:张三,13800138000,1)。 - 实现“列出所有联系人”功能:读取
contacts.txt文件,解析每一行,并将联系人信息显示在控制台。 - 挑战: 修改程序,将联系人数据以二进制格式存储到
contacts.dat中。思考如何解决name和phone字符串长度不固定的问题?(提示:可以先写入字符串长度,再写入字符串内容)。
检验你的理解
- 以下代码意图是向
log.txt追加一行日志,但它有错误。请指出错误并修正。std::ofstream logFile("log.txt"); logFile << "Program started." << std::endl; - 二进制模式
std::ios::binary和文本模式的主要区别是什么?在Windows和Linux上,这个区别对换行符(‘\n’)的处理有何不同? - 如果对一个
std::ifstream对象调用write()方法,会发生什么?编译器会报错吗?为什么?
本章小结
- 文件流是桥梁:
ifstream,ofstream,fstream分别用于读、写和读写文件,需配合正确的打开模式使用。 - 文本与二进制是两种世界: 文本文件使用
<<和>>操作符,便于人类阅读;二进制文件使用read()/write(),用于保存原始内存数据,必须处理序列化问题。 - 状态检查是关键: 任何文件操作前后都应检查流状态(
is_open(),!stream.fail()),这是写出健壮程序的基石。 - 指针控制实现随机访问: 通过
seekg()/seekp()和tellg()/tellp()可以在文件中任意位置进行读写,适用于大数据文件或数据库类应用。 - 序列化是核心技能: 将复杂对象(尤其是包含动态内存的对象)持久化到文件,需要设计专门的序列化与反序列化逻辑,不能简单内存拷贝。
微信扫码关注
抖音扫码关注