在C++程序中,把数据持久化到磁盘文件是最常见的需求之一,无论是保存配置信息、记录运行日志,还是导出计算结果,都离不开文件写入操作。C++标准库提供了fstream这一套流式文件操作工具,配合流插入运算符和write成员函数,几乎可以覆盖所有写入场景。本文将从基础用法入手,逐步讲解文本写入、二进制写入、追加模式以及错误处理等内容,帮助读者系统地掌握C++向文件写入数据的完整方法。

一、使用ofstream进行文本写入
ofstream是C++标准库中专门负责文件输出的类,定义在<fstream>头文件中。它的名字可以拆解为output file stream,含义非常直观:数据从程序流向文件。使用时只需构造一个ofstream对象并指定文件名,就可以像使用cout一样,通过流插入运算符<<把各种类型的数据写进文件。
下面的例子演示了最基本的写入流程:构造对象、写入数据、关闭文件。ofstream的构造函数支持传入文件名和打开模式,若不指定模式,默认为ios::out,即覆盖式写入。如果文件不存在会自动创建,如果文件已存在,则原有内容会被清空,这一点在使用时务必注意。
#include <fstream>
#include <iostream>
int main() {
// 构造ofstream对象并打开文件,默认为覆盖模式
std::ofstream outFile("data.txt");
if (!outFile.is_open()) {
std::cerr << "文件打开失败" << std::endl;
return 1;
}
// 像使用cout一样写入数据
outFile << "姓名:张三" << std::endl;
outFile << "分数:" << 95.5 << std::endl;
// 关闭文件,将缓冲区数据真正写入磁盘
outFile.close();
std::cout << "写入完成" << std::endl;
return 0;
}这段代码执行后,当前目录下会生成data.txt文件,内容为两行文本。需要强调的是,close调用虽然不是强制性的,因为ofstream对象析构时会自动关闭文件,但显式关闭是一个好习惯,特别是在程序异常退出或需要立即读取写入结果的场景下。此外,写入整数、浮点数时流会自动完成格式转换,无需手动拼接字符串,这也是流式写入相对于C风格fprintf的便利之处。
二、追加模式与open函数的进阶用法
实际开发中经常需要向已有文件末尾追加内容,比如写运行日志。此时不能使用默认的覆盖模式,而应传入ios::app标志。app是append的缩写,表示每次写入都会定位到文件末尾。如果同时指定ios::out和ios::app,多个模式之间用按位或运算符连接。
除了在构造函数中指定文件名,也可以先构造一个空对象,再调用open成员函数打开文件。这种方式的好处是同一个流对象可以先后操作多个文件。下面的例子演示了追加写入的完整写法,并向同一个文件写入了三次,每次运行程序都会在文件末尾追加,而不会清空原有内容。
#include <fstream>
#include <iostream>
int main() {
std::ofstream logFile;
// 以追加模式打开,文件不存在则创建
logFile.open("app.log", std::ios::out | std::ios::app);
if (!logFile) {
std::cerr << "打开日志文件失败" << std::endl;
return 1;
}
for (int i = 1; i <= 3; ++i) {
logFile << "第" << i << "条日志记录" << std::endl;
}
logFile.close();
return 0;
}常见的打开模式还有ios::ate(打开后定位到文件末尾,但后续写入位置可变)、ios::trunc(打开时清空文件,与ios::out配合使用)、ios::binary(以二进制模式打开)。判断文件是否成功打开有两种方式:一是调用is_open函数,二是直接对流对象做布尔判断,两种写法效果等价。写入过程中还可以通过good、fail、bad等函数检查流的状态,一旦写入磁盘失败(例如磁盘已满),流的failbit会被置位,后续写入将被忽略,因此关键数据写完后检查流状态很有必要。
三、二进制数据写入与write函数
流插入运算符适合写文本,但保存结构体、数组等原始内存数据时,文本方式既浪费空间又可能丢失精度,这时应使用write成员函数进行二进制写入。write函数接收两个参数:第一个是指向数据的字符指针,第二个是要写入的字节数。由于函数参数类型是const char*,写入其他类型数据时需要做一次强制类型转换。
二进制写入必须搭配ios::binary模式,否则系统会对换行符做转换,导致数据被破坏。以Windows平台为例,文本模式下每个换行符会被写成回车加换行两个字节,而二进制模式则原样输出。下面的例子把一个结构体数组完整写入文件,读取时用对应的read函数按相同结构还原即可。
#include <fstream>
#include <iostream>
#include <cstring>
struct Student {
char name[20];
int age;
double score;
};
int main() {
Student students[2] = {
{"张三", 20, 88.5},
{"李四", 21, 92.0}
};
std::ofstream binFile("students.dat",
std::ios::out | std::ios::binary);
if (!binFile) {
std::cerr << "无法创建二进制文件" << std::endl;
return 1;
}
// 将结构体数组整体写入,sizeof计算总字节数
binFile.write(reinterpret_cast<char*>(students),
sizeof(students));
binFile.close();
// 验证文件大小
std::cout << "写入字节数:" << sizeof(students) << std::endl;
return 0;
}使用reinterpret_cast把Student指针转换为char指针是标准做法,它只是重新解释指针类型,不改变底层数据。需要注意二进制文件的移植性问题:结构体中若包含指针成员,写入的只是地址值而非实际数据,读取后毫无意义;不同编译器的结构体对齐方式也可能不同,跨平台交换二进制文件时要格外小心。建议只对固定大小的POD类型(不含指针、不含虚函数)做整体二进制写入。
四、缓冲区管理与常见问题处理
流的写入并非直接到达磁盘,而是先进入内存缓冲区,待缓冲区满或文件关闭时才真正落盘。多数情况下这对性能是有利的,减少了对磁盘的频繁小规模访问。但在某些场景下需要立即落盘,例如程序可能在写入后立刻崩溃、或者另一个进程要马上读取该文件,这时可以调用flush成员函数强制刷新缓冲区。
#include <fstream>
#include <iostream>
int main() {
std::ofstream file("output.txt");
if (!file) return 1;
file << "重要数据";
// 立即把缓冲区内容刷到磁盘
file.flush();
if (file.good()) {
std::cout << "数据已确保写入磁盘" << std::endl;
}
// endl换行符本身也会触发flush
file << "第二行" << std::endl;
file.close();
return 0;
}这里有一个容易被忽视的细节:std::endl除了输出换行符之外,还会刷新缓冲区,而\n只换行不刷新。在循环中频繁写入时,使用endl会显著拖慢速度,追求性能的场合应改用\n,最后统一flush一次。另一个常见坑是写入中文乱码问题,源文件编码与读取程序的编码不一致会导致乱码,Windows下建议统一使用UTF-8编码并让编辑器保持一致。
最后总结一下选择建议:写普通文本用ofstream加流插入运算符,简单直观;写日志用ios::app追加模式;保存结构化原始数据用write加二进制模式;对落盘时机敏感时记得flush。掌握这几点,C++文件写入的绝大多数需求都能从容应对。