六个常量 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(差别在"怎么解释字节")
- 不给
binary= 文本模式:Windows 上写\n会自动变成\r\n,读的时候又会变回来; - 给了
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 |
数字怎么来的:
- ③ 的文件内容是
"12345\n"= 6 字节,所以打开后写位置在 6(末尾); - 用
seekp(0)回到开头,写"XY"覆盖了前两个字符 →"XY345\n"→ 读到XY345。
读结果——三句话:
- ① 证明
trunc每次都清空:第二次跑完,A不见了,只剩B。 - ② 证明
app会保留:A和B都在。 - ③ 证明
ate与app的本质差别:ate打开后虽然站在末尾,但seekp能把它拉回开头。换成app试试?(见下面的 30 秒实验)
六、30 秒实验
- 把 ③ 的
ios::ate换成ios::app,再跑 → 文件里还是12345(XY没写进去,或者写到了末尾)。因为app会把写位置强行拉回末尾 ——这就是app与ate的分水岭。 - 把 ① 里的
ios::trunc删掉(只留ios::out),再跑 → 结果一样(还是只剩B)。因为ofstream的默认模式里就含trunc(15.2.2)。这就是那个"看不见的trunc"。 - 把 ① 里的
ios::out | ios::trunc改成ios::in | ios::out(用fstream对象) →A和B会同时存在(因为in抑制了默认的清空)。体会"模式一变,命运就变"。
七、常见误区
- "
ios::out就是打开来写,不会动旧内容" ——会清空(ofstream默认含trunc)。要保留就必须显式app。 - "
app和ate都表示末尾,随便用一个" ——差别很大:app之后写永远在末尾,ate之后还能 seek 到别处。 - "
trunc和app一起写没关系,反正都要末尾" ——不要一起写,行为由实现决定(标准没规定)。想要追加就只写app。 - "写二进制文件只要内容是二进制就行,不用加
binary" ——必须加。不加就是文本模式,Windows 上\n会被自动变成\r\n,文件就坏了(15.3.4)。 - "读文件也可以随便配
trunc" ——别配。ios::in | ios::trunc是"打开就清空、然后再读" ——读一个已经被你清空的文件,这几乎从来不是你想要的。 - "打开方式可以运行时决定" ——可以(它是一个整数),但可读性会变差。如果非要动态决定,至少写清楚注释。
自测三问(点开对答案)
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`、读的时候又变回来,字节数就乱了**——对文本无感,**对二进制数据是致命的**。- ☐ 下一节(15.2.2 默认打开方式)
相关笔记
- 文件的打开 —— 上一节:
open的用法与检查 - 默认打开方式 —— 下一节:省略第二个参数时会发生什么
- ios类中的枚举常量 —— 这些常量的出处与"位标志位"原理
- 文件的追加 ——
app/ate的再展开 - 枚举类型的声明 —— 2 的幂做标志位
- 🔷 C++基础 · 索引 —— 全课路线图