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

六个常量 in / out / app / ate / trunc / binary,用 | 拼出你的意图。 本节的重点不在"认"这六个词(15.1.3 已经列过),而在"选"——同样一句 ofstream out("a.txt"),加不加 app 是"接在后面写"还是"把文件清空"的天壤之别。 一句话概括:打开方式是"这次要对文件做什么"的完整声明——写清楚它,比写对代码更重要。

📝 本节和前面几节的关系

15.1.3 给了你常量表(有什么),本节给你决策表(该用哪个): - 15.1.3 讲过这些常量是"2 的幂标志位"——所以本节全部用法都是 | 组合; - 本节要讲清最容易混的三对:app vs ate、trunc vs app、文本 vs binary; - 15.4.2「文件的追加」会把 app 再展开一次(那里配合"写位置"讲得更透); - 15.3.4 会把 binary 展开。 一句话:本节是"打开方式"的总纲,后面两节是它的两个重点补充。

一、六个常量:是什么、用在哪

常量 效果 典型场景
ios::in 为读取打开 读已有文件
ios::out 为写入打开 写新内容(常带 trunc)
ios::app 追加(append):每次写都自动到末尾 日志、不断累积的记录
ios::ate 打开后先移到末尾(at end):之后可以自由挪 想从末尾开始、但还要回头改
ios::trunc 截断:文件存在就清空 重新生成报表
ios::binary 二进制模式:不做换行符转换 图片、结构体、可执行文件(15.3.4)

前四个是"访问方式",第五个是"要不要清空",第六个是"用什么格式"。 它们互不冲突,所以能组合。

二、最容易混的三对,一次分清

第一对:ios::app vs ios::ate(差别在"以后")

ios::app ios::ate
打开时的位置 末尾 末尾
打开之后能不能 seek 到别处写 不能——每次写都被拉回末尾 能——seekp 到哪儿就写哪儿
本质 "以后所有写都追加" "只是开局先站到末尾"
适合 日志文件 要在文件尾附近改数据

一句话记住:app 是"只能往后写",ate 是"先站到最后,然后随便你"。

第二对:ios::trunc vs ios::app(差别在"旧内容")

ios::trunc ios::app
打开时旧内容 全部清空 原样保留
第一次写的落点 开头 末尾
适合 重新生成 累积追加
危险程度 高(数据说没就没) 低

这两个是本节最需要记住的一对——因为它们决定"原来那个文件还在不在"。

第三对:文本模式 vs ios::binary(差别在"怎么解释字节")

结论:处理"给程序看的数据"(结构体、图片、exe)必须加 binary,否则文件内容会被悄悄改写(15.3.4 详讲)。

三、决策表:我想干什么 → 用什么打开方式

这张表建议直接记住。

我想干什么 打开方式 用哪个类
读一个已有文件 ios::in ifstream
覆盖写(不要旧内容) ios::out \| ios::trunc ofstream
追加写(保留旧内容) ios::out \| ios::app ofstream
读 + 写,保留原内容 ios::in \| ios::out fstream
读 + 写,清空重建 ios::in \| ios::out \| ios::trunc fstream
二进制读 ios::in \| ios::binary ifstream
二进制写(覆盖) ios::out \| ios::binary ofstream
二进制追加 ios::out \| ios::app \| ios::binary ofstream

注意:ios::trunc 和 ios::app 不要同时写。 一个说"清空"、一个说"接着写",同时出现时结果由实现(编译器)决定,标准没规定。别制造这种歧义。

四、本节第一号事故:ofstream 的默认方式带 trunc

ofstream out("important.txt");     // ★ 这一行跑过去,important.txt 就空了

为什么? 因为 ofstream 的默认方式里含 trunc(15.2.2 会列全表)。

于是这三行的效果,往往与新手预期相差十万八千里:

ofstream out("log.txt");                    // ❌ 清空!日志全没了
ofstream out("log.txt", ios::app);          // ✅ 追加
ofstream out("log.txt", ios::out | ios::app);   // ✅ 同上,写全了更清楚

建议:写文件时,习惯性地把模式写出来。

ofstream out("log.txt", ios::app);          // 一眼能看出"我在追加"

代码是给人看的——多写五个字符,省下的是"半年后回来一看,这文件到底清不清空"的疑惑。

五、动手 Demo:三种方式,三种命运

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

// 把整个文件内容打出来
void dump(const string &fname) {
    ifstream in(fname);
    if (!in) { cout << "  (" << fname << " 打不开)" << endl; return; }
    string line;
    bool any = false;
    while (getline(in, line)) { cout << line << " | "; any = true; }
    if (!any) cout << "(空)";
    cout << endl;
    in.close();
}

