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

用文件流永远是五步:包含头文件 → 创建流对象 → 打开文件 → 像用 cin/cout 那样读写 → 关闭。 其中最容易被跳过、也最不能跳过的是第三步之后的一句"检查": 打开可能失败,而且失败不报错 ——文件不存在、路径写错、没有权限、被别的程序占着,ifstream in("x.txt"); 都静静地失败。 一句话概括:文件操作是本章最容易出错的地方,因为它失败时不吭声——所以"每次打开都检查"不是谨慎,是基本功。

📝 本节和前面几节的关系

前面两节讲了"流是什么"和"家族长什么样",本节是第一次真正动手: - 8.6.1 学过的"2 的幂做标志位"在这里第二次上岗(15.1.3 的打开方式就是标志位组合); - 第十四章讲过的"该用返回值还是异常"在这里有了第一个现实场景:文件打开失败是"高频且就地能处理"的,所以标准库选了状态位 + 返回值(而不是抛异常); - 10.3 的析构函数在后面兜着:忘了 close(),对象析构时会自动关(但别指望它,15.5 会说清楚)。 一句话:本节把"知道"变成"会写"。

一、五步走:一个文件 Stream 的完整生命周期

① 包含头文件

#include <fstream>      // 文件流
#include <iostream>     // cout(提示信息用)
#include <string>       // string(读行用)

② 创建流对象 —— 三种姿势,效果一样

ifstream in;                             // 先造对象,后打开
ifstream in("data.txt");                 // 造对象的同时打开(最常用)
ifstream in("data.txt", ios::in);        // 造对象 + 打开 + 指定方式

③ 打开文件(如果②没顺手打开)

in.open("data.txt");
in.open("data.txt", ios::in);            // 带打开方式

④ 检查是否成功(这一步绝不能省)

if (!in) {                               // 最常用的写法
    cerr << "打不开文件" << endl;
    return 1;
}

等价写法还有:

if (in.fail()) { ... }                   // 同上(!in 就是 in.fail())
if (!in.is_open()) { ... }               // 问"文件开没开"

⑤ 读写 —— 和 cin/cout 一模一样

int x;
in >> x;                     // 读一个数
string line;
getline(in, line);           // 读一整行
out << "hello" << endl;      // 写

⑥ 关闭

in.close();
out.close();

完整的最短可用程序(写一遍,胜过读十遍):

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

int main() {
    // 写
    ofstream out("note.txt");
    if (!out)                                                // ← 永远检查
        { cerr << "写不了 note.txt" << endl; return 1; }
    out << "hello file" << endl;
    out.close();                                             // ← 写完就关

    // 读
    ifstream in("note.txt");
    if (!in)
        { cerr << "读不了 note.txt" << endl; return 1; }
    string line;
    while (getline(in, line))                                // ← 惯用法
        cout << "读到: " << line << endl;
    in.close();
    return 0;
}

看到什么算成功:

读到: hello file

文件夹里多一个 note.txt。

二、为什么"检查"这一步不能省

对比一下两类操作:

操作 会不会失败 失败了会怎样
int x = 5; 不会 —
x = y / z;(z 是 0) 会 崩溃(或未定义行为)
cin >> x;(输入字母) 会 流进入失败状态,不崩
ifstream in("x.txt"); 常常会 流进入失败状态,不崩、还不报错

注意最后两行共有的那个特点:不崩、也不报错。

为什么会失败? 至少五种常见原因:

  1. 文件根本不存在(读的时候最常见);
  2. 路径写错了(尤其 Windows 的 \ 要写 \\);
  3. 没有权限(比如想写 C:\Windows\ 下面);
  4. 文件被别的程序占用(比如用 Excel 打开了,或者你上一次运行没关);
  5. 目录不存在(ofstream 能建文件,但不会帮你建目录)。

标准库选择了"安静失败",这是有道理的:

文件打不开是"高频且就地能处理"的情况(提示一句就完了), 而异常适合"罕见且本层处理不了"的情况(14.2)。

所以这里用的是状态位 + 返回值那一路——但代价是:你必须自己检查。

⚠️ 不检查会怎样

ifstream in("不存在.txt"); int x; in >> x; → x 保持原样(垃圾值); string line; getline(in, line); → line 变成空串; 程序继续往下跑,用着假数据,一路错到底,而且不告诉你。

三、文件名怎么写

三种写法:

ifstream in("data.txt");                             // ① 相对路径:exe 所在目录下
ifstream in("sub/data.txt");                         // ② 相对路径 + 子目录
ifstream in("D:\\cpp\\data.txt");                    // ③ 绝对路径(★ 反斜杠要写两个)

