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

open() 是"关联"(把流对象和某个文件绑起来),close() 是"分离"(解开绑定)。 理解了这对动作,就明白两件事: - 一个流对象可以先后关联不同的文件(close 之后 open 新的)——换文件前必须先分离; - 流对象析构时会自动分离(析构函数调 close,10.3)——这就是"资源交给对象管"在文件上的第一次兑现。 一句话概括:流对象是"插头",open 是插上、close 是拔下——同一个插头能插不同的插座,但要先拔下来。

📝 本节和前面几节的关系

本节是 15.5~15.6 两节"收尾知识"的第一节,把"生命周期"这件事讲完: - 10.3 的析构函数在这里正式承担"自动关闭"的职责——14.3 留的 RAII 账,在这里还了第一笔; - 15.2 的"两种打开时机"在这里有了新的理解:"先建对象、后 open"正是为了"一个对象处理多个文件"; - 15.4.1 的失败状态在这里再次出现:在失败的流上 open 会失败。 一句话:本节讲"流对象的一生"。

一、关联与分离:一对动作

动作 方法 效果
关联 open("文件") 或构造函数 流对象连上一个文件;开始能读写
分离 close() 断开连接;刷新缓冲区;可以再关联别的文件

三个要点:

  1. 不关联就不能读写 ——刚声明、还没 open 的流对象是"失败状态"(15.2 的 Demo ② 见过);
  2. close() 会刷新缓冲区(15.1 / 15.3.1)——所以"写完要关"不只是"礼貌",是"保证数据落地";
  3. close() 之后对象并没有废,还能再用。

二、一个流对象,先后处理多个文件

这是 open() / close() 存在的最大理由。看这个模式:

ofstream out;                            // ① 先建"空壳"
for (int i = 0; i < 3; i++) {
    out.open(names[i]);                  // ② 关联
    out << "..." << endl;
    out.close();                         // ③ 分离 —— 换下一个之前必须做
}

为什么必须先 close()?

因为"一个流对象同一时刻只能连一个文件"。 它已经连着一个的时候再 open,标准规定会直接失败(置 failbit) ——不会自动帮你关掉前一个。

所以"没关就换"的结果是:新文件根本没被打开,后面的写全部无效、还不报错。

三、析构:忘了 close() 时的最后一道保险

流对象的析构函数会调用 close()(10.3 的机制)——所以你写:

{
    ofstream out("a.txt");
    out << "hello" << endl;
}                        // ← 出作用域,析构函数自动 close(顺便刷缓冲)

这在大多数情况下都能正常工作 ——这就是"资源交给对象管"的价值(14.3 留的 RAII 账)。

但为什么还是要养成"显式 close()"的习惯? 三个理由:

理由 说明
① 让数据"尽早"落地 析构发生在"出作用域"那一刻,中间可能还有很长一段代码,这期间数据都在缓冲里
② 显式表达意图 读代码的人能立刻看到"这里我写完了"
③ 出异常时更可控 析构的时机由作用域决定,而 close() 的位置由你决定

结论:"靠析构兜底"可以接受,但"显式 close"更好。

四、一个容易被想到的问题:一个文件能被几个流同时打开?

答案:标准没有规定,由实现决定。

实际情况:

结论:

同一个文件,同一个时刻,只让一个流"拿着"它。 要"一读一写",就在同一个 fstream 里做(ios::in | ios::out);要"两个程序同时写",那是另一门学问(文件锁、数据库)。

五、动手 Demo:关联、分离、换文件

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

int main() {
    // ══ ① 一个 ofstream 对象,先后写三个文件 ══
    {
        ofstream out;                                  // 先建空壳
        const char *names[3] = {"f1.txt", "f2.txt", "f3.txt"};
        for (int i = 0; i < 3; i++) {
            out.clear();                               // ★ 万一上一轮出过错,先复位
            out.open(names[i]);                        // 关联
            if (!out) { cerr << "打开失败: " << names[i] << endl; continue; }
            out << "file " << (i + 1) << endl;
            out.close();                               // 分离
        }
        cout << "① 一个 ofstream 依次写完了 3 个文件" << endl;
    }

    // ══ ② 忘了 close 就换文件:open 会失败 ══
    {
        ofstream out("g1.txt");
        out << "first" << endl;
        out.open("g2.txt");                            // ★ 忘了先 close
        cout << "② 没 close 就 open 另一个: fail=" << out.fail()
             << "(失败)" << endl;
        out << "second" << endl;                       // 无效:流已失败
        out.close();                                   // 关掉的是 g1

        ifstream a("g1.txt");
        string l1; getline(a, l1);
        cout << "   g1.txt 内容: [" << l1 << "]" << endl;
        a.close();

        ifstream b("g2.txt");
        cout << "   g2.txt 存在吗: " << (b ? "在" : "不在(根本没被创建)") << endl;
        b.close();
    }

    // ══ ③ close 之后再 open:正常换文件 ══
    {
        ofstream out("h1.txt");
        out << "hello h1" << endl;
        out.close();                                   // ★ 先分离
        out.open("h2.txt");                            // ★ 再关联
        cout << "③ close 后换文件: fail=" << out.fail() << "(成功)" << endl;
        out << "hello h2" << endl;
        out.close();

        ifstream b("h2.txt");
        string l2; getline(b, l2);
        cout << "   h2.txt 内容: [" << l2 << "]" << endl;
        b.close();
    }

    // ══ ④ rdbuf():一行把整个文件打到屏幕 ══
    {
        ifstream in("h1.txt");
        if (!in) { cerr << "打不开 h1.txt" << endl; return 1; }
        cout << "④ rdbuf 直接输出整个文件: " << in.rdbuf();
        in.close();
    }
    return 0;
}

