三个文件流类 ifstream / ofstream / fstream 各管一段,"用哪个"由"你要干什么"决定,不由"哪个更强大"决定。
本节把 15.2 的"打开"和 15.3 的"读写"接成一个完整闭环,并借实例03 演示那个处理文本文件最常用的套路:
getline 读一行 → istringstream 拆成几段 → 处理。
一句话概括:文件操作永远是"写进去 → 读出来 → 能对上",验证闭环才算真的会。
本节是 15.3 组的开头,起"承上启下"的作用:
- 15.2 讲了怎么打开、怎么检查;15.3 讲了有两条路;本节把这些串成一个能跑的程序;
- 实例03 里用到了 istringstream ——13.4.2 见过它的亲戚 istream_iterator,这次是它当主角;
- 用 { } 把读写分开是"资源交给对象管"的习惯(14.3 留的 RAII 账),本节第一次正式使用。
一句话:本节是"能跑起来"的第一节。
一、三个文件流类,各管一段
| 类 | 能干什么 | 不能干什么 | 什么时候用 |
|---|---|---|---|
ifstream |
读 | 不能 << |
只要读(最常用) |
ofstream |
写 | 不能 >> |
只要写(第二常用) |
fstream |
读 + 写 | — | 要在同一个文件里又读又改 |
"能干什么"是编译期就定死的(15.1.1 讲过:istream / ostream 在第二层就分家了):
ifstream in("a.txt");
in << "x"; // ❌ 编译错误:ostream 的 << 不在 istream 上
这个错误是好事:编译器帮你挡住了"往只读的流里写"这种逻辑错误。
"选哪个"的原则:
不要因为
fstream功能最全就用它。 按用途选:只读用ifstream(还能享受"打开失败就是文件不存在"的语义),只写用ofstream(会顺手建文件)。
二、完整闭环:写 → 关 → 读 → 关
这是本节要你带走的骨架:
// ── 写 ────────────────────────────
{
ofstream out("a.txt");
if (!out) { /* 处理失败 */ }
out << "内容" << endl;
out.close(); // ★ 写完就关,确保数据落地
}
// ── 读 ────────────────────────────
{
ifstream in("a.txt");
if (!in) { /* 处理失败 */ }
string line;
while (getline(in, line)) {
// 处理每一行
}
in.close();
}
三处细节都值得说:
① 为什么用 { } 把两段包起来
两个作用:
- 让流对象在块结束时析构(10.3)——即使你忘了
close(),析构也会兜住; - 防止名字冲突(两个块里都能叫
in/out,互不干扰)。
这是"资源交给对象管"的雏形:对象的生存期 = 资源的持有期。
② 为什么"写完先关、再读"
因为缓冲区(15.1):
- 你
<<进去的内容可能还在内存里; - 同一程序里接着读——如果写的那份还没刷,读到的可能是不完整的内容;
- 先
close(),数据落地了再读,最稳。
③ close() 到底做了什么
| 动作 | 说明 |
|---|---|
| 刷缓冲区 | 把还没落地的数据写进磁盘 |
| 断开与文件的关联 | 之后这个流对象不再连着那个文件 |
可以重新 open 别的文件 |
同一个对象能反复用(15.5) |
三、文本文件的标准套路:getline + istringstream
处理文本文件时,最常用的一组配合是:
① getline(in, line) → 拿到一整行(比如 "Ada 95")
② istringstream ss(line) → 把这一行当输入流
③ ss >> name >> score → 按空白拆成几段
为什么不能直接 in >> name >> score?
能,但那样就丢掉了"行"的概念:
- 直接
in >> name >> score是跨行的——它会一直读到有数据为止,行的边界消失了; - 而大多数文本文件"一行 = 一条记录",用
getline先划出行,再拆,才和文件结构对得上; - 更重要的是:一行拆坏了,只坏这一行;直接
>>出错,整个流就乱了。
这是处理 CSV、配置、日志的标准姿势。
四、实例03:读/写文件
教材这道题要做的:完整地"写一个文件、再读回来",并验证内容对得上。
#include <iostream>
#include <fstream>
#include <sstream> // istringstream 在这里
#include <string>
using namespace std;
int main() {
const string fname = "scores.txt";
// ══ 第一步:写 ══════════════════════════
{
ofstream out(fname); // 默认 out|trunc:重新生成
if (!out) { cerr << "写不了 " << fname << endl; return 1; }
out << "Ada 95" << endl;
out << "Bob 88" << endl;
out << "Cid 72" << endl;
out.close();
cout << "① 已写入 3 条记录" << endl;
}
// ══ 第二步:读(逐行读 + 拆词 + 统计)═══
{
ifstream in(fname);
if (!in) { cerr << "读不了 " << fname << endl; return 1; }
string line;
int count = 0, total = 0;
while (getline(in, line)) { // 一行一行拿
istringstream ss(line); // 把这一行当成一个输入流
string name;
int score;
if (ss >> name >> score) { // 拆成功才算一条记录
cout << "② " << name << " -> " << score << endl;
total += score;
count++;
}
}
in.close();
cout << "③ 共 " << count << " 条,总分 " << total
<< ",平均 " << (count ? (double)total / count : 0.0) << endl;
}
return 0;
}
看到什么算成功:
① 已写入 3 条记录
② Ada -> 95
② Bob -> 88
② Cid -> 72
③ 共 3 条,总分 255,平均 85
数字怎么来的:95 + 88 + 72 = 255;255 / 3 = 85。
读结果:
- ② 的输出顺序和写入顺序完全一致 ——证明文本文件是"顺序写入、顺序读出"的(这一点在 15.4.4 讲"随机访问"时会变成对照);
if (ss >> name >> score)这个写法:把"拆词"放进if条件里——拆成功才处理(对流来说,"成功"就是"流还是好的")。- ③ 表明:读进来的东西可以直接参与计算 ——这就是"数据持久化"的意义:这一份
scores.txt下次运行还能用。
顺手做个小实验:手动用记事本把 Bob 88 改成 Bob abc,再跑程序 → Bob 那一行不会被统计(因为 ss >> score 失败),总数变成 2 条。这就是"手工能改"带来的双面性:方便,也脆弱。
五、30 秒实验
- 把
out.close();和in.close();都注释掉,再跑 → 大概率照样对(作用域结束时析构兜住了)。但把两段合并成一个作用域、又去掉close(),就可能读到不完整的内容。 所以别赌,关就对了。 - 把 ③ 的
(double)total / count改成total / count→ 平均变成 85 还是 85?(这里恰好相等)。换成Bob 87(总分 254)再试 →254/3会得到 84(整数除法砍掉小数),而(double)254/3是 84.6667。经典坑(2.4 / 3.3.2 讲过)。 - 把
while (getline(in, line))改成while (!in.eof()) { getline(in, line); ... }→ 最后会多输出一次空行。这是本章最经典的错误写法,15.4.3 会专门解剖。
六、常见误区
- "
fstream最全能,所以一切文件都用它" ——按需要选。只读用ifstream、只写用ofstream,语义更清楚,还能避免"不小心写坏文件"。 - "写完不用
close(),反正在同一个程序里接着读" ——很危险。数据可能还在缓冲区,先关再读最稳。 - "
in.close()之后对象就废了" ——没有废。close()只是断开与文件的关联,对象还能用的——还能再open()另一个文件(15.5)。 - "
getline读回来的行里带着换行符" ——不带。getline把换行符读走并丢掉(这也是它和get的区别)。所以输出时可以自己补endl。 - "读文件时
in >> name >> score更省事,不用getline" ——在"一行一条记录"的文件上会出问题:>>会跨行读,行的边界丢了,而且一旦某一行的格式不对,后面的读取全乱。getline+istringstream才是标准姿势。
自测三问(点开对答案)
1. **只读、只写、读写,分别用哪个文件流类?选类的原则是什么?** ——`ifstream` / `ofstream` / `fstream`。**原则是"按用途选",不是"选功能最全的"。** 2. **读文本文件为什么推荐 `getline` + `istringstream`,而不是直接 `in >> a >> b`?** ——因为**大多数文本文件"一行 = 一条记录"**。**`>>` 会跨行读,丢掉行的边界**;而且**`getline` 先划出边界,一行拆坏了只影响这一行**。 3. **`close()` 具体做了什么?为什么"写完先关、再读"更稳?** ——**① 把缓冲区里没落地的数据刷进磁盘;② 断开流与文件的关联。** **先关再读可以保证"读到的就是写进去的全部内容"**(否则可能读到还没刷出去的部分)。- ☐ 下一节(15.3.2 写文本文件)