读文本文件有三种姿势,各读各的粒度:
- 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
数字怎么来的:
- ① 9 个词 = 3 行 × 每行 3 个字段;
- ③ 前 12 个字符正好是
Ada 95 1.62加上那个换行(A d a ␣ 9 5 ␣ 1 . 6 2  = 12 个); - ④ 每行拆出三个字段,正好对上。
读结果:
- ① 和 ② 的差别,就是"词"和"行"的差别 ——同一个文件,切法不同,看到的"单位"就不同。
- ③ 里的
\n是实实在在的一个字符 ——get连它都不放过(而getline会把它读走扔掉,所以 ② 的输出里看不到它)。 - ④ 是实际工程里最常用的套路:
getline划边界、istringstream拆字段、if (ss >> ...)保证"拆成功才处理"。
六、30 秒实验
- 把 ② 改成
while (!in.eof())的写法,跑一次 → 最后多出一行4:(空行)。亲眼见一次这个经典错误,比记十遍有用。 - 在 ① 前面加一句
int k; in >> k;,再跑 →k变成 0(读Ada失败),而且整个循环什么都不输出(流已失败,15.4.1)。体会"一次读失败,后面全废"。 - 在 ② 里加一句"读前先
in >> word;,然后getline" → 第一个getline拿到空串(>>留下的空格)。这就是混用的坑(本节第四段)。 - 把文件里
Bob 88 1.78改成Bob abc 1.78再跑 ④ →Bob那一行被跳过(因为ss >> score失败)。证明"if (ss >> ...)的检查是有效的"。
七、常见误区
- "
while (!in.eof())是最标准的写法" ——恰恰相反,它是经典错误。eof()在"读越过末尾"之后才置位,所以会多循环一次。 正确写法:while (读操作)。 - "
getline读到的行里带着换行符" ——不带。它把换行符读走并丢掉(get则不会)。 - "
in >> s会读一整行到换行" ——不会,它按空白切。读到空格就停。 - "
>>和getline随便混用" ——中间必须清一次(>>会把分隔符留在流里)。 - "读失败就是文件读完了" ——不一定。也可能是格式不匹配(比如想读
int却碰上字母)。两者在状态上是不一样的(eofbitvsfailbit,15.4.1 讲)。 - "读文件时不用担心编码" ——要担心。如果文件是 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`。**- ☐ 下一节(15.3.4 二进制文件的读-写)