两个必须知道的点:

四、动手 Demo:把五步走一遍,并且"故意失败一次"

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

int main() {
    // ── 第一次:正常写 + 正常读 ────────────────
    ofstream out("usage.txt");
    if (!out) { cerr << "① 打开失败(写)" << endl; return 1; }
    out << "line one" << endl;
    out << "line two" << endl;
    out.close();
    cout << "① 已写入 usage.txt" << endl;

    ifstream in("usage.txt");
    if (!in) { cerr << "② 打开失败(读)" << endl; return 1; }
    string line;
    int n = 0;
    while (getline(in, line)) {
        n++;
        cout << "② 第 " << n << " 行: " << line << endl;
    }
    in.close();

    // ── 第二次:故意打开一个不存在的文件 ────────
    ifstream bad("no_such_file.txt");
    cout << "③ 打开 no_such_file.txt 的结果:" << endl;
    cout << "   !bad     = " << !bad << endl;
    cout << "   bad.fail() = " << bad.fail() << endl;
    cout << "   bad.is_open() = " << bad.is_open() << endl;
    return 0;
}

看到什么算成功:

① 已写入 usage.txt
② 第 1 行: line one
② 第 2 行: line two
③ 打开 no_such_file.txt 的结果:
   !bad     = 1
   bad.fail() = 1
   bad.is_open() = 0

读结果:

改一下再试:把 ③ 的 ifstream bad(...) 换成 ofstream bad(...) ——会发现它成功了(!bad 变成 0),而且文件夹里真的多出一个 no_such_file.txt。为什么?

因为写模式(out)打开不存在的文件时,会顺手创建它;而读模式(in)不会。 ——这正是 15.2.3 要讲的"在打开文件的同时创建文件"。

五、30 秒实验

  1. 把两处 if (!out) / if (!in) 检查删掉,再跑一次 → 程序照样跑完,只是什么都不输出。体会一下"安静失败"有多安静。
  2. 把 usage.txt 改成 sub/usage.txt(子目录不存在) → 打开失败。证明"会自动建文件,但不会自动建目录"。
  3. 写完后立刻用记事本打开 usage.txt → 有内容(因为 close() 了)。然后把 out.close(); 注释掉再试 → 多半也能看到(程序结束时自动刷新)。再试一次"写完立刻读"(不 close) → 可能读不到。 这就是缓冲(15.1 讲的)。

六、常见误区

  1. "文件打不开,程序会报错提醒我" ——不会。标准库选择安静失败,必须自己写 if (!in)。
  2. "写完文件就能立刻读回来" ——不一定。数据可能还在缓冲区。先 close()(或 flush())。
  3. "ofstream out("x.txt") 打开一个已有文件没关系" ——有关系,而且是大事:ofstream 的默认模式含 trunc,会把文件清空。这是本章第一号"数据没了"事故。(15.2.1 细说)
  4. "相对路径是相对于 .cpp 文件" ——是相对于 .exe(也就是程序运行时的"当前工作目录")。在 Dev-C++ 里两者通常在一起,所以看不出来;换个环境就会懵。
  5. "路径里的 \ 直接写就行" ——\ 在字符串里是转义符,"D:\new\test.txt" 里的 \n 会被当成换行。要写 "D:\\new\\test.txt" 或 "D:/new/test.txt"。
  6. "close() 可写可不写,反正程序结束会自动关" ——大多数时候确实会自动关(析构函数负责),但:① 写的数据可能还没刷;② 想换文件必须先关;③ 出异常时析构时机可能出乎意料。该关就关,是习惯问题。
自测三问(点开对答案) 1. **用文件流的五步是什么?** ——**① `#include `;② 创建流对象;③ 打开(常在②顺手做);④ 检查是否成功;⑤ 读写;⑥ `close()`。** 其中**第 ④ 步最容易被跳过,也最不能跳**。 2. **为什么打开文件失败时"什么都不提示"?该怎么应对?** ——**标准库认为"文件打不开"是高频且就地能处理的情况,所以走"状态位 + 返回值"而不是异常**(14.2 的判据)。**应对方式:每次打开后都写 `if (!in) { ... }`**(`!in` 等价于 `in.fail()`)。 3. **`ofstream out("sub/x.txt")` 在 `sub` 目录不存在时,会发生什么?** ——**打开失败,而且不会自动建目录**(虽然写模式会自动建"文件")。**必须先保证目录存在**(C++17 的 `` 才能建目录,15.6 会提)。

相关笔记