看到什么算成功:

① 一个 ofstream 依次写完了 3 个文件
② 没 close 就 open 另一个: fail=1(失败)
   g1.txt 内容: [first]
   g2.txt 存在吗: 不在(根本没被创建)
③ close 后换文件: fail=0(成功)
   h2.txt 内容: [hello h2]
④ rdbuf 直接输出整个文件: hello h1

读结果:

  1. ① 是本节的正面示范:一个对象,三次"关联—写—分离",三个文件都写好了。
  2. ② 是反面教训:open 失败(fail=1)、g2.txt 没被创建、后面的写全部无效 ——而且没有任何提示(15.4.1 讲的"点灯不看灯")。注意我在 ① 里写了 out.clear() ——因为循环里如果上一轮失败过,流会卡住(15.4.1)。
  3. ③ 证明"先 close 再 open 就能换文件" ——fail=0、新文件有内容。
  4. ④ 的 in.rdbuf() 是"把整个文件塞给输出流"的常用技巧:cout << in.rdbuf(); ——比逐行读再打印简洁得多(因为它把"文件缓冲"直接接到"屏幕缓冲",中途不经过你的变量)。

六、30 秒实验

  1. 把 ① 里的 out.clear(); 删掉,然后故意让 f1.txt 打不开(比如改成 "sub/f1.txt",目录不存在)→ 后面两个文件也都打不开了(流已失败)。这就是"一个坏了,全都废"。
  2. 把 ② 的 out.open("g2.txt"); 前面加一句 out.close(); → 立刻变成"成功"。一行动作,天壤之别。
  3. 把 ④ 的 in.rdbuf() 换成一个空的 ifstream(文件不存在) → 输出为空、也不崩(rdbuf 给的是个无效指针,cout << 对它无动作)。别指望它报错。

七、常见误区

  1. "close() 之后这个对象就作废了" ——没有废。close() 只是"分离",对象还能 open 新文件(Demo ③)。
  2. "一个流对象同时能连多个文件" ——不能。同一时刻只连一个;连着的时候再 open 会失败(Demo ②)。
  3. "不关就换文件,系统会自动帮我关" ——不会。只会让新的 open 失败。
  4. "靠析构自动关闭就够了" ——大多数时候确实够了,但"显式 close()"更好(数据尽早落地、意图清楚、异常时更可控)。
  5. "一个文件可以随便用几个流一起读写" ——别这么干。多个写流会互相覆盖(各自的缓冲互不知道)。要同时读写,用一个 fstream。
  6. "rdbuf() 是给人用的常规写法" ——它是"快捷通道",适合"原样打印整个文件"。要做处理,还是老实用 getline / read。
自测三问(点开对答案) 1. **"关联"和"分离"分别对应哪个动作?一个流对象能不能处理多个文件?** ——**`open()` 是关联、`close()` 是分离。** **可以处理多个文件,但必须"先 `close` 再 `open`"** ——**同一时刻只能连一个文件。** 2. **忘了 `close()` 就 `open` 另一个文件,会发生什么?** ——**新的 `open` 会失败(置 `failbit`),新文件不会被创建或打开**,**而后续的读写全部无效、且不报错**。(**且 `close()` 关掉的还是原来那个文件。**) 3. **既然析构函数会自动 `close()`,为什么还要显式关?** ——**① 让数据尽早落地**(不用等到出作用域);**② 把"我写完了"这件事写明白**;**③ 出异常时关闭时机更可控**。**靠析构兜底可以接受,但显式 `close` 更好。**

相关笔记