每个流对象身上有四盏"状态灯"(状态位):
- goodbit(一切正常)· eofbit(读到末尾了)· failbit(这次操作失败了)· badbit(出大问题了);
四个查询函数:good() / eof() / fail() / bad();一个"重启键":clear()。
本节最重要的两条规则:
① fail() 包含 bad()(fail() 为真时 bad() 可能真也可能假,但 bad() 为真时 fail() 一定为真);
② "读到末尾"会同时置 eofbit 和 failbit ——所以不能用 eof() 判断"读失败"(15.4.3 的关键前提)。
一句话概括:"出错"这件事在流里不是抛异常,而是"点亮一盏灯"——你必须自己去看灯。
本节是 15.4 三节里最"底层"的一节,它解释了前面反复出现的几个现象:
- 15.2 的"打开失败不报错" ——失败在哪?就在 failbit 上;
- 15.3.3 的"while (!in.eof()) 为什么会多一次" ——本节给出机制:eofbit 是"读越过末尾之后"才点亮的;
- 15.4 的"读完后必须 clear() 才能重读" ——本节解释 clear() 到底清什么;
- 14.2 学的异常在这里有个对照:流默认不抛异常,用的是"状态位 + 检查"(要抛还得自己开开关)。
一句话:本节是"文件操作为什么不报错"的总答案。
一、四盏灯:状态位
| 状态位 | 查询函数 | 意思 | 严重程度 |
|---|---|---|---|
goodbit |
good() |
一切正常,可以继续 | — |
eofbit |
eof() |
上一次读遇到了文件末尾 | 提示 |
failbit |
fail() |
上一次操作失败了(格式不对 / 打开失败 / 读到末尾) | 警告 |
badbit |
bad() |
发生了严重错误(磁盘故障、缓冲坏了) | 严重 |
四者的关系(两条必须记住):
① fail() 为真 ⟸ failbit 或 badbit 被置
bad() 为真 ⟸ 只有 badbit 被置
→ 所以:bad() 为真时 fail() 一定为真;反之不成立
② good() 为真 ⟺ 所有位都是干净的(没有任何错误)
good() = !fail() (严格说是 !fail() 且 ... 实际上就是"没出错")
一句话:fail() 是"这次不太好",bad() 是"这次很糟"。
两个"简写"要认得:
| 写法 | 等价于 | 说明 |
|---|---|---|
!in |
in.fail() |
最常写的那个 |
if (in) |
!in.fail()(流是好的) |
while (getline(in, line)) 靠的就是它 |
这就把前面所有代码串起来了:
if (!in)检查的是fail();while (in >> x)检查的也是fail()。fail()才是"流能不能用"的总开关。
二、什么时候点亮哪盏灯
| 情况 | eof() | fail() | bad() |
|---|---|---|---|
| 读到正常读完(到末尾) | ✓ | ✓ | — |
格式不匹配(想读 int 却碰上字母) |
— | ✓ | — |
| 打开失败(文件不存在/无权限) | — | ✓ | — |
| 在失败的流上再做操作 | — | ✓ | — |
| 磁盘故障、文件被突然移除 | 可能 | ✓ | ✓ |
clear() 之后 |
清 | 清 | 清 |
最值得盯住的是第一行和第二行的对照:
"读到末尾"和"读错格式",
fail()都会为真 ——区别是"读完"会额外点亮eofbit。 反过来说:光看fail()分不清"读完了"还是"读错了"。 要区分就得看eof()。
这正是 15.4.3 的伏笔:eof() 只表示"碰到末尾了",不表示"这一次没读到东西"。
三、clear():唯一的"重启键"
作用:把所有状态位清干净,让流恢复"可用"。
in.clear(); // 清掉 eofbit / failbit / badbit,回到 good
三个必须用 clear() 的场景:
| 场景 | 为什么 |
|---|---|
| 读完后要重新读一遍 | 读到末尾置了 failbit,不 clear() 的话连 seekg 都被忽略(15.4) |
| 输入格式错了,想重试 | cin >> x 输了字母后,流罢工了,必须 clear() 才能再读 |
| 打开失败后想换个文件重试 | 流卡在失败状态,clear() 之后再 open() |
一个更实际的例子(读键盘输入):
int n;
while (!(cin >> n)) { // 读到非数字就一直重试
cin.clear(); // ★ 先清状态
cin.ignore(1000, '\n'); // ★ 再把这一行垃圾吃掉
cout << "请输入一个整数:";
}
注意这里有两个动作:clear() 治状态、ignore() 清垃圾。只做前一个会陷入死循环(因为那堆字母还在,一读又错)。
这条"两连击"请记住:clear() + ignore()。
四、一个反直觉的点:流默认不抛异常
你可能会问:文件打不开、读格式错,为什么不是异常?
因为标准库的设计是"状态位 + 显式检查",不是"异常驱动":
- 流的
exceptions()默认是"什么都不抛"; - 你如果想让它抛,得自己开:
in.exceptions(ifstream::failbit | ifstream::badbit); // 现在失败会抛异常
为什么默认不抛? 因为"读不到数据"在文件/输入场景里太常见了(读到末尾、用户输错),用异常来处理"天天发生的事"是错的(14.2 讲过判据:罕见 + 本层处理不了 才用异常)。
所以流的做法是:
"出事就点灯,你自己看灯。"
这也解释了为什么"不检查 if (!in)"这么危险 ——灯亮着,你不看,程序就带着坏数据一路跑下去。
五、动手 Demo:把四盏灯都点亮一次
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
void state(const string &tag, ifstream &in) {
cout << tag
<< " good=" << in.good()
<< " eof=" << in.eof()
<< " fail=" << in.fail()
<< " bad=" << in.bad()
<< " !in=" << !in << endl;
}
int main() {
// ══ 场景一:正常读到末尾 ══
{
ofstream o("state.txt");
o << "10 20 30\n";
o.close();
}
ifstream in("state.txt");
state("① 刚打开 ", in);
int x = 0;
in >> x; state("② 读了 10 ", in);
in >> x; state("③ 读了 20 ", in);
in >> x; state("④ 读了 30 ", in);
in >> x; state("⑤ 越过末尾读 ", in); // ★ 这里同时点亮 eof 和 fail
in.clear();
state("⑥ clear 之后 ", in);
in.close();
// ══ 场景二:格式不匹配(fail 亮、eof 不亮)══
{
ofstream o("bad.txt");
o << "abc 123\n";
o.close();
}
ifstream in2("bad.txt");
int n = 0;
in2 >> n; // 想读 int,碰上字母
state("⑦ 格式不匹配 ", in2);
in2.close();
// ══ 场景三:打开失败 ══
ifstream in3("definitely_missing.txt");
state("⑧ 打开失败 ", in3);
in3.close();
// ══ 场景四:clear 之后重读成功 ══
ifstream in4("state.txt");
string s;
while (getline(in4, s)) { } // 读到末尾
state("⑨ 读完一遍 ", in4);
in4.clear();
in4.seekg(0, ios::beg);
if (getline(in4, s)) cout << "⑩ 重读成功: " << s << endl;
in4.close();
return 0;
}
看到什么算成功:
① 刚打开 good=1 eof=0 fail=0 bad=0 !in=0
② 读了 10 good=1 eof=0 fail=0 bad=0 !in=0
③ 读了 20 good=1 eof=0 fail=0 bad=0 !in=0
④ 读了 30 good=1 eof=0 fail=0 bad=0 !in=0
⑤ 越过末尾读 good=0 eof=1 fail=1 bad=0 !in=1
⑥ clear 之后 good=1 eof=0 fail=0 bad=0 !in=0
⑦ 格式不匹配 good=0 eof=0 fail=1 bad=0 !in=1
⑧ 打开失败 good=0 eof=0 fail=1 bad=0 !in=1
⑨ 读完一遍 good=0 eof=1 fail=1 bad=0 !in=1
⑩ 重读成功: 10 20 30
读结果——请对照着看这三组的差别:
| 行 | eof | fail | 说明 |
|---|---|---|---|
| ⑤ 越过末尾读 | 1 | 1 | 读完会同时点亮两盏 |
| ⑦ 格式不匹配 | 0 | 1 | 读错只亮 fail,eof 不动 |
| ⑧ 打开失败 | 0 | 1 | 跟⑦一样,fail 亮 |
这三行就是本节的核心结论:
fail()是"总开关" ——三种情况它都亮(读完、读错、打不开)。所以if (!in)是万能的检查。eof()只在"读越过末尾"时亮 ——所以它不能当"有没有错"的判断依据。(⑦⑧ 就是反例:出错了,但eof是 0。)bad()全程是 0 ——没出真正的硬件故障,一切都还在"可恢复"的范围内。- ⑥ 证明了
clear()的作用:四盏灯全灭、good()恢复 1。 配合 ⑩(clear+seekg+ 重读成功),就完成了"流复活"的完整流程。
六、30 秒实验
- 把 ⑥ 的
in.clear()删掉,然后接着in.seekg(0, ios::beg); in >> x;→x不会被改动。证明"失败的流连挪位置都不肯"。 - 把 ⑦ 的文件内容改成
"123 abc",先读一个int再读一个int→ 第一次成功、第二次fail亮但eof不亮。再次确认"格式错 ≠ 读完"。 - 在 ⑧ 之后加一句
in3.clear(); in3.open("state.txt");→ 打开成功。证明"打开失败后要换文件,必须先clear()"。
七、常见误区
- "
eof()为真就表示出错了" ——读完是"正常结束",不是错误。只是fail()也会跟着亮(这一点最容易让人误解)。 - "
fail()为真就是文件读完了" ——不一定:格式错、打不开也一样fail。 要区分必须看eof()。 - "
bad()和fail()是并列的两件事" ——bad()是fail()的一种(更严重的) ——bad()为真时fail()一定为真。 - "流会自己抛异常,不用检查" ——默认不抛。必须自己检查
if (!in)(想抛要显式exceptions())。 - "
clear()会把流里的垃圾数据也清掉" ——不会。clear()只清"状态位",不动数据。清数据要用ignore()或读走它(本节第三段那对"两连击")。 - "反正程序会崩,不检查也行" ——最危险的就是"不崩":流失败了,程序照跑,你的变量还是旧值/垃圾值 ——错误会一路静默传播。
自测三问(点开对答案)
1. **四个状态位分别是什么?四个查询函数叫什么?** ——`goodbit` / `eofbit` / `failbit` / `badbit`,对应 **`good()` / `eof()` / `fail()` / `bad()`**。**`!in` 等价于 `fail()`**,**`if (in)` 等价于"流是好的"。** 2. **"读到末尾"会点亮哪几盏灯?为什么说不能用 `eof()` 判断"出没出错"?** ——**会同时点亮 `eofbit` 和 `failbit`。** 而**"格式错"和"打开失败"只点亮 `failbit`、`eofbit` 不动** ——**所以 `eof()` 只说明"碰到了末尾",与"出没出错"不是一回事。判断"能不能继续用"要看 `fail()`。** 3. **`clear()` 做什么?读了一遍想再读一遍,为什么要 `clear()` + `seekg(0)` 两步?** ——**`clear()` 把状态位清干净,让流恢复可用。** **因为读到末尾会置 `failbit`,而失败状态的流会忽略 `seekg`** ——**所以必须先"治状态"(`clear`)、再"挪位置"(`seekg`),两步缺一不可。**- ☐ 下一节(15.4.2 文件的追加)