C++ 文件 IO
文件 IO(input/output)是程序和外部数据交互的基本方式——读配置、写日志、处理数据文件。C++ 提供 ifstream(读)、ofstream(写)、fstream(读写)三个类来操作文件,它们和 cin/cout 用法几乎一样(都用 >> << 运算符)。这一章我们看 C++ 文件 IO 的全部要点。
一、C++ 文件 IO 的层次
C++ 提供两套文件 IO:
- 高层(stream):ifstream/ofstream/fstream。用
>><<运算符,自动类型转换,易用。本教程只讲这层。 - 底层(streambuf/filebuf):直接操作字节流,性能更高但难用。
- C 风格:fopen/fread/fwrite。老式,不推荐。
高层 IO 全部基于 RAII——构造时打开,析构时自动关闭,你几乎不用手动调用 close。
二、写文件:ofstream
ofstream(output file stream)用于写文件:
#include <iostream>
#include <fstream> // ifstream/ofstream 都在这里
using namespace std;
int main() {
// ofstream:output file stream,用于写文件
// 构造时打开文件(RAII),析构时自动关闭
ofstream out("hello.txt");
if (!out) {
cerr << "无法打开文件!" << endl;
return 1;
}
// 用 << 运算符写,和 cout 完全一样
out << "Hello, File IO!" << endl;
out << "这是第二行" << endl;
out << "数字: " << 42 << ", 浮点: " << 3.14 << endl;
// 出作用域,out 析构自动关闭文件
// 也可以手动 close:out.close();
return 0;
}要点:
- 构造时打开:RAII,析构自动关闭。
- 检查打开成功:
if (!out)或if (!out.is_open())。常见失败原因:路径不存在、权限不足、磁盘满。 - 用
<<:和 cout 完全一样,自动类型转换。 - 默认覆盖:如果文件存在,默认会截断(覆盖)。要追加用
ios::app。
三、读文件:ifstream
ifstream(input file stream)用于读文件:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main() {
// ifstream:input file stream,用于读文件
ifstream in("hello.txt");
if (!in) {
cerr << "无法打开文件!" << endl;
return 1;
}
// 1. 逐词读取(以空格/换行分隔)
string word;
while (in >> word) {
cout << "词: " << word << endl;
}
in.clear(); // 清除 EOF 标志,才能继续读
in.seekg(0); // 把读指针移回文件开头
// 2. 逐行读取(保留行内空格)
string line;
while (getline(in, line)) {
cout << "行: " << line << endl;
}
return 0;
}两种读取方式对比:
- 逐词
in >> word:以空白(空格、tab、换行)分隔。最常用。 - 逐行
getline(in, line):保留行内空格,适合读带空格的内容(CSV、日志、文本)。 - 读完后 EOF:循环条件
while (in >> word)在 EOF 时返回 false。 - 重读文件:先
clear()(清 EOF 标志),再seekg(0)(指针归零)。
四、文件打开模式
open 时可以指定模式,用 | 组合:
#include <fstream>
using namespace std;
int main() {
// 文件打开模式(可用 | 组合):
// ios::in 读
// ios::out 写(默认截断)
// ios::app append,在末尾追加
// ios::ate 打开后定位到末尾
// ios::trunc 截断(覆盖已有内容)
// ios::binary 二进制模式(不做换行转换)
// 默认:ofstream 用 out|trunc,ifstream 用 in
ofstream o1("a.txt"); // 写,覆盖
ofstream o2("b.txt", ios::app); // 写,追加
ofstream o3("c.txt", ios::out | ios::binary); // 二进制写
ifstream i1("a.txt"); // 读
ifstream i2("data.bin", ios::in | ios::binary); // 二进制读
// fstream:可读可写
fstream fs("d.txt", ios::in | ios::out | ios::app);
return 0;
}常见模式:
- ios::in:读。ifstream 默认。
- ios::out:写。ofstream 默认。
- ios::app:append,在文件末尾追加(不覆盖)。日志文件必备。
- ios::trunc:truncate,打开时清空文件。
- ios::binary:二进制模式,不做换行转换(\r\n vs \n)。跨平台文件必备。
五、读取整个文件
有时你想一次性读完整个文件:
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
using namespace std;
int main() {
ifstream in("hello.txt");
if (!in) return 1;
// 方法 1:用 stringstream 一次性读完整个文件
stringstream ss;
ss << in.rdbuf(); // 把整个文件读进 stringstream
string content = ss.str();
cout << "全文(" << content.size() << " 字节):" << endl;
cout << content << endl;
// 方法 2:C++ 的 string 流读取技巧
in.clear();
in.seekg(0, ios::end);
size_t size = in.tellg(); // 获取文件大小
in.seekg(0, ios::beg);
string buf(size, '\0');
in.read(&buf[0], size); // 一次性读到 buffer
cout << "读到 " << buf.size() << " 字节" << endl;
return 0;
}- 方法 1 stringstream + rdbuf:最优雅,一行读完。
- 方法 2 seekg + read:性能更好,先获取大小再一次性读。
- 注意编码:C++ 文件 IO 不处理编码(UTF-8/GBK),你读到的是原始字节。要处理中文需注意编码。
六、二进制文件
二进制 IO 用于高效存储结构化数据(图片、视频、序列化的 struct):
#include <iostream>
#include <fstream>
#include <vector>
using namespace std;
struct Point {
int x, y;
};
int main() {
// 写二进制文件
{
ofstream out("points.bin", ios::binary);
vector<Point> pts = {{1, 2}, {3, 4}, {5, 6}};
// write 需要const char* + 字节数
out.write(reinterpret_cast<const char*>(pts.data()),
pts.size() * sizeof(Point));
// out 析构自动关闭
}
// 读二进制文件
{
ifstream in("points.bin", ios::binary);
vector<Point> pts(3);
in.read(reinterpret_cast<char*>(pts.data()),
3 * sizeof(Point));
for (const auto& p : pts) {
cout << "(" << p.x << ", " << p.y << ")" << endl;
}
}
return 0;
}
// 注意:
// 1. reinterpret_cast 在二进制 IO 中常见(把对象转成字节流)
// 2. 二进制格式跨平台不兼容(字节序、对齐、类型大小)
// 3. 含指针/STL 容器的类不能直接二进制读写
// 4. 需要跨平台用 JSON/MessagePack/Protobuf 等序列化二进制 IO 的要点:
- 用
ios::binary:不做换行转换(否则在 Windows 上 \n 会被改成 \r\n,数据被破坏)。 - read/write 方法:接受
char*+ 字节数。reinterpret_cast把对象转字节流。 - 跨平台不兼容:字节序(大小端)、类型大小、对齐都可能不同。
- 不能直接存指针/容器:指针存盘后再读,地址已失效;vector 有内部指针,直接存盘读出会崩。
- 需要序列化:跨平台用 JSON/MessagePack/Protobuf 等格式。
七、stringstream:字符串流
stringstream 把"字符串"当"文件"操作,非常有用:
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main() {
// stringstream:字符串流,把字符串当文件操作
// 用途:类型转换、字符串拼接、解析
// 1. 数字转字符串
stringstream ss1;
ss1 << "age=" << 25 << ", pi=" << 3.14;
string s1 = ss1.str();
cout << s1 << endl; // age=25, pi=3.14
// 2. 字符串转数字
stringstream ss2("42 3.14 hello");
int n;
double d;
string w;
ss2 >> n >> d >> w;
cout << n << " " << d << " " << w << endl; // 42 3.14 hello
// 3. 解析 CSV/日志
string logLine = "2024-01-01,INFO,Server started";
stringstream ss3(logLine);
string date, level, msg;
getline(ss3, date, ','); // 用逗号分隔
getline(ss3, level, ',');
getline(ss3, msg);
cout << "[" << date << "][" << level << "] " << msg << endl;
// C++11 还可以直接用 to_string / stoi / stod
// 但 stringstream 更灵活,适合复杂场景
return 0;
}stringstream 的典型用途:
- 类型转换:数字 ↔ 字符串(C++11 后
to_string/stoi替代了部分用法,但复杂场景仍用 stringstream)。 - 字符串拼接:替代一长串
+,性能更好。 - 解析:CSV、日志、配置文件,用
getline(ss, field, ',')按分隔符切分。 - 读取整个文件:配合
ss << in.rdbuf()。
八、文件位置指针(seek)
读写文件时,有个"当前位置指针"。可以手动控制:
#include <iostream>
#include <fstream>
using namespace std;
int main() {
fstream fs("data.txt", ios::in | ios::out | ios::trunc);
if (!fs) return 1;
// 写入一些数据
fs << "ABCDEFGHIJ";
// 文件位置指针(读写位置)
fs.seekp(3); // 把"写指针"移到位置 3(D)
fs << "XX"; // 把 DE 改成 XX,现在文件是 ABCXXFGHIJ
fs.seekg(0); // 把"读指针"移回开头
string content;
fs >> content;
cout << content << endl; // ABCXXFGHIJ
fs.seekg(0, ios::end);
streampos size = fs.tellg(); // 文件大小
cout << "文件大小: " << size << " 字节" << endl;
return 0;
}
// 两个指针:
// - 读指针(seekg/tellg):控制读位置
// - 写指针(seekp/tellp):控制写位置
// fstream 共享一个位置,但 ifstream 只有 g,ofstream 只有 p
// 三个定位参考点:
// ios::beg 文件开头
// ios::cur 当前位置
// ios::end 文件末尾- 读指针:
seekg(seek get)、tellg(tell get)。 - 写指针:
seekp(seek put)、tellp(tell put)。 - fstream 共享一个位置;ifstream 只有 g,ofstream 只有 p。
- 三个参考点:
ios::beg(开头)、ios::cur(当前)、ios::end(末尾)。 - 用途:随机访问文件、读文件大小、修改文件中间内容。
九、错误处理
文件 IO 容易出错(文件不存在、权限不足、磁盘满)。正确处理:
#include <iostream>
#include <fstream>
using namespace std;
int main() {
ifstream in("nonexistent.txt");
// 检查打开是否成功
if (!in) {
cerr << "打开失败" << endl;
// 错误状态位:
// - good():一切正常
// - eof(): 到达文件末尾(EOF)
// - fail(): 操作失败(如类型不匹配),可恢复
// - bad(): 严重错误(如磁盘坏),不可恢复
}
// 流的状态可以用 bool 测试
while (in >> x) { // 读到 EOF 后,!in 为 true,循环结束
}
// 清除错误状态
in.clear();
// 推荐用异常模式处理 IO 错误(可选)
ifstream file("data.txt");
file.exceptions(ifstream::failbit | ifstream::badbit);
// 现在打开失败、读失败都会抛异常
try {
int x;
file >> x;
} catch (const ios_base::failure& e) {
cerr << "IO 异常: " << e.what() << endl;
}
return 0;
}几个状态判断函数:
- good():一切正常。
- eof():到达文件末尾(EOF)。读到末尾是正常状态,不算错误。
- fail():操作失败(如格式错误),可恢复(
clear())。 - bad():严重错误(如磁盘损坏),不可恢复。
- bool 测试:if(in) / while(in >> x) 会自动检查状态。
异常模式(file.exceptions(...))让你不用每次手动检查,出错自动抛异常。但默认不开启。
十、实战技巧
- 用 RAII,不要手动 close:让析构函数做。出作用域自动关闭,异常安全。
- 大文件按块读:不要一次性读完整个 1GB 文件到内存,用循环按块处理。
- 性能瓶颈:磁盘 IO 远慢于内存。批量读写 > 频繁小读写。
- flush 刷新:写完想立刻刷盘,用
out.flush()或endl。默认会自动 flush,但频繁 flush 影响性能。 - 路径处理:用 C++17 的
<filesystem>(跨平台、易用)。 - 处理中文:文件用 UTF-8 编码,string 直接读写没问题,但
cout显示中文在 Windows 控制台可能乱码(需 SetConsoleOutputCP)。
小结
这一章你掌握了 C++ 文件 IO:ofstream 写文件、ifstream 读文件、fstream 读写、打开模式、按行/按词/一次性读、二进制 IO、stringstream 字符串流、seek 文件定位、错误状态判断。恭喜,你已经完成了 C++ 入门系列的全部内容!
系列结语
恭喜你读完了 C++ 入门 16 篇!回顾一下你的旅程:
- 从 Hello World 起步,学会了语法、变量、数据类型。
- 掌握了控制流、函数、引用,能写复杂的逻辑。
- 学懂了面向对象(类、继承、多态)和泛型(模板)。
- 理解了 STL(vector、map、algorithm)、智能指针、异常、文件 IO。
下一步学什么?
- 多线程:std::thread、mutex、atomic、并发编程。
- 更深入 STL:C++17/20/23 新特性、ranges、concepts。
- 实际项目:写一个简易的 Web 服务器、JSON 解析器、游戏。
- 读经典书:Effective C++、C++ Primer、Effective Modern C++。
编程是手艺活,继续边读边敲。Good luck!
← 上一篇 C++ 异常
← 返回 C++ 教程目录