"打开文件"就是把流对象和磁盘上的某个文件绑起来:绑好了才能读写,绑不上(打开失败)后面全是空转。
两种时机:创建对象时顺手打开(最常用)或先建对象、再 open()。
本节最重要的不是语法,而是一条纪律:
每次打开后立刻检查 ——因为"打不开"这件事,标准库不会大声告诉你。
15.1 讲了"流"和"类库",本节开始真的去碰文件:
- 15.1.2 的五步走在这里展开成完整流程(本节讲"打开"这一步);
- 15.1.3 的 ios::in / out / app / trunc 是本节 open 的第二个参数;
- 10.1「类=图纸、对象=实物」在这里有个具体对照:ifstream in; 是造了个"还没接管的流对象",in.open("a.txt") 才把它接到文件上。
一句话:本节讲"怎么接上",15.3 讲"接上之后怎么读写"。
一、两种打开时机(效果一样,风格不同)
方式一:创建对象时就打开(推荐,最常用)
ifstream in("data.txt"); // 造对象 + 打开,一句话
ofstream out("result.txt");
fstream db("db.txt", ios::in | ios::out);
为什么推荐:少写一行、对象一出生就是"可用状态",后面不容易忘。
这个用法背后是构造函数在干活(10.2)——构造函数里既申请了缓冲,也把文件打开了。
方式二:先建对象,后 open()
ifstream in; // 先造一个"空壳"
in.open("data.txt"); // 再接到文件上
// ...
in.close(); // 断开
in.open("data2.txt"); // ★ 再接到另一个文件上(可以反复用)
什么时候需要它:
- 一个流对象要先后处理好几个文件(比如把日志按日期拆成多个文件);
- 文件名是运行时才算出来的(想用变量拼路径);
- 想把"打开"放到某个条件分支里。
注意:同一个流对象换文件之前,必须先 close()(15.5 会细讲)。
二、open() 的两个参数
void open(const char *filename, ios::openmode mode = 默认值);
| 参数 | 写什么 | 例子 |
|---|---|---|
| 文件名 | 字符串(C 风格字符串或 string) |
"data.txt"、"D:/cpp/a.dat" |
| 打开方式 | 用 \| 拼起来的 ios 常量(可省略) |
ios::in \| ios::binary |
省略第二个参数就用"默认方式" ——而默认方式因类而异(15.2.2 专门讲),这是最容易踩的坑:
ofstream out("a.txt"); // 默认含 trunc → 清空!
ofstream out("a.txt", ios::app); // 显式追加 → 保留原内容
三、打开之后必须检查(本节的核心纪律)
ifstream in("data.txt");
if (!in) { // ★ 最常用的写法
cerr << "打不开 data.txt" << endl;
return 1;
}
三种等价写法(都用得上,任选其一):
| 写法 | 含义 | 什么时候用 |
|---|---|---|
if (!in) |
流是"假"的(fail() 为真) |
最简洁,首选 |
if (in.fail()) |
同样的意思 | 想写明确一点时 |
if (!in.is_open()) |
问"文件是否真的开着" | 想强调"开没开"时 |
还有更现代的写法(C++11 起)——构造函数里直接判:
if (ifstream in{"data.txt"}) { // 打开成功才进这个分支
string line;
while (getline(in, line)) cout << line << endl;
} // ← 出作用域自动关闭(10.3 的析构)
else {
cerr << "打不开" << endl;
}
这个写法好在两点:① 检查和使用贴在一起,不会忘;② 变量只活在这个 if 里,出范围自动 close() ——这就是"资源交给对象管"的思路(14.3 留的那个 RAII 账,在这里已经能看到影子)。
四、打开失败了,是什么状态
打开失败时,流对象不会崩,只是"变成假的":
ifstream bad("no_such_file.txt");
cout << !bad << endl; // 1
cout << bad.fail() << endl; // 1
cout << bad.is_open() << endl; // 0
而且——注意这一点很重要:流会进入失败状态,之后的读写全部无效:
int x = 99;
bad >> x; // 什么都没发生
cout << x << endl; // 99(x 一点没变)
这跟 cin >> x 输入字母时的表现一模一样(2.5 / 15.1.4 讲过)——因为"失败状态"是流家族的通用机制,所有流共用(状态位定义在树根 ios 上,15.1.1 讲过)。
五、动手 Demo:两种打开时机 + 一个失败的例子
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
void show(const string &tag, ifstream &in) {
cout << tag << " -> !in=" << !in
<< " fail=" << in.fail()
<< " is_open=" << in.is_open() << endl;
}
int main() {
// ── 方式一:构造函数直接打开 ──────────────
ofstream out("open_demo.txt");
if (!out) { cerr << "写不了" << endl; return 1; }
out << "hello" << endl << "world" << endl;
out.close();
ifstream in1("open_demo.txt");
show("① 构造时打开", in1);
string line;
while (getline(in1, line)) cout << " " << line << endl;
in1.close();
// ── 方式二:先建对象,再 open(顺便换个文件)──
ifstream in2;
show("② 还没 open", in2); // 注意这一行
in2.open("open_demo.txt");
show("③ open 之后", in2);
in2.close();
// ── 一个失败的例子 ────────────────────────
ifstream in3("definitely_not_here.txt");
show("④ 打不开的文件", in3);
int x = 99;
in3 >> x; // 失败状态下,读操作无效
cout << " x 还是 " << x << "(没被改动)" << endl;
return 0;
}
看到什么算成功:
① 构造时打开 -> !in=0 fail=0 is_open=1
hello
world
② 还没 open -> !in=1 fail=1 is_open=0
③ open 之后 -> !in=0 fail=0 is_open=1
④ 打不开的文件 -> !in=1 fail=1 is_open=0
x 还是 99(没被改动)
读结果:
- ② 那一行最值得看:一个刚造好、还没
open的流对象,本身就处于失败状态(!in = 1)。所以"没打开"和"打开失败",从状态上看是一样的。 - ④ 说明:打不开时,流的状态跟"还没打开"完全一样 ——流不会告诉你"是因为文件不存在",还是"因为没打开"(想知道具体原因要靠系统调用,标准 C++ 不提供)。
- 最后一行 x 还是 99:失败状态下的读操作完全无效,而且不动你的变量 ——所以不检查的话,你会拿着一堆"看起来正常"的假数据继续往下跑。
六、30 秒实验
- 把 ② 的
in2.open(...)注释掉,后面直接while (getline(in2, line))→ 一行都不输出、也不报错。这就是"没打开就读写"的后果:静悄悄。 - 故意在文件名里写个中文路径或空格路径 → 常常打不开。体会"路径写错"有多容易。
- 把
ifstream in1("open_demo.txt");改成把open_demo.txt改个名字再跑 → ① 变成失败状态,hello/world不再输出。证明"检查"那一步不是形式主义。
七、常见误区
- "打开失败会抛异常" ——不会(默认情况下流不抛异常,15.4.1 会讲怎么打开异常)。它会安静地进入失败状态,必须自己
if (!in)。 - "
ifstream in;之后就能用了" ——不能。没open的流对象处于失败状态(Demo ② 就是证据)。 - "先建对象再
open是多此一举" ——它解决"一个对象处理多个文件"的场景,不是多余(本节第一段列了三个用例)。 - "换文件直接
open就行,不用close" ——必须先close()。同一个对象换文件前不关,行为不可靠(15.5 细讲)。 - "文件名只能写字符串字面量" ——可以传
string(大多数编译器接受),也可以运行时拼出来(比如"log_" + date + ".txt",注意用string拼不是+字面量)。 - "打开失败了,后面的读写会自动跳过" ——不会跳过,只是"无效"。代码照样往下跑,你的变量还是旧值 ——这是最危险的一种"看起来正常"。
自测三问(点开对答案)
1. **打开文件有哪两种时机?各适合什么场景?** ——**① 构造函数里直接打开**(`ifstream in("a.txt");`)——**推荐,最简洁**;**② 先建对象再 `open()`** ——适合**一个对象处理多个文件**、**文件名运行时才算出来**、**打开要放进条件分支**的场景。 2. **为什么"打开之后要检查"这件事这么重要?** ——**因为打开失败时标准库"安静失败"**:不崩、不报错,**只是把流置为失败状态**。**之后所有读写全部无效,却不影响程序继续跑** → **你会在错误数据上一直算下去。** 3. **一个刚 `ifstream in;` 声明、还没 `open()` 的对象,`fail()` 是什么?为什么?** ——**是 1(真)**。**因为"没关联到任何文件"在状态上等同于"打开失败"** ——`is_open()` 也是 0。**所以"忘了 open"和"打不开"表现一致,这也解释了为什么"忘了 open"很难被发现。**- ☐ 下一节(15.2.1 打开方式)