第三章 C++基本输入/输出
一、标准输入/输出
1.1 流是什么
流可以想成一条传送带。数据一件件放上去,从这头送到那头。程序不用关心传送带另一头接的是键盘、屏幕还是文件,拿到手里都是同一套读法。这也是这一章两节能放在一起学的原因,标准输入输出和文件输入输出,说到底是一条传送带的两段。
┌─────────────────────────────────────────────────────────┐ │ 输入输出四个现成对象 │ ├─────────────────────────────────────────────────────────┤ │ 程序不直接碰键盘和屏幕,中间隔着一层 │ │ │ │ +------------+------------+------------+------------+ │ │ |对象 |方向 |接哪 |缓冲 | │ │ +------------+------------+------------+------------+ │ │ |cin |往程序流 |键盘 |有 | │ │ |cout |往屏幕流 |屏幕 |有 | │ │ |cerr |往屏幕流 |屏幕 |无 | │ │ |clog |往屏幕流 |屏幕 |有 | │ │ +------------+------------+------------+------------+ │ │ │ │ 四个都是标准库给好的,include iostream 就能用 │ │ cerr 不攒货,崩溃前的最后几句话交给它 │ └─────────────────────────────────────────────────────────┘四个对象都在 std 里住着,不用创建,拿来就用。前两章一直 using namespace std 图省事,工程里建议写清楚 std::,理由和第二章说 const 一样,写得显眼,搜得到。
| 对象 | 方向 | 默认接哪 | 缓冲 |
|---|---|---|---|
| cin | 流进程序 | 键盘 | 有 |
| cout | 流出程序 | 屏幕 | 有 |
| cerr | 流出程序 | 屏幕 | 无 |
| clog | 流出程序 | 屏幕 | 有 |
cerr 和 cout 都接屏幕,差别在缓冲。cout 攒着送,cerr 立刻走,这个差别 1.3 节专门说。
1.2 cout 与链式输出
<< 叫插入运算符,把右边的东西递进左边的流。第一章见过它的面,这里补一句,为什么它能一路接下去。
┌──────────────────────────────────────────────────────────┐ │ 链式输出怎么接起来 │ ├──────────────────────────────────────────────────────────┤ │ cout << a << b 是一轮轮接起来的 │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ cout << a 把 a 递出去,cout 把自己还回来 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ v │ │ ┌────────────────────────────────────────────────────┐ │ │ │ cout << b 拿到的还是 cout,接着往后递 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ v │ │ ┌────────────────────────────────────────────────────┐ │ │ │ cout << endl 一路递到缓冲区,等刷新 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ 每个 << 只干一件事,递出去,再把自己还回来 │ │ 所以能写成一条长龙,从左往右读就行 │ └──────────────────────────────────────────────────────────┘关键在于还回来的还是 cout 自己,所以右边能接着接。一条输出写多长都行,读的时候从左往右,跟读句子一样。
cout << "学号 " << id << " 姓名 " << name << "\n";字符串、变量、常量混着排,编译器按类型各自找递法。有个细节,相邻的两个字符串会并成一条,"abc" "def"编译出来就是"abcdef",长字符串折行写靠它。
cout << "第一行内容" << "接在上一行后面拼" << "\n";单引号包字符,双引号包字符串,第二章讲过,这儿只提个醒。cout << 'A'送出去一个字母,cout << "A"送出去一个字母加一个结尾标记,后者在文件里是多出一个字节的。
1.3 endl 和 \n 的区别
endl 这个名字听着像换行,它其实干两件事。不少人只把它当换行用,白刷了性能还不知道。
┌──────────────────────────────────────────────────────────┐ │ 缓冲与刷新 │ ├──────────────────────────────────────────────────────────┤ │ cout 递出去的东西先攒着,不立刻上屏 │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ cout << 字 内容进流的缓冲区 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ v │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 缓冲区 攒着,等一个刷新的时机 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ v │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 屏幕 刷新那一刻才看见 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ endl 是换行加刷新两个字,\n 只管换行 │ │ 循环里刷几万行,endl 能把速度拖慢一截 │ │ 程序正常走完会自己刷,崩溃才丢最后几行 │ └──────────────────────────────────────────────────────────┘缓冲存在的理由很简单,屏幕慢。程序跑一次能吐几万行,行行都等屏幕确认,一天什么都干不完。先攒在缓冲区,攒够了或者时机到了,一次性倒过去。
必须刷新的场合有三种。交互提示语,问完话得让人看见,不然对方对着黑框框干等。崩溃前的最后遗言,这时候不刷,话就跟着程序一起没了。多个线程同时往终端写,不刷容易串行。
别用的场合就一种,大循环。写日志、刷表格、批量输出,用\n,攒着一路倒完。cout 在程序正常结束时会自己把剩下的刷掉,结尾不用操心。真正会丢的只有崩溃那一下,所以崩溃现场的遗言都交给 cerr,它不攒货,说了就走。
for (int i = 0; i < 100000; ++i) { cout << "第 " << i << " 行\n"; // 循环里用这个 } cerr << "配置文件读不到,先用默认值\n"; // 这个立刻走重定向可以把两路分开收。跑程序的时候在命令行后面加 > 和 2>,一路输出进一个文件,一路错误进另一个。
./tool > result.txt 2> error.txt写个同时往两路输出的程序,跑一遍重定向,两个文件里的内容对得上号,这事就算记住了。clog 也是往屏幕去,有缓冲,定位是日志,日常用得少,知道有它就行。
1.4 cin >> 的规矩
叫提取运算符,从流里按类型取东西。int 变量只认数字,double 认小数点,string 认到空白为止。取之前先把前导空白吃掉,空格、tab、回车都算。
┌──────────────────────────────────────────────────────────┐ │ >> 怎么跳过空白 │ ├──────────────────────────────────────────────────────────┤ │ cin >> 伸手之前,先把前面的空白吃掉 │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 缓冲区 躺着空格 1 空格 2 换行 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ v │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 第一次 >> 跳过空白,拿走 1,停住 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ v │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 第二次 >> 空白和换行都拦不住,拿走 2 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ 所以 cin >> a >> b 分两行敲,一样读得进去 │ │ 代价是读字符串遇到空格就停,复姓只录半个 │ └──────────────────────────────────────────────────────────┘回车被当空白跳掉,所以怎么换行敲都读得进去。方便是真方便,雷也埋在这儿,读字符串遇到空格就停,开篇那个欧阳 修 正撞上。
读个数不确定的一批数字,拿流本身当循环条件,读到文件尾自动停。
int x; int sum = 0; int cnt = 0; while (cin >> x) { sum += x; ++cnt; } cout << "共 " << cnt << " 个数,和是 " << sum << "\n";命令行里敲 Ctrl+Z 再回车,Linux 和 macOS 是 Ctrl+D,告诉程序输入到头了。这个写法以后写实战反复用,现在先混个脸熟。
1.5 getline 读一整行
的短板在空格,一整行文本它读不动。这种场合交给 getline,自由函数,住在 string 头文件里,读到换行为止,换行符吃掉,行内的空格全留下。
string line; cout << "说点什么: "; getline(cin, line); cout << "你说了 " << line.size() << " 个字符\n";三个参数的版本能换分隔符,读分号分隔的数据正好用。
string field; getline(cin, field, ';');和 getline 分工明确,数字和单个词找 >>,一行文本找 getline。麻烦出在前后脚用的场合,前面 >> 留下的换行没人管,getline 上来就捡走,当成一整行。
┌──────────────────────────────────────────────────────────┐ │ 换行被谁捡走了 │ ├──────────────────────────────────────────────────────────┤ │ >> 和 getline 轮流上岗,中间要清场 │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 键盘 敲下 25 回车 欧阳 修 回车 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ v │ │ ┌────────────────────────────────────────────────────┐ │ │ │ cin >> age 拿走 25,回车留在原地 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ v │ │ ┌────────────────────────────────────────────────────┐ │ │ │ getline 看见回车,捡走就当一整行 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ v │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 结果 姓名是空的,修 字没人管 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ 残留的换行,一句 ignore 捡走 │ │ 拿不准就统一用一种,别混着来 │ └──────────────────────────────────────────────────────────┘开篇那个程序就这么修,在 >> 之后补一句。
cin >> id; cin.ignore(); // 把 >> 留下的换行丢掉 getline(cin, name); cout << id << "\t" << name << endl;ignore() 默认只丢一个字符,正好是换行。想丢到行尾,写cin.ignore(10000, '\n'),稳妥一点用numeric_limits<streamsize>::max(),那个数大到不可能先读完,limits 头文件要给面子。嫌这两行烦还有一条路,全程 getline,拿到的字符串自己转数字,那是另一个话题,这里不展开。
四种输入方式放一张表里对照。
| 方式 | 读到哪停 | 留不留空白 | 场合 |
|---|---|---|---|
| cin >> x | 空白或类型不符 | 丢 | 数字、单个词 |
| getline(cin, s) | 换行 | 留 | 一行文本 |
| cin.getline(buf, n) | 换行 | 留 | 字符数组,老代码 |
| cin.get(ch) | 一个字符 | 留 | 逐字符处理 |
后两种是给字符数组准备的,装不下会溢出,初学用前面两个就够。知道有它们,是以后读老代码不慌。
1.6 输入失败了怎么办
给 int 喂一个字母,程序不报错,它静静地把 failbit 置上,然后后面所有的读取直接失败,cin 变成一座雕像。
int n; cin >> n; cout << "你输入了 " << n << "\n"; // 输入 abc,这里打印 0 cout << "再读一个: "; cin >> n; // 这行直接跳过,问话都看不到雕像的成因在流的状态位上。failbit 管类型对不上,eofbit 管读到尾,badbit 管流本身坏掉。状态不干净,流就一直装死。
┌────────────────────────────────────────────────────────┐ │ 流的状态位 │ ├────────────────────────────────────────────────────────┤ │ 流自己带着状态位,读挂了看得见 │ │ │ │ +------------+------------------------+------------+ │ │ |状态位 |什么时候置位 |后果 | │ │ +------------+------------------------+------------+ │ │ |failbit |字母喂给 int |之后全失败 | │ │ |eofbit |读到尾还接着读 |读到空内容 | │ │ |badbit |流本身出了故障 |基本没救 | │ │ +------------+------------------------+------------+ │ │ │ │ 修复三板斧,clear 清状态,ignore 倒垃圾,重读 │ │ 循环判断别用 eof,最后一行会被处理两遍 │ └────────────────────────────────────────────────────────┘救活它有三板斧,代码里见分晓。
int n; while (!(cin >> n)) { cin.clear(); cin.ignore(10000, '\n'); cout << "要一个整数,请重输: "; } cout << "这次对了,是 " << n << "\n";这个循环是控制台程序的标准件,输错就重问,直到给对为止。if (cin >> n)也能探状态,流在条件里会把自己变成真假,干净是真,脏了是假。
eofbit 单独说一句,它只在读过界之后才置位。所以while (!fin.eof())这种循环是经典错写法,2.3 节读文件还会碰见它。正确的判断方式是把流本身写进条件。
1.7 输出格式化
cout 默认输出不讲格式,数字挤数字,列对不齐。要列对齐,找 iomanip 头文件里的操纵器,用 << 递进去,跟递数据一个姿势。
| 操纵器 | 作用 | 粘不粘 |
|---|---|---|
| setw(n) | 占满 n 列,不够补空格 | 只管下一个 |
| setfill(c) | 补什么字符 | 粘 |
| left / right | 靠哪边站 | 粘 |
| fixed | 小数写法,不用科学计数 | 粘 |
| setprecision(n) | 留几位小数 | 跟着 fixed |
| hex / oct / dec | 换进制输出 | 粘 |
| boolalpha | true 和 1 之间切换 | 粘 |
setw 只管下一个,是新手头号翻车点。设了一次以为一劳永逸,第二列开始全乱。
#include <iomanip> cout << setw(8) << 42 << "\n"; // 右边补空格,对齐到 8 列 cout << left << setw(8) << 42 << "\n"; // 靠左,之后都靠左 cout << fixed << setprecision(2) << 3.14159 << "\n"; // 3.14 cout << hex << 255 << "\n"; // ff对齐一张表是个综合例子,setw 和 left 配着用,每一列都设一遍。
cout << left << setw(10) << "name" << setw(6) << "age" << "city" << "\n"; cout << left << setw(10) << "zhangsan" << setw(6) << 20 << "beijing" << "\n"; cout << left << setw(10) << "ouyangxiu" << setw(6) << 21 << "chuzhou" << "\n";输出长这样。
name age city zhangsan 20 beijing ouyangxiu 21 chuzhou全是英文和数字才对得齐,掺中文就歪。setw 按字符数补位,不管显示宽度,一个汉字占两列它不问。想对齐中文,自己写一个按显示宽度补位的小函数,这题留到课后练习。格式化还有 printf 一派的老写法,C++20 之后有了 std::format,这本书不展开,知道有它就行。
二、文件输入/输出
2.1 ofstream 写文件
文件的读写搬到 fstream 头文件,三个类按读写分工。
| 类 | 能干什么 | 默认打开方式 |
|---|---|---|
| ofstream | 只写 | out 加 trunc |
| ifstream | 只读 | in |
| fstream | 读写都行 | in 加 out,不清空 |
最小示例,写三行进一个文件。
#include <fstream> using namespace std; int main() { ofstream fout("notes.txt"); fout << "第一行\n"; fout << "第二行\n"; fout << "第三行\n"; fout.close(); return 0; }用法和 cout 一模一样,<< 递进去,东西进文件。长得一样,前面学的链式和操纵器,搬到文件流上照样好使。
两个坑先说在前头。第一,ofstream 默认带 trunc,文件原来有内容,一打开就清空,写完发现老朋友没了。要留着旧内容,用 app 追加,写日志的标配。第二,路径是相对于程序运行目录的,不是源码所在目录。在 IDE 里点运行和在命令行里跑,工作目录经常不是一回事,文件找不到,先怀疑这儿。
ofstream fout("log.txt", ios::app); fout << "又一次启动\n"; // 接在旧内容后面关闭文件两种,显式 close,或者等对象析构自动关。忘关大概率没事,析构会兜底,但有两处要显式关,写完这份马上就要读回来,以及同一个流要换个文件重新开。
2.2 打开模式的组合
打开方式用标志位拼,六个标志,竖线相连。
┌────────────────────────────────────────────────────────┐ │ 打开模式的组合 │ ├────────────────────────────────────────────────────────┤ │ 六个打开标志,常用组合就四种 │ │ │ │ +------------+--------------------+----------------+ │ │ |标志 |干什么 |常见组合 | │ │ +------------+--------------------+----------------+ │ │ |in |开下来读 |ifstream 默认 | │ │ |out |开下来写 |ofstream 默认 | │ │ |app |写在末尾 |追加写日志 | │ │ |trunc |先清空再写 |默认就带它 | │ │ |ate |开完停末尾 |定位后再写 | │ │ |binary |按字节搬 |图片与音频 | │ │ +------------+--------------------+----------------+ │ │ │ │ 默认那套覆盖绝大多数场合 │ │ 文件默认文本模式,binary 放 2.5 节细说 │ └────────────────────────────────────────────────────────┘常用组合就四种,只写覆盖、只写追加、只读、读写。读写两头占用 fstream,注意它默认不清空,这点和 ofstream 不一样。
fstream fs("data.txt", ios::in | ios::out);文件不存在还想写,out 会顺带新建一个,in 不会,打不开就是打不开。这个差别 2.4 节要考。
2.3 ifstream 读文件
读文件的三种读法和标准输入同源,按词、按行、按字符。
┌────────────────────────────────────────────────────────┐ │ 三种读法各管一摊 │ ├────────────────────────────────────────────────────────┤ │ 读文件三种读法,按词按行按字符 │ │ │ │ +------------+------------------------+------------+ │ │ |读法 |写法 |适合场合 | │ │ +------------+------------------------+------------+ │ │ |按词 |fin >> word |数单词 | │ │ |按行 |getline(fin, s) |日志逐行看 | │ │ |按字符 |fin.get(ch) |数字符 | │ │ +------------+------------------------+------------+ │ │ │ │ 按词读最快,按行最常用,按字符最灵活 │ │ 读大文件按行走,内存吃得消 │ └────────────────────────────────────────────────────────┘三种写法各配一个场合,数单词用 >>,逐行处理用 getline,数字符用 get。
ifstream fin("notes.txt"); string line; int no = 1; while (getline(fin, line)) { cout << no << ": " << line << "\n"; ++no; } fin.close();循环条件直接写流,读到文件尾它自己变假,循环自然停。这比查 eof 优雅,也比它正确,1.6 节说过 eof 置位太晚。
整个文件原样拷到另一个文件,有一行搞定的写法。
ifstream fin("a.txt"); ofstream fout("b.txt"); fout << fin.rdbuf();rdbuf 是流自带的缓冲区,倒过去连循环都省了。大文件这么拷也快,日常够用。
2.4 打开失败与检查
打开失败不报错,这是文件操作最阴的地方。路径少写了一层目录,程序跑完静悄悄的,以为写完了,一看文件是空的,半天白忙。
检查就一句话,is_open,或者直接把流塞进 if。
ofstream fout("out/report.txt"); if (!fout.is_open()) { cerr << "打不开 out/report.txt,检查目录和权限\n"; return 1; } fout << "内容\n";失败的原因翻来覆去就那几种。
| 现象 | 八成是 |
|---|---|
| 提示打不开 | 文件不存在或者路径写错 |
| 目录里没有 | 写的时候目录不存在,out 不会顺带建目录 |
| 有权限还打不开 | 文件被别的程序占着,Windows 尤其常见 |
| 能打开,读出来是乱码 | 编码不对,看下面 |
最后一条多说两句。文件里存的是字节,按什么规矩解释它是编码的事。UTF-8 存的文件用 UTF-8 读,相安无事,拿 GBK 的文件当 UTF-8 读,满屏问号。第一章那个 chcp 65001 的坑,根子也在这儿。还有个隐蔽的,Windows 记事本存 UTF-8 会在文件头塞三个字节的标志,读出来行首多一个看不见的字符,表格第一列对不齐,先查它。
2.5 文本模式和二进制模式
默认文本模式,Windows 上会做一个小动作,写出去的\n换成\r\n落盘,读进来再换回来。
┌──────────────────────────────────────────────────────────┐ │ 文本与二进制的差别 │ ├──────────────────────────────────────────────────────────┤ │ 同一个换行,两种模式落盘不一样 │ │ │ │ Linux h e l l o \n 6 字节 │ │ Windows h e l l o \r \n 7 字节 │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 文本模式 Windows 把 \n 换成 \r \n 落盘 │ │ │ └────────────────────────────────────────────────────┘ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ 二进制模式 原样搬,一个字节都不动 │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ Linux 写的文本在 Windows 记事本里挤成一团 │ │ 拿不准就用二进制,原样搬总不会错 │ └──────────────────────────────────────────────────────────┘Linux 不管这些,\n就是\n。后果有两个方向。Linux 上写的文本文件拿到 Windows 记事本里,全糊成一段,记事本只认\r\n。反过来,Windows 上写的文件拿回 Linux 读,行尾多一个\r,按\n切行,每段尾巴挂个野字符。
跨机器传数据,一律二进制模式打开,一个字节都不转换。
ofstream fout("data.bin", ios::binary); int nums[] = {1, 2, 3, 4, 5}; fout.write(reinterpret_cast<char*>(nums), sizeof(nums)); fout.close(); ifstream fin("data.bin", ios::binary); int back[5]; fin.read(reinterpret_cast<char*>(back), sizeof(back)); for (int i = 0; i < 5; ++i) { cout << back[i] << " "; } cout << "\n";reinterpret_cast 是强制类型转换里最野蛮的那一档,把 int 数组的内存原样当成字符流递出去。日常别用,二进制读写这一种场合除外。
结构体往文件里搬要小心,编译器会在成员之间塞对齐用的填充字节,sizeof 出来的数比肉眼加起来的大,换一个编译器或者换一个平台又不一样。想踏实存结构体,一个字段一个字段地写,或者上序列化的库,这话题留到以后。
文件里跳着读写,用 seekg 和 seekp 搬位置,tellg 和 tellp 问位置,二进制模式配它们最顺手。初学阶段知道名字就行,用上了再查。
三、小结
┌────────────────────────────────────────────────────────┐ │ 输入输出全景 │ ├────────────────────────────────────────────────────────┤ │ 一章走完,这条线就通了 │ │ │ │ 标准流 │ │ cout 往屏幕送,cerr 不缓冲,留遗言用 │ │ cin 从键盘取,>> 按类型,getline 按行 │ │ │ │ 文件流 │ │ ofstream 写,ifstream 读,fstream 两头占 │ │ 文本模式在 Windows 换 \n,二进制原样搬 │ │ │ │ +----------------+------------------+--------------+ │ │ |现象 |先查哪 |再查哪 | │ │ +----------------+------------------+--------------+ │ │ |姓名读成空的 |>> 后面没清场 |补 ignore | │ │ |喂字母就卡死 |failbit 没人清 |clear ignore | │ │ |文件写了是空的 |打开失败没查 |路径与权限 | │ │ |末行处理两遍 |循环用了 eof |while getline | │ │ +----------------+------------------+--------------+ │ │ │ └────────────────────────────────────────────────────────┘回归顺序建议这么走。先把 cout 玩熟,打印、对齐、重定向。再碰 cin,>>、getline、状态三板斧。最后上文件,写、读、模式、检查。每一步都留一个能跑的小程序,下一章的流程控制要拿它们当积木。
3.1 本章知识收口表
| 主题 | 一句话 | 提醒 |
|---|---|---|
| 流 | 程序和外界之间的传送带 | 标准流和文件流同一套读法 |
| 标准流 | cin cout cerr clog | cerr 不缓冲,崩溃遗言靠它 |
| 输出 | << 链式,endl 换行加刷新 | 大循环用\n |
| 输入 | >> 按类型,getline 按行 | 前后脚用要清场 |
| 状态 | failbit eofbit badbit | clear 加 ignore 救活 |
| 文件类 | ofstream ifstream fstream | ofstream 默认清空 |
| 打开模式 | in out app trunc ate binary | 写日志用 app |
| 格式化 | setw setfill left fixed | setw 只管下一次 |
3.2 五句话记住本章
- 流是传送带,标准输入输出和文件输入输出共用一套读法。
- endl 换行加刷新,大循环用
\n,崩溃遗言找 cerr。 - cin >> 和 getline 前后脚用,中间一句 ignore 清场。
- 打开文件一定查 is_open,静默失败最坑人。
- 跨机器传数据用二进制,文本模式只在本地玩。
3.3 课后练习
- 默写四个流对象,说清 cerr 和 cout 的差别,什么场合非它不可。
- 写一个程序,先读学号再读姓名,姓名允许带空格。先用 >> 和 getline 混用实现,再用纯 getline 实现一遍,对比两次代码。
- 改 1.7 节的对齐表格例子,数据换成中文姓名,观察列怎么歪,写一个按显示宽度补位的小函数修好它。提示,一个汉字算两列。
- 写一个日志小程序,每次运行往 log.txt 末尾追加一行。时间那步先写个假字符串占位,以后学了时间函数再补。
- 统计一个文本文件的行数、单词数、字符数,三种读法各用一遍,对结果。
- 挑战题,把一个文本文件的
\n换成\r\n存成新文件,模拟 Windows 记事本的行为。提示,按字符读按字符写,\r排在\n前面。
附录|输入输出速查卡
| 类别 | 成员 | 一句话提醒 |
|---|---|---|
| 标准流 | cin cout cerr clog | cerr 不缓冲,崩溃遗言靠它 |
| 输出 | << endl | endl 换行加刷新 |
| 输入 | >> getline ignore | 混用要清场 |
| 状态 | clear ignore fail eof | 三板斧救活 |
| 文件类 | ofstream ifstream fstream | ofstream 默认清空 |
| 打开模式 | in out app trunc ate binary | 写日志用 app |
| 格式化 | setw setfill left fixed hex | setw 只管一次 |
| 键盘结束 | Ctrl+Z 或 Ctrl+D | 告诉程序输入到头 |
| 常见现象 | 原因 | 处理 |
|---|---|---|
| 姓名读到一半 | >> 遇空格就停 | 换 getline |
| 名字栏是空的 | 换行被 getline 捡走 | >> 后补 ignore |
| 喂字母程序卡死 | failbit 没清 | clear 加 ignore 重问 |
| 表格列对不齐 | setw 设少或以为它粘 | 每列都设一遍 |
| 文件写了但是空的 | 打开失败没检查 | 先 is_open |
| 最后一行处理两遍 | 循环用了 eof | 判断写流本身 |
| Linux 文件在 Windows 挤一团 | 缺\r | 转换或换编辑器 |