目录
[1. 打开文件](#1. 打开文件)
[2. 检查状态 ------ 重要](#2. 检查状态 —— 重要)
[3. 关闭文件](#3. 关闭文件)
[4. 其他通用接口](#4. 其他通用接口)
[① flush() ------ 强制刷新缓冲区](#① flush() —— 强制刷新缓冲区)
[② swap() ------ 交换两个文件流的状态(C++11)](#② swap() —— 交换两个文件流的状态(C++11))
[③ rdbuf() ------ 获取底层缓冲区指针](#③ rdbuf() —— 获取底层缓冲区指针)
[1. 核心读取接口](#1. 核心读取接口)
[① 流提取运算符 >>](#① 流提取运算符 >>)
[② getline() ------ 读取整行](#② getline() —— 读取整行)
[③ get() ------ 读取单个字符](#③ get() —— 读取单个字符)
[④ read() ------ 二进制块读取](#④ read() —— 二进制块读取)
[⑤ gcount() ------ 获取上次读取的字节数](#⑤ gcount() —— 获取上次读取的字节数)
[⑥ ignore() ------ 跳过指定字符](#⑥ ignore() —— 跳过指定字符)
[⑦ peek() ------ 查看下一个字符](#⑦ peek() —— 查看下一个字符)
[⑧ putback() / unget() ------ 回退字符](#⑧ putback() / unget() —— 回退字符)
[1. 核心写入接口](#1. 核心写入接口)
[① 流插入运算符 <<](#① 流插入运算符 <<)
[② put() ------ 写入单个字符](#② put() —— 写入单个字符)
[③ write() ------ 二进制块写入](#③ write() —— 二进制块写入)
[1. fstream 的读接口](#1. fstream 的读接口)
[2. fstream 的写接口](#2. fstream 的写接口)
[3. fstream 的优势:随机访问](#3. fstream 的优势:随机访问)
在 C++ 编程中,文件操作是一项核心技能。标准库
<fstream>为我们提供了一套强大易用的文件流类:ifstream(读文件)、ofstream(写文件)和fstream(读写文件)。
一、基础概念与文件打开模式
在介绍具体类之前,我们需要先了解 文件打开模式 , 这些模式定义在
std::ios命名空间中,控制文件被打开的方式。
这里的in是读,out是写,个人觉得是有些反直觉的,所以我是这样理解的:
- 以自身为主体
- in就是向我输入,即我读取文件
- out就是我输出,即我向文件写入
| 模式标志 | 含义 | 适用类 | 关键说明 |
|---|---|---|---|
ios::in |
读模式打开 | ifstream, fstream |
文件必须存在,否则打开失败。不能与 ios::trunc 同时使用。 |
ios::out |
写模式打开 | ofstream, fstream |
- ofstream:默认隐含 ios::trunc,文件不存在则创建,存在则清空。- fstream:不隐含 ios::trunc,文件不存在则创建,存在则保留原内容。 |
ios::app |
强制追加模式 | ofstream, fstream |
所有写入操作强制在文件末尾进行 ,即使通过 seekp() 移动了写指针也无效。需配合 ios::out 使用。 |
ios::trunc |
截断清空模式 | ofstream, fstream |
打开文件时清空原有内容。必须配合 ios::out 使用。 |
ios::binary |
二进制模式 | 所有类 | 以字节流形式读写,不做换行符转换(Windows 下 \n ↔ \r\n)。随机访问必须使用此模式。 |
ios::ate |
初始置尾模式 | 所有类 | 打开后立即将读写指针定位到文件末尾,但之后仍可自由移动指针。与 ios::app 本质不同。 |
使用示例:
cpp
// 正确:二进制读写,文件不存在则创建,存在则保留原内容
ios::in | ios::out | ios::binary
// 正确:文本追加写
ios::out | ios::app
// 错误:ifstream 不能用 trunc
// ifstream file("data.txt", ios::in | ios::trunc);
// 错误:trunc 必须配合 out 使用
// fstream file("data.txt", ios::in | ios::trunc);
注意:
fstream 默认模式陷阱
fstream的默认打开模式是ios::in | ios::out,但如果文件不存在,默认模式下打开会直接失败 ,不会自动创建文件。要让fstream创建不存在的文件,必须显式加上ios::out(单独加或组合加均可)。
二、打开、关闭与状态检查
1. 打开文件
有两种方式打开文件:构造函数 和
open()方法。
函数原型:
cpp
// 构造函数
explicit ifstream (const char* filename, ios_base::openmode mode = ios_base::in);
explicit ofstream (const char* filename, ios_base::openmode mode = ios_base::out);
explicit fstream (const char* filename, ios_base::openmode mode = ios_base::in | ios_base::out);
// C++11 支持 std::string
explicit ifstream (const std::string& filename, ios_base::openmode mode = ios_base::in);
explicit ofstream (const std::string& filename, ios_base::openmode mode = ios_base::out);
explicit fstream (const std::string& filename, ios_base::openmode mode = ios_base::in | ios_base::out);
// C++17 支持 std::filesystem::path
explicit ifstream (const std::filesystem::path& filename, ios_base::openmode mode = ios_base::in);
explicit ofstream (const std::filesystem::path& filename, ios_base::openmode mode = ios_base::out);
explicit fstream (const std::filesystem::path& filename, ios_base::openmode mode = ios_base::in | ios_base::out);
// open() 方法(参数与构造函数完全一致)
void open(const char* filename, ios_base::openmode mode = default_mode);
void open(const std::string& filename, ios_base::openmode mode = default_mode);
void open(const std::filesystem::path& filename, ios_base::openmode mode = default_mode);
参数:
filename: 文件路径(支持 C 风格字符串、std::string和 C++17std::filesystem::path)。mode: 打开模式(见上表),有默认值。
示例:
cpp
#include <fstream>
#include <string>
#include <filesystem> // C++17
int main()
{
// 方式 1: 构造时直接打开 (推荐,RAII 风格)
std::ifstream inFile("data.txt");
// 方式 2: 默认构造后调用 open()
std::ofstream outFile;
outFile.open("output.txt", ios::out | ios::app); // 追加模式
// C++17 filesystem::path 示例
std::filesystem::path filePath = "config.ini";
std::fstream configFile(filePath, ios::in | ios::out); // 文件不存在会失败
return 0;
}
2. 检查状态 ------ 重要
永远不要在不检查文件是否打开成功的情况下就进行读写
状态检查方法:
| 方法 | 作用 | 返回值 | 说明 |
|---|---|---|---|
is_open() |
检查文件是否成功打开 | bool (成功为 true) |
仅检查文件句柄是否有效 |
operator bool() (C++11) |
检查流是否处于可操作状态 | bool (良好为 true) |
等价于 !fail(),最推荐用于条件判断 |
operator!() |
检查流是否处于失败状态 | bool (失败为 true) |
等价于 fail() |
good() |
检查流是否完全正常 | bool |
当 eofbit、failbit、badbit 均未设置时返回 true |
eof() |
检查是否到达文件末尾 | bool |
仅当读操作尝试读取文件尾之后的数据时才会置位 |
fail() |
检查是否发生可恢复错误 | bool |
如格式错误、文件不存在等,清除状态后可继续操作 |
bad() |
检查是否发生不可恢复错误 | bool |
如文件系统损坏、内存不足等,流基本无法再使用 |
clear() |
清除流的所有错误状态 | void |
必须在错误发生后调用,才能继续使用流 |
setstate(iostate state) |
设置指定的错误状态 | void |
手动设置流的错误标志 |
示例(补充错误恢复):
cpp
std::ifstream file("data.txt");
// 最推荐的检查方式 (利用 operator bool)
if (!file)
{
std::cerr << "无法打开文件!错误码: " << file.rdstate() << std::endl;
return 1;
}
// 错误恢复示例
int num;
file >> num;
if (file.fail())
{
std::cerr << "读取格式错误!" << std::endl;
file.clear(); // 清除错误状态
file.ignore(1024, '\n'); // 跳过错误行
}
3. 关闭文件
函数原型:
cpp
void close();
虽然文件流对象在析构时会自动关闭文件,但显式调用
close()是个好习惯,特别是在需要立即释放文件句柄、重新打开文件或检查关闭是否成功时。
示例:
cpp
file.close();
// 检查关闭是否成功(很少需要,但某些场景有用)
if (file.is_open())
{
std::cerr << "文件关闭失败!" << std::endl;
}
4. 其他通用接口
① flush() ------ 强制刷新缓冲区
函数原型:
cpp
ostream& flush();
作用: 将输出缓冲区中所有未写入的数据立即写入文件。说明: 输出流默认会缓冲数据,只有当缓冲区满、程序结束或调用
close()时才会真正写入文件。flush()可以强制立即写入。
示例:
cpp
std::ofstream logFile("log.txt");
logFile << "程序启动成功" << std::endl;
logFile.flush(); // 立即写入日志,确保崩溃前能记录
② swap() ------ 交换两个文件流的状态(C++11)
函数原型:
cpp
void swap (basic_fstream& other);
// 非成员函数版本
void swap (basic_fstream& lhs, basic_fstream& rhs);
作用: 交换两个文件流的内部状态(包括打开的文件句柄、缓冲区、错误状态等)。
示例:
cpp
std::ifstream file1("a.txt");
std::ifstream file2("b.txt");
file1.swap(file2); // 现在 file1 指向 b.txt,file2 指向 a.txt
③ rdbuf() ------ 获取底层缓冲区指针
函数原型:
cpp
basic_filebuf<char_type, traits_type>* rdbuf() const;
作用: 获取指向底层
std::filebuf对象的指针,用于低级 I/O 操作或流重定向。
示例:
cpp
// 将 cout 重定向到文件
std::ofstream file("output.txt");
std::streambuf* oldCoutBuf = std::cout.rdbuf();
std::cout.rdbuf(file.rdbuf());
std::cout << "这句话会写入文件" << std::endl;
std::cout.rdbuf(oldCoutBuf); // 恢复 cout
三、ifstream (输入文件流)
ifstream继承自istream,专门用于从文件中读取数据。
1. 核心读取接口
① 流提取运算符 >>
用于格式化读取(自动跳过空格、制表符、换行符等空白字符)。
示例:
cpp
std::ifstream in("data.txt");
int a;
double b;
std::string str;
// 像使用 cin 一样使用
in >> a >> b >> str;
② getline() ------ 读取整行
这是最常用的接口之一,读取直到遇到换行符
\n。
函数原型:
cpp
// 成员函数版本(读取到字符数组)
istream& getline (char* s, streamsize n, char delim = '\n');
// 非成员函数版本(读取到 std::string,推荐)
istream& getline (istream& is, string& str, char delim = '\n');
参数:
s/str: 存储结果的字符数组或std::string。n: 最多读取的字符数(含终止符)。delim: 分隔符(默认是换行符),遇到此字符停止读取(该字符会被从流中丢弃,不会存入结果)。
返回值:
- 返回流对象本身(
*this),因此可以链式调用或用于if/while判断。
示例:
cpp
std::ifstream inFile("poem.txt");
std::string line;
// 经典逐行读取循环
while (std::getline(inFile, line))
{
std::cout << "读到: " << line << std::endl;
}
③ get() ------ 读取单个字符
读取流中的下一个字符,不跳过空白字符。
函数原型:
cpp
int get(); // 返回字符的 ASCII 码,若到文件尾返回 EOF
istream& get (char& c);
示例:
cpp
char ch;
while (inFile.get(ch))
{
// 处理字符 ch(包括空格、换行符)
}
④ read() ------ 二进制块读取
用于读取二进制数据块,不做任何格式转换。
函数原型:
cpp
istream& read (char* s, streamsize n);
s: 指向内存缓冲区的指针。n: 要读取的字节数。
示例:
cpp
std::ifstream binFile("image.png", ios::binary);
char buffer[1024];
binFile.read(buffer, 1024); // 读取 1024 个字节
⑤ gcount() ------ 获取上次读取的字节数
作用:返回最后一次非格式化读取操作(如
get,getline,read)实际读取的字符数。
示例:
cpp
inFile.read(buffer, 100);
std::cout << "实际读取了 " << inFile.gcount() << " 个字节" << std::endl;
⑥ ignore() ------ 跳过指定字符
函数原型:
cpp
istream& ignore (streamsize n = 1, int delim = EOF);
参数:
n: 最多跳过的字符数。delim: 分隔符,遇到此字符停止跳过(该字符也会被丢弃)。
作用: 跳过流中的字符,最常用于解决>>和getline()混用的坑。
注意:
>>与getline()混用 当使用>>读取数据后,输入缓冲区会留下一个换行符\n。如果紧接着调用getline(),它会读到这个换行符,返回一个空字符串。
解决方案: 在 >> 之后调用 ignore() 跳过换行符。
示例:
cpp
std::ifstream file("data.txt");
int id;
std::string name;
file >> id;
file.ignore(); // 跳过 >> 留下的换行符
std::getline(file, name); // 正确读取下一行的姓名
⑦ peek() ------ 查看下一个字符
函数原型:
cpp
int peek();
作用: 返回流中的下一个字符,但不提取它 (指针不移动)。若到文件尾返回
EOF。
示例:
cpp
// 跳过所有空行
std::string line;
while (inFile)
{
if (inFile.peek() == '\n')
{
inFile.ignore(); // 跳过空行
}
else
{
std::getline(inFile, line);
// 处理非空行
}
}
⑧ putback() / unget() ------ 回退字符
函数原型:
cpp
istream& putback (char c); // 将指定字符放回流中
istream& unget(); // 将最后读取的字符放回流中
作用: 将字符放回输入流,下一次读取会先读到这个字符。
示例:
cpp
char ch;
inFile.get(ch);
if (isdigit(ch))
{
inFile.putback(ch); // 把数字放回去
int num;
inFile >> num; // 读取整个数字
}
四、ofstream (输出文件流)
ofstream继承自ostream,专门用于向文件写入数据。
1. 核心写入接口
① 流插入运算符 <<
用于格式化写入。
示例:
cpp
std::ofstream out("log.txt");
int id = 1001;
double score = 95.5;
// 像使用 cout 一样使用
out << "ID: " << id << ", Score: " << score << std::endl;
注意:endl 与 '\n' 的区别
std::endl: 输出一个换行符 并强制刷新缓冲区,频繁使用会严重影响性能。'\n': 仅输出一个换行符,不刷新缓冲区,性能更好。
注: 除非确实需要立即写入,否则使用 '\n' 代替 std::endl。
② put() ------ 写入单个字符
函数原型:
cpp
ostream& put (char c);
示例:
cpp
outFile.put('A');
outFile.put('\n');
③ write() ------ 二进制块写入
用于写入二进制数据块,不做任何格式转换。
函数原型:
cpp
ostream& write (const char* s, streamsize n);
示例:
cpp
std::ofstream binOut("data.bin", ios::binary);
int data[] = {1, 2, 3, 4, 5};
// 将内存中的数组直接写入文件
binOut.write(reinterpret_cast<char*>(data), sizeof(data));
注意:
二进制读写的字节序问题不同平台(x86 是小端,ARM 可能是大端)的字节序不同,直接写入的二进制数据在跨平台读取时会出错。如果需要跨平台,应统一字节序(如网络字节序)。
五、fstream (文件流)
fstream继承自iostream,因此它同时拥有 上述ifstream和ofstream的所有接口。它既可以读,也可以写,是最灵活的文件流类。
注意:
- 如果只写,用
ofstream;只读,用ifstream。只有当需要同时读写 或随机访问 时,才使用fstream。- 读写交替操作时,必须在中间插入一个
seekg()、seekp()或flush()操作,否则行为未定义(C++ 标准规定)。
1. fstream 的读接口
由于
fstream继承自istream,它拥有ifstream的所有读取接口:>>、getline()、get()、read()、gcount()、ignore()、peek()、putback()、unget()。
2. fstream 的写接口
由于
fstream继承自ostream,它拥有ofstream的所有写入接口:<<、put()、write()、flush()。
3. fstream 的优势:随机访问
文件流内部有两个独立的指针:读指针 (Get Pointer) 和 写指针 (Put Pointer) ,分别控制当前的读写位置。
fstream允许我们自由移动这两个指针,实现随机读写。
注意:
文本模式下随机访问不可靠 在文本模式(默认)下,由于换行符会被转换(Windows 下
\n转成\r\n,占两个字节),tellg()/tellp()返回的位置与实际字符数不一致,导致seekg()/seekp()定位错误。随机访问必须使用二进制模式ios::binary。
| 接口 | 作用 | 函数原型 |
|---|---|---|
| 读指针操作 | ||
tellg() |
返回当前读指针的位置 | streampos tellg(); |
seekg(pos) |
将读指针移动到绝对位置 | istream& seekg(streampos pos); |
seekg(offset, dir) |
从基准位置偏移指定字节 | istream& seekg(streamoff offset, ios_base::seekdir dir); |
| 写指针操作 | ||
tellp() |
返回当前写指针的位置 | streampos tellp(); |
seekp(pos) |
将写指针移动到绝对位置 | ostream& seekp(streampos pos); |
seekp(offset, dir) |
从基准位置偏移指定字节 | ostream& seekp(streamoff offset, ios_base::seekdir dir); |
方向参数 dir:
ios::beg------ 文件开头 (Beginning)ios::cur------ 当前位置 (Current)ios::end------ 文件末尾 (End)
参数:
pos: 绝对位置(通常由tellg()或tellp()返回)。offset: 偏移量(正数向后,负数向前)。
返回值:
tellg()/tellp(): 返回当前指针位置(类型为streampos)。seekg()/seekp(): 返回流对象本身,支持链式调用。
示例(二进制模式随机访问):
cpp
#include <iostream>
#include <fstream>
#include <string>
int main()
{
// 必须使用 ios::binary 才能保证随机访问准确
std::fstream file("random_access.bin", ios::in | ios::out | ios::trunc | ios::binary);
if (!file)
{
std::cerr << "文件打开失败!" << std::endl;
return 1;
}
// --- 1. 先写入一些数据 ---
int data[] = {10, 20, 30, 40, 50};
file.write(reinterpret_cast<char*>(data), sizeof(data));
// --- 2. 随机读取第 3 个整数(索引从 0 开始)---
file.seekg(2 * sizeof(int), ios::beg); // 移动到第 3 个整数的位置
int value;
file.read(reinterpret_cast<char*>(&value), sizeof(int));
std::cout << "第 3 个整数: " << value << std::endl; // 输出 30
// --- 3. 随机修改第 2 个整数为 200 ---
file.seekp(1 * sizeof(int), ios::beg);
int newValue = 200;
file.write(reinterpret_cast<char*>(&newValue), sizeof(int));
// --- 4. 验证修改结果 ---
file.seekg(0, ios::beg);
int result[5];
file.read(reinterpret_cast<char*>(result), sizeof(result));
std::cout << "修改后的数组: ";
for (int i = 0; i < 5; i++)
{
std::cout << result[i] << " ";
}
// 输出: 10 200 30 40 50
return 0;
}
六、综合示例
下面是一个完整的示例,展示如何使用
ofstream写入配置,再用ifstream读取回来,最后用fstream修改中间数据。
cpp
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
int main()
{
const string filename = "config.txt";
// --- 1. 写入文件 (ofstream) ---
{
ofstream outFile(filename);
if (!outFile)
{
cerr << "创建文件失败!" << endl;
return 1;
}
outFile << "Username: Admin" << endl;
outFile << "Level: 99" << endl;
outFile << "HP: 1000.5" << endl;
cout << "数据写入成功。" << endl;
}
// --- 2. 读取文件 (ifstream) ---
{
ifstream inFile(filename);
if (!inFile)
{
cerr << "读取文件失败!" << endl;
return 1;
}
string key, colon;
string name;
int level;
double hp;
inFile >> key >> colon >> name;
inFile >> key >> colon >> level;
inFile >> key >> colon >> hp;
cout << "\n--- 读取结果 ---" << endl;
cout << "玩家名: " << name << endl;
cout << "等级: " << level << endl;
cout << "血量: " << hp << endl;
}
// --- 3. 修改文件 (fstream) ---
{
// 文本模式下修改整行,注意长度匹配
fstream file(filename, ios::in | ios::out);
if (!file)
{
cerr << "打开文件失败!" << endl;
return 1;
}
string line;
streampos levelPos = -1;
while (getline(file, line))
{
if (line.find("Level: ") != string::npos)
{
// 计算当前行的起始位置(文本模式下仅作近似,推荐二进制模式)
levelPos = file.tellg();
levelPos -= (line.length() + 1); // +1 是换行符
break;
}
}
if (levelPos != streampos(-1))
{
file.seekp(levelPos);
// 注意:新内容长度必须 >= 原内容长度,否则会残留旧内容
file << "Level: 100" << endl;
cout << "\n等级已修改为 100。" << endl;
}
}
return 0;
}
建议
- 优先使用
ifstream读、ofstream写,仅在需要同时读写时用fstream- 永远检查文件是否打开成功
- 二进制读写必须加
ios::binary- 随机访问必须使用二进制模式
>>之后用ignore()跳过换行符再用getline()- 优先使用
'\n'代替std::endl提升性能
感谢阅读。本文如有错漏之处,烦请斧正。