用文件流永远是五步:包含头文件 → 创建流对象 → 打开文件 → 像用 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"); |
常常会 | 流进入失败状态,不崩、还不报错 |
注意最后两行共有的那个特点:不崩、也不报错。
为什么会失败? 至少五种常见原因:
- 文件根本不存在(读的时候最常见);
- 路径写错了(尤其 Windows 的
\要写\\); - 没有权限(比如想写
C:\Windows\下面); - 文件被别的程序占用(比如用 Excel 打开了,或者你上一次运行没关);
- 目录不存在(
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"); // ③ 绝对路径(★ 反斜杠要写两个)
两个必须知道的点:
- Dev-C++ 里,"相对路径"的参照点是
.exe所在目录,不是.cpp所在目录。(默认它们在一起;但如果你改过输出目录,就会找不到文件。) - Windows 的路径分隔符是
\,而\在 C++ 字符串里是转义符,所以要写\\。或者正斜杠/在 Windows 上也能用:"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
读结果:
- ② 的输出证明写进去的换行确实被
getline按行切开了; - ③ 的三个值说明:打开失败时,流对象"变成假" ——
!bad是1(真),fail()是1,is_open()是0。 - 重点:③ 那一段没有任何报错、没有任何提示。如果你不主动打印这三个值,这段代码就是"安静地什么都没干"。
改一下再试:把 ③ 的 ifstream bad(...) 换成 ofstream bad(...) ——会发现它成功了(!bad 变成 0),而且文件夹里真的多出一个 no_such_file.txt。为什么?
因为写模式(
out)打开不存在的文件时,会顺手创建它;而读模式(in)不会。 ——这正是 15.2.3 要讲的"在打开文件的同时创建文件"。
五、30 秒实验
- 把两处
if (!out)/if (!in)检查删掉,再跑一次 → 程序照样跑完,只是什么都不输出。体会一下"安静失败"有多安静。 - 把
usage.txt改成sub/usage.txt(子目录不存在) → 打开失败。证明"会自动建文件,但不会自动建目录"。 - 写完后立刻用记事本打开
usage.txt→ 有内容(因为close()了)。然后把out.close();注释掉再试 → 多半也能看到(程序结束时自动刷新)。再试一次"写完立刻读"(不 close) → 可能读不到。 这就是缓冲(15.1 讲的)。
六、常见误区
- "文件打不开,程序会报错提醒我" ——不会。标准库选择安静失败,必须自己写
if (!in)。 - "写完文件就能立刻读回来" ——不一定。数据可能还在缓冲区。先
close()(或flush())。 - "
ofstream out("x.txt")打开一个已有文件没关系" ——有关系,而且是大事:ofstream的默认模式含trunc,会把文件清空。这是本章第一号"数据没了"事故。(15.2.1 细说) - "相对路径是相对于 .cpp 文件" ——是相对于
.exe(也就是程序运行时的"当前工作目录")。在 Dev-C++ 里两者通常在一起,所以看不出来;换个环境就会懵。 - "路径里的
\直接写就行" ——\在字符串里是转义符,"D:\new\test.txt"里的\n会被当成换行。要写"D:\\new\\test.txt"或"D:/new/test.txt"。 - "
close()可写可不写,反正程序结束会自动关" ——大多数时候确实会自动关(析构函数负责),但:① 写的数据可能还没刷;② 想换文件必须先关;③ 出异常时析构时机可能出乎意料。该关就关,是习惯问题。
自测三问(点开对答案)
1. **用文件流的五步是什么?** ——**① `#include- ☐ 下一节(15.1.3 ios类中的枚举常量)
相关笔记
- C++中的流类库 —— 上一节:流类库的族谱
- ios类中的枚举常量 —— 下一节:打开方式的那些常量是什么
- 数据的输入与输出 ——
cin失败会"罢工",那时就见过"状态" - 枚举类型的声明 —— 用 2 的幂做标志位(打开方式就是这个套路)
- 异常处理 —— "该用返回值还是异常"的判据在这里第一次实战
- 析构函数 —— 忘了
close()时,最后兜底的是它 - 🔷 C++基础 · 索引 —— 全课路线图