前言
如果说算法是程序的"大脑",那么 IO 就是程序的"感官"。C++ 用一套流(Stream)继承体系统一了控制台、文件、字符串的输入输出。
📑 目录
- 1. IO 继承家族
- 2. IO 流状态
- 3. 管理输出缓冲区
- 4. 标准 IO 流 —— 你每天都在用却未必真正理解的 cin/cout
- 5. 文件 IO 流 —— 从文本读写到二进制序列化
- 6. string IO 流 —— 内存中的"字符串瑞士军刀"
- 7. 总结与最佳实践
这篇文章会从继承体系 → 状态管理 → 缓冲区机制 → 文件操作 → string 流,讲透 C++ IO 库的全部核心知识。
读完你将收获:
- 掌握 C++ IO 流家族的继承体系与设计思想
- 理解四个流状态位(goodbit/eofbit/failbit/badbit)与错误恢复
- 搞懂输出缓冲区的刷新机制与高性能 IO 的配置方法
- 熟练使用文件流进行文本/二进制读写,避开深坑
- 学会用 stringstream 实现数据与字符串的互相转换
1. IO 继承家族
1.1 设计哲学
C++ 语言不直接处理输入输出,而是通过标准库中的一组类来完成 IO 操作。这套类的设计有三大巧妙之处:
- 统一接口:无论是控制台、文件还是 string,都使用同一套
<<和>>运算符——“一处学会,处处通用” - 继承体系:将通用能力放在基类(
ios_base→ios→istream/ostream),将设备专属能力放在派生类 - 模板支持:不仅能在
char上做 IO,也能在wchar_t宽字符上做 IO(对应的类和对象都以w开头)
1.2 全局对象速查
| 对象 | 类型 | 绑定设备 | 说明 |
|---|---|---|---|
cin | istream | 标准输入(键盘) | 默认关联到控制台 |
cout | ostream | 标准输出(屏幕) | 带缓冲 |
cerr | ostream | 标准错误(屏幕) | 无缓冲,立即输出 |
clog | ostream | 标准日志(屏幕) | 带缓冲 |
2. IO 流状态
2.1 四个状态位
IO 操作中可能会发生各种错误,每个流对象内部维护着一个状态标志字,通过四个位来标记流的健康状况:
| 状态位 | 含义 | 触发条件 | 是否可恢复 |
|---|---|---|---|
goodbit | 一切正常 | 流的初始状态,无任何错误 | ✅ 这是目标状态 |
eofbit | 到达文件末尾 | 读取时遇到 EOF | ✅ 可恢复 |
failbit | IO 操作失败(逻辑错误) | 期望读 int 却读到字符、格式不匹配等 | ✅ 可恢复 |
badbit | 流崩溃(系统级错误) | 不可恢复的底层读写错误 | ❌ 通常无法恢复 |
重要:这四位是可以组合的位标志——它们定义在 ios_base 基类中,可以作为独立二进制位通过位运算组合。
2.2 实战:cin 读取失败后如何恢复?
这是新手最常遇到的困惑——不小心往数字输入里打了一个字母,此后所有的 cin >> 全都废了:
#include <iostream>
using namespace std;
int main()
{
// ===== 查看初始状态 =====
cout << cin.good() << endl; // 1(正常)
cout << cin.eof() << endl; // 0
cout << cin.bad() << endl; // 0
cout << cin.fail() << endl; // 0
cout << endl;
int i = 0;
// ===== 故意输入一个字符(比如 'a'),触发 failbit =====
cin >> i;
cout << i << endl; // 输出:0(读取失败,i 保持原值)
// ===== 查看失败后的状态 =====
cout << cin.good() << endl; // 0(不再正常)
cout << cin.eof() << endl; // 0
cout << cin.bad() << endl; // 0
cout << cin.fail() << endl; // 1(failbit 被设置!)
cout << endl;
if (cin.fail())
{
// ===== 第 1 步:恢复流状态为 goodbit =====
cin.clear(); // 否则后面的 IO 操作全部会失败!
// ===== 第 2 步:把缓冲区里的"垃圾字符"读走 =====
// 否则再去 cin >> i 还会读到那个字符,又失败!
char ch = cin.peek(); // 偷看一眼缓冲区首字符
while (!(ch >= '0' && ch <= '9'))
{
ch = cin.get(); // 读走一个非数字字符
cout << "丢弃字符: " << ch << endl;
ch = cin.peek(); // 再看下一个字符
}
cout << endl;
}
// ===== 恢复后再次读取 =====
cout << cin.good() << endl; // 1(恢复正常!)
cout << cin.eof() << endl; // 0
cout << cin.bad() << endl; // 0
cout << cin.fail() << endl; // 0
cout << endl;
cin >> i;
cout << "成功读到: " << i << endl;
return 0;
}
核心要点:
clear()只是重置状态位,不会自动清理缓冲区中残留的"垃圾数据"- 恢复流程 =
clear()重置状态 + 手动清空缓冲区中的无效字符- 一个流一旦进入错误状态,后续所有 IO 操作都会自动失败——这是"fail-fast"的防御设计
2.3 eofbit、failbit、badbit 的进阶关系
值得特别关注的是:当到达文件末尾时,eofbit 和 failbit 会被同时设置。这也解释了为什么用 while (cin >> x) 读到文件尾时会退出循环——failbit 被设置导致 operator bool() 返回 false。
另外,setstate() 和 rdstate() 两个函数也能用来操作流状态,具体用法可以参考官方文档。
3. 管理输出缓冲区
3.1 为什么需要缓冲区?
任何输出流都管理着一个输出缓冲区。执行 os << "hello world"; 时,字符串可能被立即输出,也可能被暂时存在缓冲区里,稍后再输出。
为什么要这样做?因为设备写操作非常耗时。有了缓冲区,操作系统可以将多个输出操作合并为一次系统调用,带来可观的性能提升:
无缓冲区: write("h") → write("e") → write("l") → write("l") → write("o")
每个字符一次系统调用,5 次!
有缓冲区: [h][e][l][l][o][ ][w][o][r][l][d]
攒够一批 → 一次系统调用全部写出去!
3.2 触发缓冲区刷新的 5 种情况
| 触发条件 | 说明 |
|---|---|
| 1. 程序正常结束 | main 函数 return 时,所有流会被自动刷新关闭 |
| 2. 缓冲区满了 | 缓冲区写满时被迫刷新(一般不用你操心) |
3. 使用了 endl 或 flush | endl = 换行 + 刷新;flush = 只刷新不换行 |
4. 设置了 unitbuf | 告诉流每次写后立即刷新;cerr 默认设置了它 |
| 5. 关联流发生 IO | 输出流关联到另一个流时,另一个流一旦读写,输出流就刷新 |
3.3 实战对比:理解缓冲区的"延迟感"
#include <iostream>
#include <fstream>
using namespace std;
void func(ostream& os)
{
os << "hello world";
os << "hello bit";
// 此时 "hello world" 和 "hello bit" 是否已输出?
// 答案:不确定!它们可能还在缓冲区里
system("pause");
// 加上下面任意一行,前面的内容一定会被刷新:
// os << endl; // 方式1:输出换行并刷新
// os << flush; // 方式2:只刷新,不换行
// int i; cin >> i; // 方式3:如果 os 关联了 cin,读 cin 会触发 os 刷新
os << "hello cat";
// "hello cat" 是否已输出?——同样不确定
system("pause");
}
int main()
{
ofstream ofs("test.txt");
// 试验 1:默认情况 —— 内容延迟写入
// func(cout);
// 试验 2:设置 unitbuf —— 每次写都立即刷新
// ofs << unitbuf;
// 试验 3:cin 绑定到 ofs —— 读 cin 时会刷新 ofs 的缓冲区
// cin.tie(&ofs);
func(ofs);
return 0;
}
3.4 tie() —— 流的关联绑定
tie() 函数用于在流之间建立关联:
- 默认绑定:
cin和cerr都被关联到了cout。这意味着——你每次读cin或写cerr,cout的缓冲区都会被先刷新。这保证了提示信息和输入操作的顺序正确。 - 解绑:
cin.tie(nullptr)可以解除cin与cout的绑定 - 绑定到别的流:
cin.tie(&ofs)可以让读取cin时顺便刷新ofs的缓冲区
#include <iostream>
using namespace std;
int main()
{
// ===== 竞赛必用:高性能 IO 三件套 =====
// 1. 关闭 C++ 流和 C 流的同步
// 关闭后,cout 和 printf 的输出顺序就不能保证了!
ios_base::sync_with_stdio(false);
// 关闭同步后,下面三行的输出顺序可能是 b a c(而不是 a b c)
// std::cout << "a\n";
// std::printf("b\n");
// std::cout << "c\n";
// 2 & 3. 解绑 cin 和 cout 与其他流的关联
cin.tie(nullptr);
cout.tie(nullptr);
// 这两行解开后,每次 cin 读取不会触发 cout 刷新
// 大幅减少系统调用,尤其在大量输入输出的场景下
return 0;
}
竞赛选手的必备姿势:
ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); // 然后用 '\n' 代替 endl(避免不必要的刷新)这能让你在使用
cin/cout时获得接近scanf/printf的性能。但关闭同步后绝对不能混用 C 和 C++ 的 IO 函数,否则输出顺序会错乱。
4. 标准 IO 流 —— 你每天都在用却未必真正理解的 cin/cout
4.1 基础回顾
| 对象 | 类型 | 特点 |
|---|---|---|
cin | istream | 标准输入,关联键盘 |
cout | ostream | 标准输出,带缓冲区 |
cerr | ostream | 标准错误,无缓冲(设置了 unitbuf) |
clog | ostream | 标准日志,带缓冲 |
4.2 operator bool —— "流能当条件用"的秘密
你有没有好奇过为什么可以写 while (cin >> x)?
#include <iostream>
#include <string>
using namespace std;
int main()
{
int i = 0, j = 1;
// ===== 为什么 cin >> i >> j 能放在 while 条件里? =====
// operator>> 的返回值是 istream&(即 cin 本身)
// cin 这个 istream 对象可以被隐式转换为 bool 值
//
// 转换规则:
// - goodbit → true (流正常,可以继续读)
// - badbit 或 failbit 被设置 → false (流不正常,停止)
//
while (cin >> i >> j)
{
cout << i << " : " << j << endl;
}
// 输入 Ctrl+Z 换行可以结束循环
// Ctrl+Z 会让流读到 EOF → eofbit + failbit → operator bool 返回 false
// ===== 流状态检查 =====
cout << cin.good() << endl; // 0
cout << cin.eof() << endl; // 1(读到文件尾了)
cout << cin.bad() << endl; // 0
cout << cin.fail() << endl; // 1
cout << endl;
// ===== 流一旦出错就不能再用,需要恢复 =====
cin.clear();
string s;
while (cin >> s)
{
cout << s << endl;
}
return 0;
}
4.3 几个重要细节
istream和ostream是不可拷贝的(拷贝构造和赋值是delete的),只能通过引用传递- 内置类型(
int、double、char*等)的<<和>>已经在标准库中做好了重载 - 自定义类型需要我们自己重载
operator<<和operator>>
5. 文件 IO 流 —— 从文本读写到二进制序列化
5.1 三个核心类
| 类 | 继承关系 | 用途 |
|---|---|---|
ifstream | istream ← ifstream | 读文件 |
ofstream | ostream ← ofstream | 写文件 |
fstream | iostream ← fstream | 读写文件 |
5.2 文件打开模式
文件流可以在构造时打开文件,也可以调用 open() 函数打开文件。这些打开模式是 ios_base 中定义的标志位,可以通过按位或组合使用:
| 模式 | 含义 |
|---|---|
in | 以读方式打开 |
out | 以写方式打开 |
binary | 以二进制模式打开 |
ate | 打开后立即寻位到流结尾(At The End) |
app | 每次写入前寻位到流结尾(APPend) |
trunc | 打开时舍弃(清空)文件原有内容 |
5.3 app vs ate —— 面试高频考点
这两个模式非常容易搞混:
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
ofstream ofs("test.txt");
// ===== 基本写操作 =====
ofs.put('x'); // 写单个字符
ofs.write("hello\nworld", 11); // 写指定字节数
ofs << "22222222" << endl; // 用 << 操作符
int x = 111;
double y = 1.11;
ofs << x << endl;
ofs << y << endl;
ofs.close();
// ===== 实验1:app 模式——永远在文件尾写 =====
ofs.open("test.txt", ios_base::out | ios_base::app);
ofs << "1111111" << endl;
// 即使手动移动写指针到文件头……
ofs.seekp(0, ios_base::beg);
// app 模式下,写入还是会发生在文件末尾!
// seekp 对写入位置无效,因为 app 强制每次写前寻位到尾部
ofs << x << " " << y << endl;
ofs.close();
// ===== 实验2:ate 模式——可自由移动写指针 =====
ofs.open("test.txt", ios_base::out | ios_base::ate);
ofs << "1111111" << endl; // 打开后在末尾,所以追加
ofs.seekp(0, ios_base::beg); // 移动到文件头
ofs << x << " " << y << endl; // 从头部覆盖写!
ofs.close();
return 0;
}
一句话区别:
app= 每次写入强制在文件尾部追加,你无法通过seekp改变写入位置ate= 只是在文件打开瞬间跳到文件尾,之后你可以用seekp去任何位置写
5.4 out vs out|trunc —— trunc 到底有没有必要?
// 以下两种写法在当前主流实现中行为一致:
ofs.open("test.txt", ios_base::out);
ofs.open("test.txt", ios_base::out | ios_base::trunc);
// 两者都会先把文件内容清空,再开始写
// 那 trunc 存在的意义是什么?
// 1. 语义明确:out|trunc 更清楚地表达了"有内容就清掉"的意图
// 2. 跨平台保险:在某些非主流实现中,out 的行为可能不完全等同于截断
5.5 文件读写的常用函数
ifstream 的读函数:get()(读一个字符)、read()(读指定字节)、>>(格式化读)
ofstream 的写函数:put()(写一个字符)、write()(写指定字节)、<<(格式化写)
5.6 实战:二进制复制图片文件
#include <iostream>
#include <fstream>
using namespace std;
int main()
{
// 必须以 binary 模式打开,否则图片中的某些字节会被错误解释
ifstream ifs("source.png", ios_base::in | ios_base::binary);
ofstream ofs("copy.png", ios_base::out | ios_base::binary);
int n = 0;
while (ifs && ofs)
{
char ch = ifs.get();
ofs << ch;
++n;
}
cout << "复制完成,共 " << n << " 字节" << endl;
return 0;
}
5.7 进阶实战:自定义类型的二进制与文本读写
这个例子综合展示了文件 IO 的核心能力,也是项目中最常见的需求——配置文件的读写:
#include <iostream>
#include <fstream>
#include <string>
using namespace std;
// ===== 自定义 Date 类,重载了 IO 运算符 =====
class Date
{
friend ostream& operator << (ostream& out, const Date& d);
friend istream& operator >> (istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year)
, _month(month)
, _day(day)
{}
private:
int _year;
int _month;
int _day;
};
istream& operator >> (istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
ostream& operator << (ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day;
return out;
}
// ===== 服务器配置信息结构体 =====
struct ServerInfo
{
// 二进制读写时,这里千万不能用 string!!
// 原因:string 对象中存储的是指向堆上字符数组的指针
// 如果直接二进制 write(&info, sizeof(info))
// 写出去的是指针的值(一个地址),不是字符串内容!
// 再读回来时,指针指向的位置可能已经无效了 → 野指针!
char _address[32]; // ✅ 二进制安全
// string _address; // ❌ 二进制不安全!
int _port;
Date _date;
};
// ===== 配置文件管理器 =====
struct ConfigManager
{
public:
ConfigManager(const char* filename)
:_filename(filename)
{}
// ---------- 二进制写 ----------
// "内存中怎么存的,就囫囵吞枣怎么直接写出去"
void WriteBin(const ServerInfo& info)
{
ofstream ofs(_filename, ios_base::out | ios_base::binary);
// 把 info 的内存块原封不动写出去
ofs.write((const char*)&info, sizeof(info));
}
// ---------- 二进制读 ----------
// "文件中的内容直接囫囵吞枣读到内存中"
void ReadBin(ServerInfo& info)
{
ifstream ifs(_filename, ios_base::in | ios_base::binary);
ifs.read((char*)&info, sizeof(info));
}
// ---------- 文本写 ----------
void WriteText(const ServerInfo& info)
{
ofstream ofs(_filename);
// 利用重载好的 << 逐个字段写
ofs << info._address << " " << info._port << " " << info._date;
}
// ---------- 文本读 ----------
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename);
// 利用重载好的 >> 逐个字段读
ifs >> info._address >> info._port >> info._date;
}
private:
string _filename;
};
// ===== 测试 =====
void WriteBin()
{
ServerInfo winfo = { "192.0.0.1", 80, { 2025, 1, 10 } };
ConfigManager cf_bin("test.bin");
cf_bin.WriteBin(winfo);
}
void ReadBin()
{
ConfigManager cf_bin("test.bin");
ServerInfo rbinfo;
cf_bin.ReadBin(rbinfo);
cout << rbinfo._address << " " << rbinfo._port << " " << rbinfo._date << endl;
}
void WriteText()
{
ServerInfo winfo = { "192.0.0.1", 80, { 2025, 1, 10 } };
ConfigManager cf_text("test.txt");
cf_text.WriteText(winfo);
}
void ReadText()
{
ConfigManager cf_text("test.txt");
ServerInfo rtinfo;
cf_text.ReadText(rtinfo);
cout << rtinfo._address << " " << rtinfo._port << " " << rtinfo._date << endl;
}
int main()
{
WriteBin();
ReadBin(); // 二进制读写验证
WriteText();
ReadText(); // 文本读写验证
return 0;
}
🚨 二进制读写的致命陷阱:
结构体中包含
string成员时,绝对不要用write()/read()做二进制读写!原因图解:
string 对象的实际内存布局: ┌─────────────────────┐ │ _Ptr ───────────────→ 堆上的字符数组 "192.0.0.1" │ _Size │ │ _Capacity │ └─────────────────────┘ write(&info, sizeof(info)) 写出的是 _Ptr 的值(一个地址值,如 0x1A2B3C4D) 而不是 "192.0.0.1" 这个字符串本身! 当程序重新运行,read 回来时: _Ptr = 0x1A2B3C4D ← 这个地址早就失效了! 访问 = 野指针 = 崩溃解决方案:
- 二进制读写用
char _address[32]这种定长字符数组(数组内容直接存在结构体内存中)- 或者用文本读写(
<<和>>),string的重载会正确处理
5.8 文本读写 vs 二进制读写 对比
| 维度 | 文本读写 | 二进制读写 |
|---|---|---|
| 可读性 | ✅ 人可直接阅读 | ❌ 乱码,不可读 |
| 文件大小 | 较大(数字要转字符串) | 较小(原样存储) |
string 支持 | ✅ 安全 | ❌ 致命bug |
| 跨平台 | ❌ 文本格式可能不同 | ❌ 字节序/对齐可能不同 |
| 适用场景 | 配置文件、日志 | 图片、音视频、性能敏感 |
6. string IO 流 —— 内存中的"字符串瑞士军刀"
6.1 三个类
| 类 | 继承关系 | 用途 |
|---|---|---|
istringstream | istream ← istringstream | 从 string 读 |
ostringstream | ostream ← ostringstream | 往 string 写 |
stringstream | iostream ← stringstream | 双向读写 |
底层都维护了一个 string 对象来保存数据,通过 str() 函数获取或设置。
6.2 基础用法:数据类型 ↔ 字符串
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
int main()
{
// ===== 数据 → 字符串(ostringstream) =====
int i = 123;
Date d = { 2025, 4, 10 };
ostringstream oss;
oss << i << endl;
oss << d << endl;
string s = oss.str(); // 获取底层 string 对象
cout << s << endl; // 输出:123\n2025 4 10
// ===== 字符串 → 数据(istringstream) =====
// 方式1:构造时传入字符串
istringstream iss("100 2025 9 9");
// 方式2:用 str() 设置底层字符串
// istringstream iss;
// iss.str("100 2025 9 9");
int j;
Date x;
iss >> j >> x;
cout << j << endl; // 输出:100
cout << x << endl; // 输出:2025 9 9
return 0;
}
6.3 必须警惕:重复使用 stringstream 的坑
这是一个极易踩中的陷阱:
int main()
{
int a = 1234;
int b = 5678;
string str;
stringstream ss;
ss << a << " " << b;
// 注意:>> 遇到空格就停了,这里只会读到 "1234"
ss >> str;
cout << str << endl; // 输出:1234
cout << ss.fail() << endl; // 0(还没读完)
cout << ss.bad() << endl; // 0
// ========== 陷阱!第一次用完后想接着用 ==========
// stringstream 在读到流尾时,会设置 eofbit 和 failbit
// 如果不重置状态,后续的 << 和 >> 全部失效!
// ===== 正确做法:两步必须都做 ====
// 第 1 步:重置流状态位为 goodbit
ss.clear();
// 第 2 步:清空底层 string 的内容
ss.str("");
double dd = 12.34;
ss << dd;
ss >> str;
cout << str << endl; // ✅ 成功输出:12.34
return 0;
}
核心结论:重复使用 stringstream 时,必须同时调用
clear()和str("")。前者重置状态位,后者清空数据。只调用clear()不清空数据,会导致老数据残留;只调用str("")不重置状态,会导致流仍然处于失败状态。
6.4 综合实战:聊天信息序列化
这是 stringstream 在真实项目中的经典应用——将结构体数据序列化为字符串,通过网络发送后再反序列化回来:
#include <iostream>
#include <sstream>
#include <string>
using namespace std;
// ===== Date 类(同上,省略构造函数细节) =====
class Date
{
friend ostream& operator << (ostream& out, const Date& d);
friend istream& operator >> (istream& in, Date& d);
public:
Date(int year = 1, int month = 1, int day = 1)
:_year(year), _month(month), _day(day) {}
private:
int _year, _month, _day;
};
istream& operator >> (istream& in, Date& d)
{
in >> d._year >> d._month >> d._day;
return in;
}
ostream& operator << (ostream& out, const Date& d)
{
out << d._year << " " << d._month << " " << d._day;
return out;
}
// ===== 聊天消息结构体 =====
struct ChatInfo
{
string _name; // 名字
int _id; // ID
Date _date; // 时间
string _msg; // 聊天信息
};
int main()
{
// ==================== 序列化(结构体 → 字符串)====================
ChatInfo sendInfo = { "张三", 135246, { 2022, 4, 10 }, "晚上一起看电影吧" };
ostringstream oss;
oss << sendInfo._name << " "
<< sendInfo._id << " "
<< sendInfo._date << " "
<< sendInfo._msg;
string str = oss.str();
cout << "序列化后的字符串:" << str << endl << endl;
// ===== 实际开发中,这个字符串就可以通过网络发给对方了 =====
// 更复杂的场景一般会用 JSON、XML、Protobuf 等来序列化
// ==================== 反序列化(字符串 → 结构体)====================
ChatInfo recvInfo;
istringstream iss(str);
iss >> recvInfo._name
>> recvInfo._id
>> recvInfo._date
>> recvInfo._msg;
// ===== 恢复成结构化数据显示 =====
cout << "-------------------------------------------------------" << endl;
cout << "姓名:" << recvInfo._name << "(" << recvInfo._id << ") ";
cout << recvInfo._date << endl;
cout << recvInfo._name << ":>" << recvInfo._msg << endl;
cout << "-------------------------------------------------------" << endl;
return 0;
}
运行结果:
序列化后的字符串:张三 135246 2022 4 10 晚上一起看电影吧
-------------------------------------------------------
姓名:张三(135246) 2022 4 10
张三:>晚上一起看电影吧
-------------------------------------------------------
局限性说明:这种空格分隔的简单序列化方案只适用于字段中不含空格的简单场景。真实项目中消息往往包含空格、换行等复杂的结构化数据,通常会选用 JSON、XML 或 Protobuf 来承载。
7. 总结与最佳实践
7.1 知识地图
C++ IO 库
├── 继承体系
│ ├── ios_base → ios → istream/ostream
│ ├── 控制台:cin, cout, cerr, clog
│ ├── 文件:ifstream, ofstream, fstream
│ └── 字符串:istringstream, ostringstream, stringstream
├── 流状态管理
│ ├── goodbit(正常)
│ ├── eofbit(文件尾)
│ ├── failbit(逻辑错误,可恢复)
│ ├── badbit(系统错误,不可恢复)
│ └── clear() 恢复,peek()/get() 清理缓冲区
├── 缓冲区管理
│ ├── endl/flush/unitbuf 刷新
│ ├── tie() 绑定关联流
│ └── sync_with_stdio(false) 提速
├── 文件操作
│ ├── 打开模式:in, out, binary, ate, app, trunc
│ ├── 文本读写:<< 和 >> 运算符
│ ├── 二进制读写:write() 和 read()
│ └── ⚠️ string 成员不能用二进制读写
└── 字符串流
├── 数据 ↔ string 互转
├── 序列化与反序列化
└── ⚠️ 重复使用需 clear() + str("")
7.2 最佳实践清单
-
优先选用 C++ 流:相比于 C 语言的
printf/scanf,C++ 流更类型安全、更易扩展——你只需要重载<<和>>,自定义类型就能无缝融入 IO 体系。 -
竞赛场景加速三件套:
ios_base::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); // 然后用 '\n' 代替 endl -
流状态出错后,恢复两步走:
clear()重置状态位 + 手动消费掉缓冲区中的垃圾数据。 -
二进制读写避免
string成员:改用定长char数组,或选用文本方式读写。 -
重复使用
stringstream时,务必clear()+str("")一起调用。 -
appvsate:记住app是每次写都强制追加(无视 seekp),ate只是打开时跳到末尾(之后可以自由移动指针)。 -
日常开发用
'\n'而非endl:除非你确实需要马上刷新缓冲区(比如日志输出),否则endl带来的额外刷新只是浪费性能。 -
查阅文档是个好习惯:cplusplus.com 和 cppreference.com 是最可靠的参考。
7.3 写在最后
C++ 的 IO 库是一套经过几十年工业验证的成熟设计。它的继承体系让你"学会一个接口,处处都能使用";它的状态管理让你能写出健壮的输入处理逻辑;它的缓冲区和流绑定机制让 IO 性能可调可控。
如果这篇文章对你有帮助,欢迎点赞、收藏、关注三连! 🎉
1万+



被折叠的 条评论
为什么被折叠?



