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

读文本文件有三种姿势,各读各的粒度: - in >> x ——按空白分词(跳过空格和换行,一次拿一个"词"); - getline(in, line) ——读一整行(含空格,不含换行符); - in.get(ch) ——读一个字符(连空白也读,一个不放过)。 而"怎么判断读完"才是本节的核心难题——最经典的错误写法是 while (!in.eof()),它会多处理一次。 一句话概括:三种读法决定"数据被切成什么形状",而 while (读操作) 才是正确的循环写法。

📝 本节和前面几节的关系

15.3.2 讲了写,本节讲"怎么读回来"——它是本章最容易出错的一节: - 2.5 的"cin 三条解析规则"在这里完整复现(文件流和 cin 的规则一模一样); - 13.4.2 的 istream_iterator 建立在这套规则上(vector<int> v(in, eof) 一句读完); - 15.4.1(状态位)和 15.4.3(结尾判断)会专门回答本节的"为什么 !eof() 是错的"。 一句话:本节讲"读的方法",15.4 三节讲"读的状态"。

一、三种读法:切出来的形状不一样

假设文件内容是三行:

Ada 95 1.62
Bob 88 1.78
Cid 72 1.71

三种读法读出来的"东西"完全不同:

读法 一次拿到 上例会依次拿到 适合
in >> x 一个"词"(按空白切) Ada 95 1.62 Bob 88 …(共 9 个) 数据是"一串值"(如纯数字文件)
getline(in, line) 一整行 "Ada 95 1.62" "Bob 88 1.78" "Cid 72 1.71"(共 3 个) 一行 = 一条记录(最常见)
in.get(ch) 一个字符 'A' 'd' 'a' ' ' '9' '5' …(含空格和换行) 要精确处理每个字节(如解析自定义格式)

选择的原则:

先想清楚"文件里的数据,天然的单位是什么" ——是一条记录?一个值?还是一个字符? ——然后再选对应的读法。

二、正确写法:把"读操作"放进循环条件

这是本节最值钱的一条。三种读法对应三种惯用法:

// ① 逐词
string word;
while (in >> word) { ... }

// ② 逐行
string line;
while (getline(in, line)) { ... }

// ③ 逐字符
char ch;
while (in.get(ch)) { ... }

为什么这样写就对了?

因为这些读操作都返回"流对象本身",而流对象能当条件判断:

流还能继续读 → 判为真(true);流已经失败/到末尾 → 判为假(false)。

所以 while (in >> word) 的完整含义是:

"试着读一个词;读成功就进循环体、读不成功就退出"

一举两得:既完成了"读",又完成了"判断" ——不管文件是空了、读完了、还是格式坏了,循环该退就退。

这就是"流能当条件用"的设计价值。

三、经典错误:while (!in.eof())

这是 C++ 里最有名的一个坑,请务必看清楚它错在哪:

// ❌ 错误写法
string line;
while (!in.eof()) {          // "只要还没到末尾,就继续"
    getline(in, line);
    cout << line << endl;    // ← 最后会多输出一次(一行空的)
}

为什么多一次? 因为 eof() 表示"上一次读已经碰到文件尾了",而不是"下一次读会碰到文件尾"。

用纸面演算看清楚(文件有 3 行):

轮次 in.eof() 此时 进不进循环 getline 的结果
1 false 进 读到第 1 行
2 false 进 读到第 2 行
3 false 进 读到第 3 行
4 还是 false(第 3 次读是"正好读到末尾",还没触发 eof) 还进 读失败 → line 变空
5 true 退出 —

结果:line 被多输出一次(空行)。

eof 是在"读操作试图越过末尾"之后才置位的 ——所以判断"读完了没有"要在读之后、而不是读之前。

结论:

永远用 while (读操作),永远不要用 while (!in.eof())。 (15.4.3 会把这件事再讲一遍,并给出"如果非要用 eof,该怎么用"。)

四、第二个经典坑:>> 和 getline 混用

int n;
in >> n;                    // 读完数字后,指针停在数字后面
string line;
getline(in, line);          // ← 读到的是"数字后面那一小段"(往往是空串)

为什么会这样? 因为 >> 停在"第一个不属于这个值的字符"之前,那个字符(多半是空格或换行)还留在流里。getline 一上来就撞上它,于是"这一行"是空的。

解法:在 >> 之后先把这一行剩下的一起吃掉:

in >> n;
string junk;
getline(in, junk);          // ★ 吃掉换行(以及后面的残余)
// 现在再 getline 就能拿到真正的下一行

一句话:"换读法之前,先清干净"。

五、实例05:读取文本文件的内容

教材这道题要做的:用几种不同的方式,把同一个文件的内容读出来。

#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
using namespace std;

