🔷 第十五章 文件操作
📄 一句话概括

三个文件流类 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();
}

三处细节都值得说:

① 为什么用 { } 把两段包起来

两个作用:

这是"资源交给对象管"的雏形:对象的生存期 = 资源的持有期。

② 为什么"写完先关、再读"

因为缓冲区(15.1):

③ close() 到底做了什么

动作 说明
刷缓冲区 把还没落地的数据写进磁盘
断开与文件的关联 之后这个流对象不再连着那个文件
可以重新 open 别的文件 同一个对象能反复用(15.5)

三、文本文件的标准套路:getline + istringstream

处理文本文件时,最常用的一组配合是:

① getline(in, line)   →  拿到一整行(比如 "Ada 95")
② istringstream ss(line)  →  把这一行当输入流
③ ss >> name >> score →  按空白拆成几段

为什么不能直接 in >> name >> score?

能,但那样就丢掉了"行"的概念:

这是处理 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。

读结果:

顺手做个小实验:手动用记事本把 Bob 88 改成 Bob abc,再跑程序 → Bob 那一行不会被统计(因为 ss >> score 失败),总数变成 2 条。这就是"手工能改"带来的双面性:方便,也脆弱。

五、30 秒实验

  1. 把 out.close(); 和 in.close(); 都注释掉,再跑 → 大概率照样对(作用域结束时析构兜住了)。但把两段合并成一个作用域、又去掉 close(),就可能读到不完整的内容。 所以别赌,关就对了。
  2. 把 ③ 的 (double)total / count 改成 total / count → 平均变成 85 还是 85?(这里恰好相等)。换成 Bob 87(总分 254)再试 → 254/3 会得到 84(整数除法砍掉小数),而 (double)254/3 是 84.6667。经典坑(2.4 / 3.3.2 讲过)。
  3. 把 while (getline(in, line)) 改成 while (!in.eof()) { getline(in, line); ... } → 最后会多输出一次空行。这是本章最经典的错误写法,15.4.3 会专门解剖。

六、常见误区

  1. "fstream 最全能,所以一切文件都用它" ——按需要选。只读用 ifstream、只写用 ofstream,语义更清楚,还能避免"不小心写坏文件"。
  2. "写完不用 close(),反正在同一个程序里接着读" ——很危险。数据可能还在缓冲区,先关再读最稳。
  3. "in.close() 之后对象就废了" ——没有废。close() 只是断开与文件的关联,对象还能用的——还能再 open() 另一个文件(15.5)。
  4. "getline 读回来的行里带着换行符" ——不带。getline 把换行符读走并丢掉(这也是它和 get 的区别)。所以输出时可以自己补 endl。
  5. "读文件时 in >> name >> score 更省事,不用 getline" ——在"一行一条记录"的文件上会出问题:>> 会跨行读,行的边界丢了,而且一旦某一行的格式不对,后面的读取全乱。getline + istringstream 才是标准姿势。
自测三问(点开对答案) 1. **只读、只写、读写,分别用哪个文件流类?选类的原则是什么?** ——`ifstream` / `ofstream` / `fstream`。**原则是"按用途选",不是"选功能最全的"。** 2. **读文本文件为什么推荐 `getline` + `istringstream`,而不是直接 `in >> a >> b`?** ——因为**大多数文本文件"一行 = 一条记录"**。**`>>` 会跨行读,丢掉行的边界**;而且**`getline` 先划出边界,一行拆坏了只影响这一行**。 3. **`close()` 具体做了什么?为什么"写完先关、再读"更稳?** ——**① 把缓冲区里没落地的数据刷进磁盘;② 断开流与文件的关联。** **先关再读可以保证"读到的就是写进去的全部内容"**(否则可能读到还没刷出去的部分)。

相关笔记