在 C++ 中,想要把程序产生的数据保存到文本文件中,我们主要使用 <fstream> 头文件中的 std::ofstream。它的用法与 std::cout 非常相似:发给 std::cout 的数据会显示在屏幕上,发给 std::ofstream 的数据则会写入磁盘文件中。
向文件写入数据时,我们需要先分清 “文件中原有的旧内容怎么处理”。根据实际需求的不同,写入文件一般分为 2 种方式:一种是 “默认覆盖写入”,另一种是配合 std::ios::app 的 “追加写入”。
使用 ofstream 覆盖写入文件
定义 std::ofstream 对象并打开目标文件后,我们可以直接使用 << 运算符把字符串、数字或各类变量写入文件中。
语法:
std::ofstream file("文件路径");
file << 数据1 << 数据2;说明:
如果指定的目标文件不存在,std::ofstream 会在当前工作目录下自动新建该文件。如果目标文件已经存在,默认情况下会先清空里面的旧内容,再从头写入新数据。
需要注意的是,<< 运算符不会在两次写入之间自动添加空格或换行。如果我们希望写入的数据在文件中分成多行保存,记得手动在末尾加上换行符 \n。
示例 1:向文件写入文本与变量
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 1. 打开文件并写入文本与变量
std::ofstream outFile("user.txt");
if (!outFile.is_open()) {
std::cout << "文件打开失败\n";
return 1;
}
std::string name = "Lucy";
int id = 1002;
double score = 96.5;
outFile << "学号:" << id << '\n';
outFile << "姓名:" << name << '\n';
outFile << "成绩:" << score << '\n';
outFile.close();
// 2. 读取文件验证写入结果
std::ifstream inFile("user.txt");
std::string line;
while (std::getline(inFile, line)) {
std::cout << line << '\n';
}
return 0;
}运行结果如下。
学号:1002
姓名:Lucy
成绩:96.5分析:
在这个例子中,outFile << "学号:" << id << '\n'; 的写法与 std::cout 完全一致。我们既可以直接写入字符串字面量,也可以把 int、std::string、double 等不同类型的变量连接起来写入文件。
写完数据后,我们调用 outFile.close() 关闭输出流,确保数据已经完整保存到磁盘上。接着采用上一节 “C++ 逐行读取文件” 的写法把文件内容读出来,可以看到 3 行数据都已经准确存入 user.txt 中。
接下来我们再做一个实验:当 user.txt 里面已经保存了 3 行旧数据时,再次使用默认的 std::ofstream 打开它并写入一行新内容,看看文件中的内容会发生什么变化。
示例 2:默认打开会覆盖旧内容
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 1. 写入 3 行初始数据
std::ofstream initFile("user.txt");
initFile << "学号:1002\n";
initFile << "姓名:Lucy\n";
initFile << "成绩:96.5\n";
initFile.close();
// 2. 默认方式重新打开并写入
std::ofstream outFile("user.txt");
if (!outFile.is_open()) {
std::cout << "文件打开失败\n";
return 1;
}
outFile << "状态:已更新\n";
outFile.close();
// 3. 读取覆盖后的内容
std::ifstream inFile("user.txt");
std::string line;
while (std::getline(inFile, line)) {
std::cout << line << '\n';
}
return 0;
}运行结果如下。
状态:已更新分析:
从运行结果可以看出,当我们第 2 次打开已经存在的 user.txt 并写入数据时,原本保存在里面的 3 行旧数据已经全部消失了,只剩下新写入的 "状态:已更新"。这就是 “默认覆盖写入” 的效果。
为什么默认打开会把旧内容清空呢?这是因为 std::ofstream 默认同时开启了 std::ios::out 和 std::ios::trunc 模式,其中 trunc 是 “truncate(截断)” 的缩写。
这里有一个细节需要大家特别留意:旧内容是在 std::ofstream 成功打开文件的那一刻就被清空了,而不是等到执行 << 写入语句时才被替换。也就是说,如果我们以默认方式打开了一个已有文件,哪怕后面一行内容都没写就直接关闭,原来的文件也会变成空白。
注意 在练习默认覆盖写入时,小伙伴们记得使用专门新建的测试文件。千万不要拿电脑里真实的配置文件、源代码或重要文档做测试,以免在打开文件的瞬间清空原有数据。
使用 ios::app 追加写入文件
在记录运行日志等场景下,我们不希望抹掉文件原有的内容,而是想把新数据继续写到文件的最末尾。遇到这种情况,我们只需要在打开文件时显式传入第 2 个参数 std::ios::app 即可。
语法:
std::ofstream file("文件路径", std::ios::app);说明:
其中,app 是 “append(追加)” 的缩写。指定 std::ios::app 模式后,每次写入数据时程序都会自动定位到文件末尾追加内容,不会清空原有数据。如果指定的目标文件原本不存在,它同样会自动新建该文件。
示例 3:向文件末尾追加内容
#include <fstream>
#include <iostream>
#include <string>
int main() {
// 1. 写入初始日志
std::ofstream initFile("app.log");
initFile << "program started\n";
initFile.close();
// 2. 追加模式打开并继续写入
std::ofstream outFile("app.log", std::ios::app);
if (!outFile.is_open()) {
std::cout << "文件打开失败\n";
return 1;
}
outFile << "user logged in\n";
outFile << "task finished\n";
outFile.close();
// 3. 读取追加后的全部日志
std::ifstream inFile("app.log");
std::string line;
while (std::getline(inFile, line)) {
std::cout << line << '\n';
}
return 0;
}运行结果如下。
program started
user logged in
task finished分析:
std::ofstream outFile("app.log", std::ios::app); 表示以追加模式打开 app.log。因此文件中原有的 "program started" 完好保留,新写入的两行日志依次排在了它的后面。
在实际开发中,这两种方式的分工非常明确。导出统计结果或保存最新配置时,我们使用默认的 std::ofstream 覆盖写入;记录运行日志或操作流水时,则传入 std::ios::app 追加写入。
常见问题
1. 如果写入路径中包含不存在的文件夹,std::ofstream 会自动创建文件夹吗?
不会。std::ofstream 只会在父文件夹已经存在的前提下,自动创建路径最后的那个文件。如果当前工作目录下没有 data 文件夹,我们直接执行 std::ofstream outFile("data/user.txt"); 就会打开失败,此时 outFile.is_open() 返回 false。
如果需要先在磁盘上自动创建缺失的文件夹,我们可以配合后面学习的 fs::create_directories() 函数先把目录建好,再打开文件写入数据。
2. 为什么执行了 outFile << 数据 之后,程序还没结束时打开文件却看不到刚写入的内容?
这是因为文件流带有 “内存缓冲区” 机制。由于直接读写磁盘比操作内存慢得多,执行 << 时数据会先暂存在内存缓冲区里。只有等缓冲区攒满、调用 outFile.close() 或流对象离开作用域时,数据才会真正写入磁盘。
平时小伙伴们在记录关键运行日志时,如果希望某条数据写完后立刻保存到磁盘上,可以在写入语句后面调用一次 outFile.flush(); 手动刷新缓冲区。