int main() {
    const string fname = "read_demo.txt";

    // ── 先准备一个文件 ──────────────────────
    {
        ofstream out(fname);
        if (!out) { cerr << "写不了" << endl; return 1; }
        out << "Ada 95 1.62\n";
        out << "Bob 88 1.78\n";
        out << "Cid 72 1.71\n";
        out.close();
    }

    // ══ ① 逐词读(>>):按空白切 ═══════════
    {
        ifstream in(fname);
        cout << "① 用 >> 逐词读:" << endl;
        string word;
        int n = 0;
        while (in >> word) cout << "   [" << ++n << "] " << word << endl;
        in.close();
    }

    // ══ ② 逐行读(getline):一行一个 ═══════
    {
        ifstream in(fname);
        cout << "② 用 getline 逐行读:" << endl;
        string line;
        int n = 0;
        while (getline(in, line)) cout << "   " << ++n << ": " << line << endl;
        in.close();
    }

    // ══ ③ 逐字符读(get):连空白也读 ═══════
    {
        ifstream in(fname);
        cout << "③ 用 get 逐字符读(前 12 个,换行用 \\n 表示):" << endl;
        char ch;
        int n = 0;
        while (n < 12 && in.get(ch)) {
            if (ch == '\n') cout << "\\n";
            else            cout << ch;
            n++;
        }
        cout << endl;
        in.close();
    }

    // ══ ④ 一行一条记录:getline + istringstream ══
    {
        ifstream in(fname);
        cout << "④ 一行一条记录(拆开处理):" << endl;
        string line;
        while (getline(in, line)) {
            istringstream ss(line);
            string name;
            int score;
            double h;
            if (ss >> name >> score >> h)
                cout << "   " << name << " 分数 " << score << " 身高 " << h << endl;
        }
        in.close();
    }
    return 0;
}

看到什么算成功:

① 用 >> 逐词读:
   [1] Ada
   [2] 95
   [3] 1.62
   [4] Bob
   [5] 88
   [6] 1.78
   [7] Cid
   [8] 72
   [9] 1.71
② 用 getline 逐行读:
   1: Ada 95 1.62
   2: Bob 88 1.78
   3: Cid 72 1.71
③ 用 get 逐字符读(前 12 个,换行用 \n 表示):
Ada 95 1.62\n
④ 一行一条记录(拆开处理):
   Ada 分数 95 身高 1.62
   Bob 分数 88 身高 1.78
   Cid 分数 72 身高 1.71

数字怎么来的:

读结果:

  1. ① 和 ② 的差别,就是"词"和"行"的差别 ——同一个文件,切法不同,看到的"单位"就不同。
  2. ③ 里的 \n 是实实在在的一个字符 ——get 连它都不放过(而 getline 会把它读走扔掉,所以 ② 的输出里看不到它)。
  3. ④ 是实际工程里最常用的套路:getline 划边界、istringstream 拆字段、if (ss >> ...) 保证"拆成功才处理"。

六、30 秒实验

  1. 把 ② 改成 while (!in.eof()) 的写法,跑一次 → 最后多出一行 4:(空行)。亲眼见一次这个经典错误,比记十遍有用。
  2. 在 ① 前面加一句 int k; in >> k;,再跑 → k 变成 0(读 Ada 失败),而且整个循环什么都不输出(流已失败,15.4.1)。体会"一次读失败,后面全废"。
  3. 在 ② 里加一句"读前先 in >> word;,然后 getline" → 第一个 getline 拿到空串(>> 留下的空格)。这就是混用的坑(本节第四段)。
  4. 把文件里 Bob 88 1.78 改成 Bob abc 1.78 再跑 ④ → Bob 那一行被跳过(因为 ss >> score 失败)。证明"if (ss >> ...) 的检查是有效的"。

七、常见误区

  1. "while (!in.eof()) 是最标准的写法" ——恰恰相反,它是经典错误。eof() 在"读越过末尾"之后才置位,所以会多循环一次。 正确写法:while (读操作)。
  2. "getline 读到的行里带着换行符" ——不带。它把换行符读走并丢掉(get 则不会)。
  3. "in >> s 会读一整行到换行" ——不会,它按空白切。读到空格就停。
  4. ">> 和 getline 随便混用" ——中间必须清一次(>> 会把分隔符留在流里)。
  5. "读失败就是文件读完了" ——不一定。也可能是格式不匹配(比如想读 int 却碰上字母)。两者在状态上是不一样的(eofbit vs failbit,15.4.1 讲)。
  6. "读文件时不用担心编码" ——要担心。如果文件是 GBK 编码,而程序按 UTF-8 解释,中文就会显示成乱码。 跨平台交换文本,统一用 UTF-8 是最省事的做法。
自测三问(点开对答案) 1. **读文本文件的三种方式分别是什么?各适合什么场景?** ——**`in >> x`(逐词,跳过空白)**——适合"一串值";**`getline(in, line)`(逐行)**——适合"一行一条记录"(最常用);**`in.get(ch)`(逐字符,含空白)**——适合精确处理字节。 2. **为什么 `while (!in.eof())` 会多处理一次?正确写法是什么?** ——**因为 `eof()` 是"上一次读越过末尾之后"才置位的**,所以进循环时它还是 `false`,**结果多跑一轮、拿到一个空值**。**正确写法:把读操作放进条件里 —— `while (in >> x)` / `while (getline(in, line))`。** 3. **`in >> n;` 之后紧接着 `getline(in, line);`,为什么会读到空串?怎么解决?** ——**`>>` 停在分隔符(空格/换行)之前,那个字符还留在流里**,**`getline` 一上来就撞上它**。**解决:`>>` 之后先 `getline` 一次把这一行剩下的吃掉,再做正经的 `getline`。**

相关笔记