写文本文件就是把 cout 换成文件流对象——<< 的用法一模一样,连 <iomanip> 那套格式控制都照用。
两个本节才需要注意的点:
- << 会自动把各种类型转成文字(int / double / string 都能直接写);
- endl 会刷盘、"\n" 不会 ——写大量数据时差别明显。
一句话概括:写文件是"输出"的搬家——语法不变,只是换了个出口。
15.3.1 打通了读写闭环,本节只管"写"这一半,讲透它:
- 2.5.3 的格式控制(setw / setprecision / fixed)在这里第二次上岗——那时写给屏幕,现在写给文件,一样有效;
- 15.2.2 的"ofstream 默认清空"在这里第一次变成实际影响(覆盖 vs 追加);
- 8.4 的结构体数组 在这里被"导出"到文件(实例04 就是导出一张成绩表)。
一句话:本节是"输出"这一侧的完整交代。
一、<< 能直接写各种类型
写文本文件时,<< 会先把数据"转成文字"再写出去:
ofstream out("a.txt");
int n = 42;
double d = 3.14;
string s = "hello";
char c = 'X';
bool b = true;
out << n << " " << d << " " << s << " " << c << " " << b << endl;
out.close();
文件内容(用记事本打开):
42 3.14 hello X 1
注意最后那个 1 ——bool 默认打成 1/0。想让它打 true/false,要加 boolalpha(15.1.3 的格式标志表里有它):
out << boolalpha << b << endl; // true
这就是"文本文件"的实质:任何类型都变成一串字符。"42" 是两个字符,不是那个 4 字节的 int。
二、格式控制:写得更"像样子"
<iomanip> 里的东西写文件时照样管用,因为它们改的是流本身的格式标志(15.1.3 讲过第三组常量)。
#include <iomanip>
out << left << setw(10) << "name" << setw(8) << "score" << endl; // 表头左对齐
out << fixed << setprecision(2) << 1.6 << endl; // 1.60(两位小数)
out << setfill('.') << setw(8) << 42 << endl; // .......42
三条必记的规矩(2.5.3 都讲过,这里复习):
| 操纵符 | 生效范围 | 说明 |
|---|---|---|
setw(n) |
只对"下一个"输出生效 | 每个值前面都要重写 |
setprecision(n) / fixed / left / right / setfill |
粘性,一直有效 | 改了就一直用,直到你改回来 |
left / right |
粘性 | setw 决定多宽,left/right 决定靠哪边 |
关于对齐的一个真相:表头和数据的"看起来齐不齐",取决于"列宽"和"对齐方式"是不是为它们分别设计的。 比如表头写
score(5 个字符)右对齐到 6 位,数据95(2 个字符)也右对齐到 6 位 ——右端是对齐的,但左端差很远。 所以"做表格"最省心的做法是:所有列统一左对齐、列宽给够。
三、endl 和 "\n" 的差别(写文件时很重要)
| 写法 | 做什么 | 代价 |
|---|---|---|
out << "\n"; |
只放一个换行符进缓冲区 | 小 |
out << endl; |
换行 + 立刻刷缓冲区(flush) | 大(每次都要真正访问磁盘) |
写几行看不出差别,写十万行就是天壤之别:
endl:十万次写盘;"\n":缓冲区满了才写盘(通常几百次)。
那什么时候必须用 endl?
需要"立刻让人/别的程序看到"的时候。 比如写日志——程序正在跑,你希望日志文件随时是最新的,那就用
endl(或者偶尔out.flush())。 纯粹攒数据(导出报表、批量写记录)→ 用"\n"。
另外注意一个平台差异:Windows 上文本模式写 \n,磁盘上实际存的是 \r\n(两个字节) ——这是文本模式的自动转换(15.3.4 详讲)。所以你数文件大小时,别按"字符个数"算。
四、实例04:向文本文件中写入数据
教材这道题要做的:把一组数据(成绩表)写进文本文件,并演示覆盖与追加两种方式。
#include <iostream>
#include <fstream>
#include <iomanip>
#include <string>
using namespace std;
struct Student { string name; int score; double height; };
int main() {
Student cls[3] = {
{"Ada", 95, 1.62},
{"Bob", 88, 1.78},
{"Cid", 72, 1.71}
};
// ══ ① 朴素写法:直接用空格分隔(最容易再读回来)══
{
ofstream out("stu_plain.txt");
if (!out) { cerr << "写不了 stu_plain.txt" << endl; return 1; }
for (int i = 0; i < 3; i++)
out << cls[i].name << " " << cls[i].score << " " << cls[i].height << "\n";
out.close();
}
// ══ ② 带格式:排成一张能看的表 ══════════════
{
ofstream out("stu_table.txt");
if (!out) { cerr << "写不了 stu_table.txt" << endl; return 1; }
out << left << setw(10) << "name" << setw(8) << "score" << "height" << "\n";
out << string(24, '-') << "\n";
for (int i = 0; i < 3; i++) {
out << left << setw(10) << cls[i].name
<< setw(8) << cls[i].score
<< fixed << setprecision(2) << cls[i].height << "\n";
}
out.close();
}
// ══ ③ 追加一行(不清空已有内容)════════════
{
ofstream out("stu_plain.txt", ios::app); // ★ app 是关键
if (!out) { cerr << "追加失败" << endl; return 1; }
out << "Dee 60 1.68" << "\n";
out.close();
cout << "③ 已向 stu_plain.txt 追加一行 Dee" << endl;
}
// ══ ④ 把两个文件都打出来看看 ═══════════════
const char *files[2] = {"stu_plain.txt", "stu_table.txt"};
for (int f = 0; f < 2; f++) {
cout << "── " << files[f] << " ──" << endl;
ifstream in(files[f]);
string line;
while (getline(in, line)) cout << line << endl;
in.close();
}
return 0;
}
看到什么算成功:
③ 已向 stu_plain.txt 追加一行 Dee
── stu_plain.txt ──
Ada 95 1.62
Bob 88 1.78
Cid 72 1.71
Dee 60 1.68
── stu_table.txt ──
name score height
------------------------
Ada 95 1.62
Bob 88 1.78
Cid 72 1.71
数字怎么来的:分隔线是 string(24, '-'),24 = 10 + 8 + 6(三列的宽度加起来,最后一列是 "height" 的 6 个字符)。
读结果:
- ① 和 ② 的内容完全一样,只是"排版不同" ——数据还是那些数据。这说明格式控制只是"好看",不影响内容。
- ③ 说明
ios::app真的没清空:stu_plain.txt里前 3 行还在,Dee接在第 4 行。如果把ios::app删掉,再跑一次 → 前 3 行会被清掉、只剩Dee(15.2.2 的默认trunc)。 - ① 用的是
"\n"、③ 用的是"\n"——没别的意思,就是想让你看到"不写endl也能换行"。
两道顺手的练习题:
- 练习 A:把 ① 的
" "换成","→ 得到的就是一个 CSV 文件,Excel 双击能直接打开。 - 练习 B:把 ② 的
fixed << setprecision(2)删掉 → 身高变成1.62/1.78/1.71(本来就只有两位,看不出差别)。换成1.6再试 → 不写fixed会输出1.6,写了会输出1.60。
五、30 秒实验
- 把 ③ 的
ios::app删掉,连跑两次 → 第一次跑完有 4 行,第二次跑完只剩 1 行(Dee)。这就是"覆盖 vs 追加"的现场对照。 - 在 ① 的循环里把
"\n"改成endl,把3改成300000(三十万行),掐表比较 →endl明显更慢。 这就是"刷盘"的代价。 - 在 ② 里把
left删掉 → 表头和数据都变成右对齐,看起来更"散"。证明left/right是粘性的、影响所有后续输出。 - 把 ① 写出的
stu_plain.txt用 Excel 打开 → 一列里全是Ada 95 1.62(因为它不认识空格分隔)。把分隔符换成逗号再试 → 自动分成三列。这就是 CSV 的由来。
六、常见误区
- "写文件要用
write,<<是给屏幕用的" ——<<写文本文件完全正确。write是二进制路(15.3.4)。 - "每行结尾必须写
endl" ——不是必须的。"\n"一样换行,而且快得多。只在需要"立刻落地"时才用endl。 - "
bool写进文件是true/false" ——默认是1/0,要boolalpha才是文字。 - "格式控制是
cout专有的" ——不是,<iomanip>对任何ostream都有效,包括文件流和字符串流。 - "
setw(10)设一次,后面都管用" ——setw只对下一个输出生效(它是唯一"非粘性"的)。其他格式标志都是粘性的。 - "用
<<写文件不用担心换行符" ——在文本模式下不用担心(系统会转换),但在二进制模式下不会转换,这一点在 15.3.4 会变成关键差别。
自测三问(点开对答案)
1. **写文本文件时,`endl` 和 `"\n"` 有什么差别?什么时候该用哪个?** ——**`endl` 是"换行 + 立刻刷缓冲区"**(每次写盘),**`"\n"` 只是放个换行符**。**要实时可见(如日志)用 `endl`;批量写数据用 `"\n"`(快得多)。** 2. **`setw` 和其他格式操纵符(`fixed` / `setprecision` / `left`)的生效范围有何不同?** ——**`setw` 只对"下一个"输出生效,每次都要重写**;**其他几个是粘性的**,设了就一直有效,直到被改。 3. **把一个成绩表"追加"到一个已有文本文件里,关键要写什么?不写会怎样?** ——**关键是在打开时写 `ios::app`**。**不写的话用的是 `ofstream` 的默认模式(含 `trunc`),会把原文件清空、只剩新写的内容。**- ☐ 下一节(15.3.3 读取文本文件)
相关笔记
- 文件流 —— 上一节:读写闭环(含实例03)
- 读取文本文件 —— 下一节:把这文件读回来(含实例05)
- 流输出格式控制 ——
setw/fixed/setprecision的出处 - ios类中的枚举常量 ——
boolalpha等格式标志 - 默认打开方式 —— 为什么追加必须写
app - 结构体数组 —— 实例04 导出的就是它
- 🔷 C++基础 · 索引 —— 全课路线图