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

"打开文件"就是把流对象和磁盘上的某个文件绑起来:绑好了才能读写,绑不上(打开失败)后面全是空转。 两种时机:创建对象时顺手打开(最常用)或先建对象、再 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(没被改动)

读结果:

六、30 秒实验

  1. 把 ② 的 in2.open(...) 注释掉,后面直接 while (getline(in2, line)) → 一行都不输出、也不报错。这就是"没打开就读写"的后果:静悄悄。
  2. 故意在文件名里写个中文路径或空格路径 → 常常打不开。体会"路径写错"有多容易。
  3. 把 ifstream in1("open_demo.txt"); 改成把 open_demo.txt 改个名字再跑 → ① 变成失败状态,hello/world 不再输出。证明"检查"那一步不是形式主义。

七、常见误区

  1. "打开失败会抛异常" ——不会(默认情况下流不抛异常,15.4.1 会讲怎么打开异常)。它会安静地进入失败状态,必须自己 if (!in)。
  2. "ifstream in; 之后就能用了" ——不能。没 open 的流对象处于失败状态(Demo ② 就是证据)。
  3. "先建对象再 open 是多此一举" ——它解决"一个对象处理多个文件"的场景,不是多余(本节第一段列了三个用例)。
  4. "换文件直接 open 就行,不用 close" ——必须先 close()。同一个对象换文件前不关,行为不可靠(15.5 细讲)。
  5. "文件名只能写字符串字面量" ——可以传 string(大多数编译器接受),也可以运行时拼出来(比如 "log_" + date + ".txt",注意用 string 拼不是 + 字面量)。
  6. "打开失败了,后面的读写会自动跳过" ——不会跳过,只是"无效"。代码照样往下跑,你的变量还是旧值 ——这是最危险的一种"看起来正常"。
自测三问(点开对答案) 1. **打开文件有哪两种时机?各适合什么场景?** ——**① 构造函数里直接打开**(`ifstream in("a.txt");`)——**推荐,最简洁**;**② 先建对象再 `open()`** ——适合**一个对象处理多个文件**、**文件名运行时才算出来**、**打开要放进条件分支**的场景。 2. **为什么"打开之后要检查"这件事这么重要?** ——**因为打开失败时标准库"安静失败"**:不崩、不报错,**只是把流置为失败状态**。**之后所有读写全部无效,却不影响程序继续跑** → **你会在错误数据上一直算下去。** 3. **一个刚 `ifstream in;` 声明、还没 `open()` 的对象,`fail()` 是什么?为什么?** ——**是 1(真)**。**因为"没关联到任何文件"在状态上等同于"打开失败"** ——`is_open()` 也是 0。**所以"忘了 open"和"打不开"表现一致,这也解释了为什么"忘了 open"很难被发现。**

相关笔记