ios 类里定义了一堆枚举常量,它们是流世界的"开关词汇表"。三大类:
- ios::in / out / app / ate / trunc / binary ——怎么打开(打开方式);
- ios::beg / cur / end ——从哪儿开始数(定位基准);
- ios::dec / hex / fixed / left … ——怎么写好看(格式标志)。
用法的关键在一句话:这些常量是"2 的幂"的标志位,可以拿 | 拼起来 ——所以"追加着写、还是二进制"这种组合意图,一个参数就能表达。
一句话概括:这些常量不是新语法,是"把一串意图打包成一个整数"的标准做法(8.6.1 讲过的老手艺)。
本节是补工具,为 15.2 起的所有打开操作做铺垫:
- 回看 8.6.1:那里讲过"用 2 的幂做可叠加的标志位"(OK=1, CREATED=2, ACCEPTED=4)。打开方式用的就是同一个套路——in、out、app 各占一个二进制位,所以能 |。
- 回看 11.2:| 能在这儿使,是因为标准库重载了 operator|(11.2.2 讲的"运算符就是函数")。
- 回看 2.5.3:那里用过的 setprecision / setw 是 <iomanip> 的操纵符,它们底层改的就是这里第三组"格式标志"。
一句话:本节把"打开文件时那几个看不懂的词"一次性讲清。
一、三组常量,一张总表
它们都挂在 ios 类里(15.1.1 讲过:ios 是所有流的公共祖先),所以用的时候要带上 ios:: 这个前缀。
| 组 | 代表常量 | 管什么 | 用在哪 |
|---|---|---|---|
| 打开方式 | in out app ate trunc binary |
文件怎么开 | open() / 构造函数 |
| 定位基准 | beg cur end |
seekg/seekp 从哪儿数 |
15.4.4 |
| 格式标志 | dec hex oct left right fixed scientific boolalpha showpoint … |
数据怎么写出来 | setf() 或 <iomanip> |
为什么前缀是 ios:: 而不是 ifstream::?
因为它们在树根那一层定义 ——所有流共享。所以
ios::in写在ifstream的参数里,也写在fstream的参数里,不用换前缀。
二、第一组:打开方式(openmode)—— 本章最常用
| 常量 | 含义 | 记法 |
|---|---|---|
ios::in |
为读取而打开 | in = 进 |
ios::out |
为写入而打开 | out = 出 |
ios::app |
追加(append):每次写都写到文件末尾 | app = append |
ios::ate |
打开后立刻把位置移到末尾(at end),之后还可以自由挪 | ate = at end |
ios::trunc |
截断(truncate):文件存在就把内容清空 | trunc = truncate |
ios::binary |
二进制模式(不做换行符转换) | 15.3.4 细讲 |
怎么用:用 | 把需要的拼起来(或者更好写:|=)。
ifstream in("a.txt", ios::in); // 只读
ofstream out("a.txt", ios::out); // 只写(会清空)
ofstream log("a.txt", ios::out | ios::app); // 追加写
fstream db("a.txt", ios::in | ios::out); // 可读可写、不清空
ifstream bin("a.dat", ios::in | ios::binary); // 二进制读
ofstream out("important.txt"); // 只要这一行跑过去,文件就空了
因为 ofstream 的默认模式里含 trunc(15.2.2 会列出来)。
想"接着写"必须显式加 app: ofstream out("important.txt", ios::app);
这一条每年都会让无数人丢掉数据。 记住它。
三、第二组:定位基准(seekdir)—— 15.4 用
| 常量 | 值 | 意思 |
|---|---|---|
ios::beg |
0 | 从文件开头算(beginning) |
ios::cur |
1 | 从当前位置算(current) |
ios::end |
2 | 从文件末尾算(end) |
典型用法(记住这两个就够):
in.seekg(0, ios::beg); // 回到开头(等价于 in.seekg(0);)
in.seekg(0, ios::end); // 挪到末尾
long size = in.tellg(); // 读位置 = 文件总字节数
"从末尾往前数"要用负数:
in.seekg(-5, ios::end);表示"末尾往前 5 个字节"。
四、第三组:格式标志(fmtflags)—— 和 2.5.3 接上
这一组管的是"数字怎么写出来"。日常你更常用 <iomanip> 里的操纵符,但两者是一回事:
| 标志 | 效果 | <iomanip> 里对应的操纵符 |
|---|---|---|
ios::dec / hex / oct |
十进制 / 十六进制 / 八进制 | dec / hex / oct |
ios::fixed |
定点小数 | fixed |
ios::scientific |
科学计数法 | scientific |
ios::left / right |
左 / 右对齐 | left / right |
ios::showpoint |
强制显示小数点 | showpoint |
ios::boolalpha |
bool 打 true/false |
boolalpha |
ios::skipws |
跳过输入前导空白(默认开) | skipws |
ios::showbase |
十六进制带 0x 前缀 |
showbase |
两种改法:
// 写法一:用 setf() 直接改标志位(老教材风格)
cout.setf(ios::hex, ios::basefield); // 切到十六进制
cout.setf(ios::showbase); // 加 0x 前缀
cout << 255 << endl; // 0xff
// 写法二:用操纵符(更常见、更短)
#include <iomanip>
cout << hex << showbase << 255 << endl; // 0xff
为什么 setf 那行要写两个参数? 因为 hex/dec/oct 是一组互斥的选项,要先把这一组清干净再设新值(只写一个参数就是"叠加",会得到奇怪的组合)。这也是为什么"用操纵符更省心"。
对你的实际影响:日常用操纵符;看到
setf认得就行。
五、为什么能"拼起来":位标志位的原理
这是 8.6.1 那个老手艺的原样重演。
六个打开方式,各占一个二进制位(值不重要,只要不重叠):
ios::in = 0000 1000
ios::out = 0001 0000
ios::trunc = 0010 0000
ios::app = 0100 0000
ios::binary = 1000 0000
ios::in | ios::binary = 1000 1000 ← 两个位同时为 1:意思是"两个都要"
所以 | 不是"加减",是"把两个指令同时挂上"。
顺带解释一件事:ios::in | ios::out 这种写法能编译过,是因为标准库为这些标志位类型重载了 operator| 和 operator|=(11.2)。如果它是普通的 enum,| 会报类型错 ——这也是为什么较新的代码里你会看到 ios::openmode(ios::in | ios::binary) 这种别扭的强转(C++ 老枚举的问题,8.6.1 提过 enum class 更安全,但会丢掉这种"自动当整数用"的方便)。
六、动手 Demo:把三组常量都摸一遍
#include <iostream>
#include <fstream>
#include <iomanip>
using namespace std;
int main() {
// ── ① 打开方式:先用 out|trunc 清空写,再用 app 追加 ──
ofstream o1("modes.txt", ios::out | ios::trunc); // 清空重写
if (!o1) { cerr << "打开失败" << endl; return 1; }
o1 << "first run" << endl;
o1.close();
ofstream o2("modes.txt", ios::out | ios::app); // 追加
if (!o2) { cerr << "打开失败" << endl; return 1; }
o2 << "second run" << endl;
o2.close();
// ── ② 定位基准:量一下文件多大 ──
ifstream in("modes.txt", ios::in);
in.seekg(0, ios::end); // 挪到末尾
long size = in.tellg(); // 读位置 = 大小
cout << "② 文件大小: " << size << " 字节" << endl;
in.seekg(0, ios::beg); // 回到开头
cout << "② 内容:" << endl;
string line;
while (getline(in, line)) cout << " " << line << endl;
in.close();
// ── ③ 格式标志:同一个数,三种写法 ──
cout << "③ 默认 : " << 255 << endl;
cout << "③ 十六进制: " << hex << showbase << 255 << dec << endl;
cout << "③ 八进制 : " << oct << 255 << dec << endl;
return 0;
}
看到什么算成功:
② 文件大小: 21 字节
② 内容:
first run
second run
③ 默认 : 255
③ 十六进制: 0xff
③ 八进制 : 377
数字怎么来的(都验算过):
- 文件内容两行:
"first run\n"是 10 字节(first空格run换行 = 9 + 1 = 10),"second run\n"是 11 字节(6 字母 + 1 空格 + 3 字母 + 1 换行 = 11),合起来 21。 0xff= 15×16 + 15 = 255;377(八进制)= 3×64 + 7×8 + 7 = 192 + 56 + 7 = 255。
读结果:
- ① 证明同一份文件,
trunc让它清空重写、app让它往后接 ——两个常量,两种截然不同的命运。 - ② 是"求文件大小"的经典写法,请记住这三行(
seekg到末尾 →tellg→seekg回来)。 - ③ 证明格式标志一设就生效,而且会一直生效(所以我在每行末尾都写
dec把它切回来——不切回来,后面的数字全都是十六进制)。
七、30 秒实验
- 把 ① 里的
ios::app改成ios::out(去掉 app),连跑两次程序 → 第二次跑完,文件里只剩first run。 亲眼看到trunc的威力。(这也是本章最需要"亲眼见一次"的坑。) - 把
in.seekg(0, ios::beg);那一行注释掉 → 内容一行都读不出来(因为读位置还停在末尾)。证明"读位置"是真的存在、真的会卡人。 - 把 ③ 第一行末尾也加上
dec,再跑 → 三个数字都是十进制。证明格式标志是"粘性"的。
八、常见误区
- "常量名前缀该写
ifstream::" ——写ios::。它们在树根定义,所有流共享(15.1.1)。ios::app在ofstream和fstream上都通用。 - "
ios::app和ios::ate差不多" ——差得挺远:app是"以后每次写都强制到末尾";ate只是"打开时先挪到末尾一次",之后你想 seek 到别处照样能写。(15.4.2 会专门对照) - "打开方式只能写一个" ——可以
|组合,因为它们是比特标志位(8.6.1 的套路)。 - "
ios::out打开文件不会清空" ——会清(因为ofstream的默认模式含trunc)。要"只写不清"得显式管好模式。 - "格式标志设一次就行了" ——它是粘性的(sticky):一旦设了
hex,后面所有数字都是十六进制,记得切回dec。 - "这些常量就是普通的
int,能随便加" ——概念上不是整数,是"比特标志位"。用|不用+(+遇到相同位会进位,把两个标志变成第三个意思)。
自测三问(点开对答案)
1. **`ios::app` 和 `ios::ate` 分别是什么意思?** ——**`app`(append)**:**每次写入都到文件末尾**;**`ate`(at end)**:**打开时把位置移到末尾一次,之后可以自由挪**。**想做日志就 `app`;想"从末尾开始但还能回头改"就 `ate`。** 2. **为什么这些常量能用 `|` 拼起来?** ——**它们是 2 的幂(各占一个二进制位)的标志位**(8.6.1 的老手艺),**`|` 是把多个标志"同时挂上"**;而且**标准库重载了 `operator|`**,所以 `ios::in | ios::binary` 才能编过(11.2)。 3. **`cout << 255` 之前写了 `cout << hex;`,之后所有输出都会变成十六进制吗?** ——**会。格式标志是粘性的**,**一直有效到你再改它**。**所以用完记得 `dec` 切回来。**- ☐ 下一节(15.1.4 流的输入/输出)