平时我们写练习代码时,程序运行完把结果打印到屏幕上,内存里的数据也就跟着清空了。但在实际开发中,软件配置、运行日志和游戏存档这类数据都需要保存到磁盘文件中。只有存进文件,下次启动程序时我们才能把数据重新读出来。
在 C++ 中,程序与磁盘文件交互主要依靠 “文件流”。完整操作一个文件,通常按顺序包含以下 3 个步骤:
- 打开文件:创建文件流对象,让程序与磁盘上的指定文件建立连接。
- 读写数据:通过文件流向文件中写入内容,或者从文件中读取内容。
- 关闭文件:读写结束后断开连接,释放占用的系统资源。
C++ 文件流是什么?
初学的小伙伴看到 “流(stream)” 这个词,可能会觉得有点抽象。其实你可以把它想象成程序与外部文件之间的一条 “数据管道”,数据就像水流一样顺着这条管道进出,如下图所示。

这和我们熟悉的控制台输入输出非常相似。其中,std::cin 负责让数据从键盘流进程序,而 std::cout 负责让数据从程序流向屏幕。文件流的用法跟它们几乎一样,只不过管道的另一头换成了磁盘上的具体文件。
在 C++ 中,如果想要使用文件流,我们需要先通过 #include <fstream> 引入头文件。其中,fstream 是 “file stream(文件流)” 的缩写。<fstream> 标准库提供了 3 种常用的文件流类型,如下表所示。
| 类型名称 | 英文全称 | 数据流向 | 主要用途 |
|---|---|---|---|
| std::ifstream | input file stream | 文件 → 程序 | 读取文件 |
| std::ofstream | output file stream | 程序 → 文件 | 写入文件 |
| std::fstream | file stream | 文件 ↔ 程序 | 同时读写文件 |
怎么快速区分 std::ifstream 和 std::ofstream 呢?我们只需要记住一个原则:永远站在 “程序” 这一边看方向。
数据从文件读进程序叫做 “输入(input)”,所以读取文件使用 std::ifstream。数据从程序写到文件叫做 “输出(output)”,所以写入文件使用 std::ofstream。
在实际开发中,如果只需要读取文件,我们优先选择 std::ifstream;如果只需要写入文件,则优先选择 std::ofstream。把读和写的职责分开,不仅代码意图更清晰,编译器还能在编译阶段帮我们找出误写或误读的错误。
注意 很多初学的小伙伴习惯把 “往文件里写内容” 说成 “向文件输入”,结果误选了 std::ifstream。记住前缀 “i” 和 “o” 都是站在程序角度说的:读进程序使用 std::ifstream,写出到文件使用 std::ofstream,千万不要搞反了。
C++ 文件的写入与读取
只要熟悉了控制台的输入和输出,上手文件读写其实非常轻松。向 std::ofstream 写入数据时,我们直接使用 << 运算符即可。从 std::ifstream 读取数据时,可以使用 >> 运算符,也可以调用 std::getline() 函数读取一整行。
下面我们通过一个完整的例子,先动手跑通 “写入文件 → 关闭文件 → 读取文件” 的基本流程。
示例 1:写入并读取文件
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 1. 打开文件并写入两行文本
std::ofstream outFile("lvye.txt");
outFile << "欢迎来到绿叶网!\n";
outFile << "https://www.lvyenet.com\n";
outFile.close(); // 写完后先关闭文件
// 2. 重新打开文件并读取内容
std::ifstream inFile("lvye.txt");
std::string line1;
std::string line2;
std::getline(inFile, line1);
std::getline(inFile, line2);
std::cout << "第 1 行:" << line1 << '\n';
std::cout << "第 2 行:" << line2 << '\n';
return 0;
}运行结果如下。
第 1 行:欢迎来到绿叶网!
第 2 行:https://www.lvyenet.com分析:
在这个例子中,第 1 步先通过 std::ofstream outFile("lvye.txt"); 打开文件。由于当前工作目录下原本没有 lvye.txt,程序会自动新建该文件。接着我们使用 << 写入两行文本,最后调用 outFile.close() 将文件关闭。
第 2 步通过 std::ifstream inFile("lvye.txt"); 重新打开刚才生成的文件,连续调用两次 std::getline() 把两行文字读入变量 line1 和 line2 并打印出来。
大家在本地运行完这段代码后,也可以去项目运行目录下找找新建的 lvye.txt,直接双击打开看看里面保存的文字。关于读取文件与写入文件的更多细节用法,我们会在后面两节中分别展开介绍。
打开文件与状态检查
在实际操作文件时,除了基本的读写语法,我们还需要掌握打开文件的写法、常用的打开模式,以及如何判断文件是否成功打开。
1. 打开文件的方式与模式
如果只写 std::ofstream file;,此时只是定义了一个空的文件流对象,还没有和磁盘上的任何文件建立关联。在 C++ 中,打开文件主要有 2 种方式:一种是在定义对象时直接传入文件路径,另一种是先定义对象、再调用 open() 成员函数打开。
语法:
文件流类型 file("文件路径");
文件流类型 file("文件路径", 打开模式);说明:
其中,第 2 个参数 “打开模式” 是可选的,省略时会自动采用该文件流的默认模式。除了定义时直接打开,我们也可以先定义空对象再调用 open() 函数。下面 2 种方式是等价的。
// 方式 1(推荐)
std::ofstream file("lvye.txt");
// 方式 2
std::ofstream file;
file.open("lvye.txt");在实际开发中,我们优先推荐使用方式 1,一步到位,代码更加紧凑直观。方式 2 则适合 “先定义好文件流对象,后面再根据条件决定打开哪个文件” 的场景。
当我们需要显式指定第 2 个参数时(比如 std::ofstream file("lvye.txt", std::ios::app);),常用的文件打开模式如下表所示。
| 打开模式 | 英文原词 | 说明 |
|---|---|---|
| std::ios::in | input | 以读取方式打开文件,是 std::ifstream 的默认模式 |
| std::ios::out | output | 以写入方式打开文件,是 std::ofstream 的默认模式 |
| std::ios::app | append | 追加模式,每次写入都添加到文件末尾 |
| std::ios::trunc | truncate | 文件已存在时,打开会先清空原有内容 |
| std::ios::binary | binary | 以二进制方式打开文件,未指定时默认为文本方式 |
上表中的模式小伙伴们先认个脸熟即可,不需要死记硬背。默认情况下,std::ifstream 自动采用 std::ios::in 模式,目标文件不存在时就会打开失败。而 std::ofstream 默认组合了 std::ios::out 与 std::ios::trunc 模式,目标文件不存在时会自动新建,已存在时则会先清空旧内容。
至于支持双向读写的 std::fstream,打开时通常需要使用 | 符号同时指定读写模式,比如写成 std::fstream rwFile("lvye.txt", std::ios::in | std::ios::out);。
不过这种双向模式要求目标文件必须已经存在,而且读取和写入共用同一个内部位置标记。刚写完直接去读,会因为位置停在文件末尾而读不到数据,所以平时把读和写分开交给 std::ifstream 与 std::ofstream 会省心很多。
2. 判断文件是否打开成功
在前面的示例 1 中,文件读写一切顺利。但在真实项目里,如果要读取的文件不存在、路径拼写错了或者没有访问权限,文件就会打开失败。因此在正式读写数据之前,我们需要先检查文件是否成功打开。
语法:
if (!file.is_open()) {
// 处理打开失败
}说明:
file.is_open() 会返回一个 bool 值:如果文件成功打开,返回 true;如果打开失败,则返回 false。配合逻辑非运算符 ! 写成 if (!file.is_open()),一旦发现打开失败就能及时给出提示并提前退出。
示例 2:判断文件是否打开成功
#include <fstream>
#include <iostream>
int main() {
// 尝试打开不存在的文件
std::ifstream file("no_such_file.txt");
if (!file.is_open()) {
std::cout << "文件打开失败\n";
return 1;
}
std::cout << "文件打开成功\n";
return 0;
}运行结果如下。
文件打开失败分析:
由于当前目录下并没有 "no_such_file.txt",file.is_open() 返回 false,!file.is_open() 条件成立,程序打印提示并通过 return 1; 提前退出。在 main() 函数中,返回 0 表示正常结束,返回非零整数表示异常退出。
除了调用 is_open(),我们也可以直接把文件流对象放进 if 条件中检查。在刚打开文件时,下面 2 种方式是等价的。
// 方式 1
if (!file.is_open()) {
// 处理打开失败
}
// 方式 2
if (!file) {
// 处理打开失败
}is_open() 与 !file 的区别
在刚打开文件的那一刻,!file.is_open() 和 !file 的判断结果是一样的。但在后续读写数据的过程中,两者的侧重点有所不同。
file.is_open() 只关心 “文件流当前有没有关联上文件”,而 !file 判断的是 “文件流整体是否处于错误状态”。比如文件已经成功打开,但中途某次读取因格式不匹配而失败时,file.is_open() 依然为 true,!file 则会变为 true。
因此在实际开发中,我们通常在打开文件时使用 !file.is_open() 检查打开状态,而在读写数据后使用 !file 检查本次读写是否成功。
C++ 关闭文件
当文件操作结束后,我们需要把文件关闭以释放系统资源。在 C++ 中,关闭文件一般分为 2 种情况:一种是 “离开作用域自动关闭”,另一种是调用 close() 函数手动关闭。
1. 离开作用域自动关闭(推荐)
可能有些小伙伴会问:“每次用完文件之后,是不是都必须手动写一行 file.close() 去关闭它呢?” 在大多数普通场景下,其实并不需要。
这是因为文件流对象本身就是一个普通的 C++ 局部对象,遵循前面学过的 RAII 机制。当文件流对象离开它所在的花括号 {} 作用域时,析构函数会自动把缓冲区里的数据写入磁盘并关闭文件,比如下面这样:
{
std::ofstream file("lvye.txt");
if (file.is_open()) {
file << "自动关闭测试\n";
}
} // 离开作用域,自动关闭文件因此,如果一个函数只是单纯读取或写入某个文件,操作完函数就结束了,我们更推荐依赖离开作用域时的自动关闭。这样不仅代码更简洁,也不用担心中途提前 return 时漏掉关闭操作。
2. 使用 close() 手动关闭
既然文件流会自动关闭,那什么时候才需要我们手动调用 close() 函数呢?主要有 2 种情况:① 在同一个作用域里刚写完文件,紧接着马上又要读取这个文件(如示例 1);② 准备让同一个文件流对象先断开当前文件,再去打开另一个新文件。
语法:
file.close();说明:
调用 file.close() 会立即将内存缓冲区中的数据刷新到磁盘,并断开当前文件流与文件的连接。关闭之后,我们就可以让该文件流对象再次调用 open() 函数去关联其他文件。
示例 3:关闭文件并切换新文件
#include <fstream>
#include <iostream>
int main() {
std::ofstream file;
// 打开并关闭第 1 个文件
file.open("first.txt");
if (file.is_open()) {
file << "first file\n";
std::cout << "第 1 个文件写入并关闭成功\n";
file.close();
}
// 复用同一个流对象打开第 2 个文件
file.open("second.txt");
if (file.is_open()) {
file << "second file\n";
std::cout << "第 2 个文件写入并关闭成功\n";
file.close();
}
return 0;
}运行结果如下。
第 1 个文件写入并关闭成功
第 2 个文件写入并关闭成功分析:
file.close(); 表示断开流对象 file 与 "first.txt" 的连接,这样后面我们才能继续复用同一个 file 对象调用 file.open("second.txt") 打开第 2 个文件。
小伙伴们要注意,如果一个文件流对象已经打开了某个文件,在关闭它之前不能直接调用 open() 去打开另一个文件。否则第 2 次打开会失败,还会导致该文件流进入错误状态。
