文件操作

在 C++ 中,fstreamifstream 和 ofstream 都是用于文件操作的标准流类,定义在 <fstream> 头文件中。它们分别对应不同的读写能力与默认行为。


1. ifstream — 输入文件流

  • 含义:Input File Stream,只能从文件中读取数据。
  • 继承:继承自 istream,因此可以使用所有输入流操作(>>getlineread 等)。
  • 默认打开模式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,可使用输出操作(<<putwrite 等)。
  • 默认打开模式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

下面用一个学生成绩管理的小程序,串起三种流的典型用法,能很直观地看出它们的区别。


场景描述

  1. 用 ofstream 创建文件 scores.txt,写入三名学生的初始成绩(覆盖写)。
  2. 用 ifstream 读取文件内容并打印。
  3. 用 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;
}

📝 这个例子如何帮你复习?

步骤类型打开方式能读?能写?文件不存在时会?
1ofstreamofstream out(filename)创建新文件
2ifstreamifstream in(filename)打开失败
3fstreamfstream 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) 时:

  1. 从 'A' 开始读取,一直读到 '\n'(即 90 后面的换行符)。
  2. 将 "Alice 90" 赋给 line,同时丢弃那个换行符。
  3. 读指针移动到换行符之后,即字符 'B' 的位置——也就是第二行的开头。

为什么不会停在这一行末尾?

“停在这一行末尾”意味着指针指向换行符之前或换行符本身。
而 getline 的设计目的就是整行提取,它认为换行符是行与行之间的分隔符,不是行内容的一部分,所以会主动跨过分隔符,把指针定位到下一行的起始。这样连续调用 getline 就能自然地一行行读取,无需额外处理换行符。

与此对照,如果使用 >> 读取字符串,它遇到空白(包括换行)会停止,且不会自动跳过那个空白字符,指针会留在空白字符前或空白字符上。因此 >> 之后想精确定位往往需要额外处理。

分享您的喜爱
WebsiteExpert
WebsiteExpert
文章: 12