在 C++ 中,fstream、ifstream 和 ofstream 都是用于文件操作的标准流类,定义在 <fstream> 头文件中。它们分别对应不同的读写能力与默认行为。
1. ifstream — 输入文件流
- 含义:Input File Stream,只能从文件中读取数据。
- 继承:继承自
istream,因此可以使用所有输入流操作(>>、getline、read等)。 - 默认打开模式:
ios::in。 - 特点:文件不存在或无法打开时,流对象会处于失败状态(
is_open()返回false),不会创建新文件。
cpp
#include <fstream>
#include <string>
std::ifstream in("data.txt");
if (in.is_open()) {
std::string line;
while (std::getline(in, line)) {
// 逐行读取
}
}
2. ofstream — 输出文件流
- 含义:Output File Stream,只能向文件中写入数据。
- 继承:继承自
ostream,可使用输出操作(<<、put、write等)。 - 默认打开模式:
ios::out(等价于ios::out | ios::trunc)。 - 特点:
- 打开文件时,默认会截断文件(清空原有内容)或创建新文件(若不存在)。
- 若想保留原内容进行追加,需显式指定
ios::app。
cpp
std::ofstream out("output.txt"); // 覆盖写
std::ofstream app("log.txt", std::ios::app); // 追加写
out << "Hello, world!" << std::endl;
3. fstream — 文件流(读写)
- 含义:File Stream,同时支持读取和写入。
- 继承:继承自
iostream(即同时从istream和ostream派生),既可使用输入操作也可使用输出操作。 - 默认打开模式:
ios::in | ios::out。 - 特点:
- 默认模式下,文件必须已存在,否则打开失败(不会自动创建)。
- 若希望创建新文件或截断旧文件,需额外指定
ios::trunc;若希望文件不存在时创建,需使用ios::out(或ios::in | ios::out | ios::trunc等组合)。 - 支持自由移动读/写位置(
seekg/seekp)。
cpp
std::fstream fs("data.bin", std::ios::in | std::ios::out | std::ios::binary);
if (fs.is_open()) {
// 可读可写
fs.write(...);
fs.seekg(0);
fs.read(...);
}
三者区别一览
| 类名 | 读写能力 | 默认打开模式 | 文件不存在时 | 继承关系 |
|---|---|---|---|---|
ifstream | 只读 | ios::in | 打开失败(不创建) | istream |
ofstream | 只写 | ios::out(含截断) | 创建新文件 | ostream |
fstream | 可读可写 | ios::in | ios::out | 打开失败(不创建) | iostream |
关键要点:
- 功能范围:
fstream是ifstream和ofstream的超集(读写兼备)。 - 默认行为:
ofstream默认会清空或创建文件,适合直接输出。ifstream和fstream默认要求文件已存在,否则打开失败。- 若希望
fstream既能创建又能读写,可显式添加ios::trunc(覆盖)或组合模式ios::in | ios::out | ios::trunc。
- 使用场景:
- 只需要读取 → 用
ifstream。 - 只需要写入(覆盖/追加) → 用
ofstream。 - 需要同时读写同一文件(如随机访问数据库) → 用
fstream。
- 只需要读取 → 用
***********************************************************************************************************************************************************
下面用一个学生成绩管理的小程序,串起三种流的典型用法,能很直观地看出它们的区别。
场景描述
- 用
ofstream创建文件scores.txt,写入三名学生的初始成绩(覆盖写)。 - 用
ifstream读取文件内容并打印。 - 用
fstream读写模式打开同一个文件,将第二名学生成绩修改为 95,然后读取整文件验证修改结果。
cpp
#include <iostream>
#include <fstream>
#include <string>
using std::cout;
using std::cerr;
using std::endl;
using std::string;
using std::ifstream;
using std::ofstream;
using std::fstream;
int main() {
const string filename = "scores.txt";
// ========== 1. ofstream:只写(覆盖创建) ==========
{
// ofstream 默认打开模式 ios::out | ios::trunc
// 文件不存在则创建,存在则清空内容
ofstream out(filename);
if (!out) {
cerr << "打开文件写入失败!" << endl;
return 1;
}
out << "Alice 90" << endl
<< "Bob 85" << endl
<< "Charlie 78" << endl;
cout << "[ofstream] 已创建/覆盖写入 " << filename << endl;
} // out 离开作用域,自动关闭文件
// ========== 2. ifstream:只读 ==========
{
// ifstream 只能读取,不能写入,默认 ios::in
ifstream in(filename);
if (!in) {
cerr << "打开文件读取失败!" << endl;
return 1;
}
cout << "[ifstream] 读取文件内容:" << endl;
string line;
while (getline(in, line)) {
cout << line << endl;
}
} // in 自动关闭
// ========== 3. fstream:可读可写(默认 in|out) ==========
{
// fstream 默认打开模式就是 ios::in | ios::out
// 因此可以直接写 fstream fs(filename);
fstream fs(filename); // 要求文件已存在
if (!fs) {
cerr << "以读写模式打开文件失败!" << endl;
return 1;
}
// 读取前两行,定位第二行开头的位置
// 我们使用更直接的写法:先读第一行,记录第二行开头
fs.seekg(0); // 回到文件头
getline(fs, line); // 读第一行 "Alice 90"
writePos = fs.tellg(); // 现在位置是第二行开头
getline(fs, line); // 读第二行 "Bob 85"(其实我们不需要内容)
// 移动写指针到第二行开头,覆盖写入新成绩
fs.seekp(writePos);
fs << "Bob 95" << endl; // 用新成绩 + 换行覆盖
cout << "[fstream] 已将第二行修改为 Bob 95" << endl;
// 回到文件头,用读指针重新读取整个文件进行验证
fs.seekg(0);
cout << "[fstream] 验证修改后文件内容:" << endl;
while (getline(fs, line)) {
cout << line << endl;
}
} // fs 自动关闭
return 0;
}
📝 这个例子如何帮你复习?
| 步骤 | 类型 | 打开方式 | 能读? | 能写? | 文件不存在时会? |
|---|---|---|---|---|---|
| 1 | ofstream | ofstream out(filename) | ❌ | ✅ | 创建新文件 |
| 2 | ifstream | ifstream in(filename) | ✅ | ❌ | 打开失败 |
| 3 | fstream | fstream fs(filename) | ✅ | ✅ | 打开失败(默认需文件已存在) |
核心要点:
ofstream最「霸道」:默认就清空或创建文件,适合输出。ifstream最「安全」:只能读不能改,防止误操作。fstream最「灵活」:既能读又能写,但需手动管理读/写位置(seekg/seekp/tellg/tellp),且默认要求文件已存在。
💡 使用
fstream fs(filename);
省略了默认的ios::in | ios::out,这是 C++ 标准库设计的便利——默认模式就能满足最常见的可读可写场景。如果文件需要创建,再加ios::trunc即可:fstream fs(filename, std::ios::in | std::ios::out | std::ios::trunc);
这样写,代码意图清晰,后续复习时一目了然。
输出示例
text
[ofstream] 已创建/覆盖写入 scores.txt [ifstream] 读取文件内容: Alice 90 Bob 85 Charlie 78 [fstream] 已将第二行修改为 Bob 95 [fstream] 验证修改后文件内容: Alice 90 Bob 95 Charlie 78
实用建议:除非真的需要同时读写同一文件,否则优先用
ifstream或ofstream,语义更清晰,也避免误操作。
***********************************************************************************************************************************************************
getline(fs, line);
为什么指针会停在下一行开头而不是停在这一行末尾?
这是因为 std::getline 的内置行为就是读取并丢弃换行符,所以读完一行后,读指针恰好跨过换行符,落在下一行的第一个字符前。
具体机制
getline(fs, line)会从流中逐个读取字符,直到遇到 换行符'\n'(或文件结束)。- 换行符本身会被读取并丢弃,不会存入
line。 - 读取完成后,流的内部读指针停在换行符之后的位置,即下一行的开头。
例如文件内容:
text
Alice 90\nBob 85\nCharlie 78\n
当第一次调用 getline(fs, line) 时:
- 从
'A'开始读取,一直读到'\n'(即90后面的换行符)。 - 将
"Alice 90"赋给line,同时丢弃那个换行符。 - 读指针移动到换行符之后,即字符
'B'的位置——也就是第二行的开头。
为什么不会停在这一行末尾?
“停在这一行末尾”意味着指针指向换行符之前或换行符本身。
而 getline 的设计目的就是整行提取,它认为换行符是行与行之间的分隔符,不是行内容的一部分,所以会主动跨过分隔符,把指针定位到下一行的起始。这样连续调用 getline 就能自然地一行行读取,无需额外处理换行符。
与此对照,如果使用 >> 读取字符串,它遇到空白(包括换行)会停止,且不会自动跳过那个空白字符,指针会留在空白字符前或空白字符上。因此 >> 之后想精确定位往往需要额外处理。