省略 open() 的第二个参数时,每个类都有自己的默认值,而且它们不一样:
- ifstream → ios::in(读,且要求文件已存在)
- ofstream → ios::out | ios::trunc(写,且会清空!)
- fstream → ios::in | ios::out(读写,不清空,但也不建文件)
"默认"这两个字背后藏着本章最大的坑:ofstream 的默认会清空文件;fstream 的默认不会创建文件。
一句话概括:默认值是"经过设计的选择",不是"随便定的"——搞清楚它,才能预判你那一行代码到底做了什么。
15.2.1 讲了六个常量怎么选,本节回答"不选的时候会怎样":
- 本节是全章的"避坑说明书":新手 90% 的"文件内容不见了 / 文件没建出来",根源都在这一节;
- 它把 15.2.1 的决策表补完整:你想省事不写模式,就得先知道省略后会是什么;
- 15.2.3 紧接着讲"什么时候会自动创建文件"——和本节的 fstream 那条正好接上。
一句话:写文件操作之前,先问自己一句"默认模式是什么"。
一、三个类的默认值(背下来也不冤)
| 类 | 默认打开方式 | 意味什么 |
|---|---|---|
ifstream |
ios::in |
只读;文件不存在 → 失败(不创建) |
ofstream |
ios::out \| ios::trunc |
只写;文件不存在 → 创建;文件已存在 → 清空! |
fstream |
ios::in \| ios::out |
可读可写;不清空;文件不存在 → 失败(不创建) |
怎么看这张表最省事:每个类的默认值,就是"这个类最常用来干的事"。
ifstream存在的意义是读 → 默认in;ofstream存在的意义是写 → 默认out;而"写"在大多数场景下是"生成一份新内容",所以顺手配了trunc——这个"顺手"就是事故的来源;fstream存在的意义是改文件 → 默认既读又写、但不动已有内容(要"改"就得先有内容)。
所以 ofstream 的 trunc 不是设计缺陷,是"以'生成文件'为默认场景"的产物。 问题是:当你其实想追加时,它不会问你的意见。
二、默认值带来的三个具体后果
后果一:ofstream 会清空已有文件(最大)
ofstream out("data.txt"); // 等价于 ofstream out("data.txt", ios::out | ios::trunc);
哪怕你后面一个字都没写,只要这一行执行了,文件就空了。
想保留内容,必须显式改:
ofstream out("data.txt", ios::app); // 追加
ofstream out("data.txt", ios::out | ios::app); // 同样意思,写全更清楚
后果二:ifstream / fstream 不会创建文件
ifstream in("maybe.txt"); // 文件不存在 → fail(),不创建
fstream db("maybe.txt"); // 同上(因为默认含 in)
为什么? 因为"读"的前提是"有东西可读" ——打开一个不存在的文件来读,本来就该失败。
所以"读模式不建文件",是合理的;"写模式建文件",也是合理的。
后果三:想用 fstream 读写一个"可能不存在的文件",会比较绕
fstream 默认(in | out)要求文件已经存在。如果文件可能不存在,最省事的做法是"先确保它存在":
// 先"碰"一下文件:用 app 方式打开(不会清空已有内容,也不写任何东西)
{ ofstream touch("data.txt", ios::app); }
// 现在它一定存在了,可以按默认方式读写
fstream db("data.txt", ios::in | ios::out);
这三行的意思是:app 模式既能创建(文件不存在时)、又不会清空(文件存在时) ——所以它是"温和地确保文件存在"的最佳工具。
这就是为什么日志文件永远用
app:它既保证文件在、又不动旧内容。
三、动手 Demo:把三个默认值验证一遍
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
// 判断一个文件是不是空的
bool isEmpty(const string &fname) {
ifstream in(fname);
string line;
return !getline(in, line); // 一行都读不到 = 空
}
int main() {
// ── ① ifstream 默认:in(要求文件已存在)────
{ ofstream o("exist.txt"); o << "old content" << endl; } // 备好一个文件
ifstream i1("exist.txt");
cout << "① ifstream 开存在的文件 : " << (!i1 ? "失败" : "成功") << endl;
i1.close();
ifstream i2("never_created_by_in.txt");
cout << "① ifstream 开不存在的文件: " << (!i2 ? "失败" : "成功")
<< "(in 不创建文件)" << endl;
i2.close();
// ── ② ofstream 默认:out|trunc(会清空)─────
{ ofstream o("exist.txt"); o << "old content" << endl; } // 重新装回内容
cout << "② 重开之前 exist.txt 是空的吗: " << (isEmpty("exist.txt") ? "是" : "不是") << endl;
ofstream o1("exist.txt"); // ★ 默认打开(什么也不写)
o1.close();
cout << "② 用默认 ofstream 重开之后 : "
<< (isEmpty("exist.txt") ? "已经被清空!" : "内容还在") << endl;
ofstream o2("brand_new.txt"); // 不存在的文件
cout << "② ofstream 开不存在的文件 : " << (!o2 ? "失败" : "成功,并创建了它") << endl;
o2.close();
// ── ③ fstream 默认:in|out(不清空、不创建)──
fstream f1("exist.txt");
cout << "③ fstream 开存在的文件 : " << (!f1 ? "失败" : "成功(且不清空)") << endl;
f1.close();
fstream f2("never_created_by_fstream.txt");
cout << "③ fstream 开不存在的文件 : " << (!f2 ? "失败" : "成功")
<< "(默认含 in,所以不创建)" << endl;
f2.close();
return 0;
}
看到什么算成功:
① ifstream 开存在的文件 : 成功
① ifstream 开不存在的文件: 失败(in 不创建文件)
② 重开之前 exist.txt 是空的吗: 不是
② 用默认 ofstream 重开之后 : 已经被清空!
② ofstream 开不存在的文件 : 成功,并创建了它
③ fstream 开存在的文件 : 成功(且不清空)
③ fstream 开不存在的文件 : 失败(默认含 in,所以不创建)
读结果——三条结论,一一对应上面三个后果:
- ① 最后一行失败:
ifstream默认只读不建。 - ② 中间那行最关键:
ofstream o1("exist.txt");什么都没写,文件却空了 ——这就是默认trunc的实锤。 - ③ 最后一行失败:
fstream默认含in,所以它也不会创建文件。
四、30 秒实验
- 把 ② 里的
ofstream o1("exist.txt");改成ofstream o1("exist.txt", ios::app);→ 内容还在。默认值一变,命运就变了。 - 把 ③ 第一段改成
fstream f1("exist.txt", ios::in | ios::out | ios::trunc);→ 文件被清空。证明"想清空得明说"。 - 在
ofstream o2("brand_new.txt");那行之后,去文件夹里看一眼 →brand_new.txt真的出现了(0 字节)。证明"写模式打开不存在的文件=创建它"(并且它现在是空的,因为还没写东西)。
五、常见误区
- "三个类的默认模式一样" ——不一样,而且差别很大:
in/out|trunc/in|out。 - "
ofstream只是打开来写,不会动旧内容" ——默认会清空(含trunc)。这是本章最容易造成数据丢失的一条。 - "
fstream是万能的,读写文件都用它" ——它默认既不创建也不清空,在"生成新文件"的场景下反而更麻烦。按用途选类:只读用ifstream、只写用ofstream、要改已有文件才用fstream。 - "
fstream打开不存在的文件会自动创建" ——不会(默认含in)。要么先"温和地建一个"(ofstream+app开一下),要么显式加out。 - "默认值那么坑,那我每次都把六个常量全写一遍算了" ——没必要。只在你真正在意的地方写清楚:写文件时写清
app还是trunc;fstream时写清in|out。 其他情况默认值就是对的。
自测三问(点开对答案)
1. **`ifstream` / `ofstream` / `fstream` 的默认打开方式分别是什么?** ——**`ios::in`** / **`ios::out \| ios::trunc`** / **`ios::in \| ios::out`**。 2. **为什么 `ofstream out("a.txt");` 会把 `a.txt` 的内容清掉?** ——**因为 `ofstream` 的默认模式里含 `trunc`(截断)**。**想保留内容必须显式写 `ios::app`。** 3. **文件可能不存在,但你想用 `fstream` 读写它,稳妥的做法是什么?** ——**先用 `ofstream touch("a.txt", ios::app);` 温和地确保文件存在**(`app` 既能创建、又不清空),**然后再 `fstream db("a.txt", ios::in | ios::out);`**。- ☐ 下一节(15.2.3 在打开文件的同时创建文件)
相关笔记
- 打开方式 —— 上一节:六个常量怎么选
- 在打开文件的同时创建文件 —— 下一节:"什么时候会自动建文件"(含实例02)
- 文件的打开 ——
open的基本用法 - 文件的追加 —— 为什么日志永远用
app - 🔷 C++基础 · 索引 —— 全课路线图