int main() {
    // ── ① trunc:每次打开都清空 ──────────────
    { ofstream o("m_trunc.txt", ios::out | ios::trunc); o << "A" << endl; }
    cout << "① trunc 第一次: "; dump("m_trunc.txt");

    { ofstream o("m_trunc.txt", ios::out | ios::trunc); o << "B" << endl; }
    cout << "① trunc 第二次: "; dump("m_trunc.txt");

    // ── ② app:接着往后写 ────────────────────
    { ofstream o("m_app.txt", ios::out | ios::app); o << "A" << endl; }
    { ofstream o("m_app.txt", ios::out | ios::app); o << "B" << endl; }
    cout << "② app  两次后 : "; dump("m_app.txt");

    // ── ③ ate:开局站末尾,但还能回头改 ───────
    { ofstream o("m_ate.txt"); o << "12345" << endl; }
    fstream f("m_ate.txt", ios::in | ios::out | ios::ate);
    if (!f) { cout << "③ 打开失败" << endl; return 1; }
    cout << "③ ate 打开后写位置: " << (long long)f.tellp() << "(在末尾)" << endl;
    f.seekp(0, ios::beg);          // ★ ate 允许回头
    f << "XY";                     // 覆盖开头两个字符
    f.close();
    cout << "③ seekp(0) 写入 XY 后: "; dump("m_ate.txt");
    return 0;
}

看到什么算成功:

① trunc 第一次: A | 
① trunc 第二次: B | 
② app  两次后 : A | B | 
③ ate 打开后写位置: 6(在末尾)
③ seekp(0) 写入 XY 后: XY345 | 

数字怎么来的:

读结果——三句话:

  1. ① 证明 trunc 每次都清空:第二次跑完,A 不见了,只剩 B。
  2. ② 证明 app 会保留:A 和 B 都在。
  3. ③ 证明 ate 与 app 的本质差别:ate 打开后虽然站在末尾,但 seekp 能把它拉回开头。换成 app 试试?(见下面的 30 秒实验)

六、30 秒实验

  1. 把 ③ 的 ios::ate 换成 ios::app,再跑 → 文件里还是 12345(XY 没写进去,或者写到了末尾)。因为 app 会把写位置强行拉回末尾 ——这就是 app 与 ate 的分水岭。
  2. 把 ① 里的 ios::trunc 删掉(只留 ios::out),再跑 → 结果一样(还是只剩 B)。因为 ofstream 的默认模式里就含 trunc(15.2.2)。这就是那个"看不见的 trunc"。
  3. 把 ① 里的 ios::out | ios::trunc 改成 ios::in | ios::out(用 fstream 对象) → A 和 B 会同时存在(因为 in 抑制了默认的清空)。体会"模式一变,命运就变"。

七、常见误区

  1. "ios::out 就是打开来写,不会动旧内容" ——会清空(ofstream 默认含 trunc)。要保留就必须显式 app。
  2. "app 和 ate 都表示末尾,随便用一个" ——差别很大:app 之后写永远在末尾,ate 之后还能 seek 到别处。
  3. "trunc 和 app 一起写没关系,反正都要末尾" ——不要一起写,行为由实现决定(标准没规定)。想要追加就只写 app。
  4. "写二进制文件只要内容是二进制就行,不用加 binary" ——必须加。不加就是文本模式,Windows 上 \n 会被自动变成 \r\n,文件就坏了(15.3.4)。
  5. "读文件也可以随便配 trunc" ——别配。ios::in | ios::trunc 是"打开就清空、然后再读" ——读一个已经被你清空的文件,这几乎从来不是你想要的。
  6. "打开方式可以运行时决定" ——可以(它是一个整数),但可读性会变差。如果非要动态决定,至少写清楚注释。
自测三问(点开对答案) 1. **`ios::app` 和 `ios::ate` 的差别是什么?** ——**`app`(append)**:**之后每次写都强制到文件末尾**,**不能 seek 到别处写**;**`ate`(at end)**:**只在打开时把位置移到末尾**,**之后 `seekp` 照样能挪到任何地方写**。**做日志用 `app`;要在末尾附近修改用 `ate`。** 2. **为什么"打开已有文件续写"必须显式写 `ios::app`?** ——**因为 `ofstream` 的默认打开方式里含 `trunc`**(清空)。**只写 `ofstream out("a.txt")` 会把原内容全清掉。** 3. **处理图片、`.exe`、结构体这类"给程序看的数据",打开方式必须加什么?为什么?** ——**必须加 `ios::binary`**。**否则是文本模式,Windows 上写 `\n` 会被替换成 `\r\n`、读的时候又变回来,字节数就乱了**——对文本无感,**对二进制数据是致命的**。

相关笔记