本篇目标:
1.学会IO的基本状态与管理输出缓存
2.了解并学会标准IO流,stringIO流,文件IO流
一.IO继承家族与IO流的状态
1.概念
C++语言不直接处理输入输出,而是通过一族定义在标准库中的类型在处理IO。这些类型支持从设 备中读取数据和向设备中写入数据的IO操作,设备可以是文件、控制台窗口等。
• 目前为止,我们已经使用过的IO类型和对象都是操纵char数据 的,默认情况下,这些对象都关联到控制台窗口。但在实际开发中,IO类并不仅限于控制台输入输出,还支持设备(如文件)以及string对象的IO操作;其次IO类型使用模板实现的,还支持对wchar_t数据的输入输出。
• 通过下图我们就可以看到C++IO类型设计的是⼀个继承家族,通过继承家族类解决控制台/文件/string的IO操作。



这里是有关C++的文档:
官网链接:https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp/io
2.IO流状态
IO操作的过程中,可能会发生各种错误,IO流对象中给了四种状态标识错误,可以参考下面的图标与图片进行理解,如图所示:






简单来讲就是:goodbit表示流没有错误,eofbit表示流到达文件结束,failbit 表示逻辑层面的 IO 操作失败,badbit 表示发生了系统级错误(如底层设备异常),也可以如图所示理解:

可能光看图片不足以理解,下面通过几个代码来演示一下:
cpp
#include<iostream>
using namespace std;
int main()
{
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.bad() << endl;
cout << cin.fail() << endl << endl;
int i = 0;
cin >> i;
cout << "i=" << i << endl;
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.bad() << endl;
cout << cin.fail() << endl << endl;
int j;
cin >> j;
cout << "j=" << j << endl;
return 0;
}
如果我们正常的输入一个数字,那么输出结果是比较正常的,那如果我输入的是一串字符呢?
输出结果:
,可以看出,我们根本就没有机会输入j的值,就结束了
原因:failbit表示⼀个逻辑错误,如期望读取⼀个整形,但是却读取到⼀个字符,failbit就被设置了,但是流是可以恢复的,恢复以后可以继续使用。
结局办法也很简单,我们需要用 cin.clear()来清除流的错误状态标志,把这些错误标志位重置为正常的goodbit,让流重新恢复工作能力,但是我们也不可以直接cin.clear(),否则缓冲区中仍然残留非法字符,后续读取依然会失败。因此需要额外读取并清理缓冲区中的数据,例如在读取 j 之前加入如下代码:
cpp
if (cin.fail())
{
cin.clear();
// 我们还要把缓冲区中的多个字符都读出来,读到数字停下来,否则再去cin >> 还是会失败
char ch = cin.peek();
while (!(ch >= '0' && ch <= '9'))
{
ch = cin.get();
cout << ch;
ch = cin.peek();
}
cout << endl;
}
补充:如果到达文件结束位置eofbit和failbit都会被置位,如果想再次读取当前文件,可以恢复⼀下流的状态,同时重置⼀个文件指针位置,这里就不再演示。
二 .管理输出缓冲区
1.什么是缓冲区?
先说一个结论:我们每次向显示器输出的内容,通常不会直接显示在屏幕上,而是先暂存在缓冲区中,待满足刷新条件后再显示。
概念: 我们向显示屏上输出内容时,先将内容暂存 到负责管理这层缓冲区的std::streambuf中,当 遇到 endl、flush 或者缓冲区满了等满足刷新缓冲区的条件时,C++ 标准库就会执行一个操作,将**C++ 缓冲区里的数据打包,通过系统调用(Linux 下的 write)移交给操作系统,**其实我们学了操作系统后会更好的理解。
这时可能会有人有疑问:**为什么要有这个缓冲区呢?**我直接给操作系统不就可以了吗,简直就是多此一举!答案也很简单,这里举个例子:
-
没有快递点(无缓冲区): 快递员(你的程序)每送一个包裹,都要在楼下等你专门跑一趟拿走(直接屏幕输出),你频繁上下楼,快递员也得干等,双方效率极低(频繁 I/O 操作拖垮系统性能)。
-
有菜鸟驿站(有缓冲区): 快递员一口气把 10 个包裹全扔进驿站(写入内存缓冲区),然后立刻去忙别的,你下班后,一次性 把 10 个包裹全拿走(遇到
endl触发"刷新",一次性显示到屏幕上)。
原因:有了缓冲区机制,操作系统就可以将多个输出操作组合成为一个单一的系统级写操作,因为设备的写操作通常是很耗时的,允许操作系统将多个输出操作组合为单一的设备写操作可以带来很大的性能提升。
触发缓冲区刷新的条件:
<1>.手动刷新缓冲区,例如:遇到 std::endl,插入一个换行符 \n,并强制刷新缓冲区;遇到 std::flush: 只强制刷新缓冲区,不换行;调用成员函数 .flush()。
<2>.系统强制刷新:缓冲区满了,因为 缓冲区的容量是有限的(通常是几 KB),当写入的数据量超出了缓冲区的承载上限时,系统为了腾出空间接收新数据,会自动把满载的数据发走;程序正常结束;⼀个输 出流关联到另⼀个流时 ,当这个流读写时,输出流会⽴即刷新缓冲区 。例如默认情况下cerr和cin都 被关联到cout,所以读cin或写cerr时,都会导致cout缓冲区会被⽴即刷新;设置了 unitbuf 标志(每次都刷新)。
2.代码演示
cpp
#include<iostream>
#include<fstream>
using namespace std;
void func(ostream& os)
{
os << "hello world";
os << "hello bit";
// "hello world"和"hello bit"是否输出不确定
system("pause");
// 遇到endl,"hello world"和"hello bit"⼀定刷新缓冲区输出了
os << endl;
//os << flush;
//int i;
//cin >> i;
os << "hello cat";
system("pause");
}
int main()
{
ofstream ofs("test.txt");
// unitbuf设置后,ofs每次写都直接刷新
ofs << unitbuf;
// // cin绑定到ofs,cin进⾏读时,会刷新ofs的缓冲区
// cin.tie(&ofs);
func(ofs);
return 0;
}
注意:ofstream 就是用来向文件里面写内容的输出流,继承了ostream这个基类
我们先运行这个代码看一下结果:

在test.txt中的内容:

可以看出是空的,我们接着运行,如图所示:

此时test.txt中的内容:

任然是空的,那我们运行结束后呢?如图所示:

此时test.txt中的内容:

运行结束后,内容立马就出现了,这也验证了我们的刷新条件,那么其他的感兴趣的可以验证一下的。
这里补充几个建议:
在 io 需求比较高的地方,如部分大量输入的竞赛题中,加上以下几行代码可以提高C++IO 效率, 并且建议用**'\n' 替代 endl** ,因为endl 会刷新缓冲区,如代码所示:
cpp
// 关闭标准C++ 流是否与标准C 流在每次输⼊/输出操作后同步。
ios_base::sync_with_stdio(false);
// 解绑cin和cout关联绑定的其他流
cin.tie(nullptr);
cout.tie(nullptr);
这里解答几个我遇见的问题:
1.如果 test.txt 不存在,就新建一个。
2.如果 test.txt 已经存在,直接清空它原本的所有内容(文件大小瞬间变成 0 字节) ,然后从头开始写入新的内容,但有时会出现这样的问题:程序已经执行了 system("pause") 或 os << endl / flush,但文件内容仍未变化。这通常是因为文本编辑器没有刷新显示内容**,其实这是因为** 很多文本编辑器(特别是系统自带的记事本),如果你一直开着它不关,它是不会自动刷新 底层文件变化的,所以硬盘里的文件其实已经被你的 C++ 程序清空并重写 了,但记事本屏幕上显示的还是它几分钟前加载进内存里的旧画面,我们可以关掉当前的记事本/编辑器,重新双击打开 test.txt,看看内容是不是变了。
三.标准IO流
1.概念:
<1>.C++标准IO流默认是关联到控制台窗口的,cin是istream 类型全局对象,cout/cerr/clog是ostream类型的全局对象,内置类型这两个类都直接进行了重载实现,所以可以直接使用,自定义类型就需要我们自己重载运算符,如图所示:


<2>.ostream和istream是不支持拷贝的,只支持移动(外部不能使用,因为是保护成员)。
<3>.istream的cin对象支持转换为bool值,进行条件逻辑判断,⼀旦被设置了badbit或failbit标志位, 就返回false,如果是goodbit就返回true。
2.代码演示
cpp
#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main()
{
int i = 0, j = 1;
while (cin >> i >> j)
{
cout << i << ":" << j << endl;
}
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.bad() << endl;
cout << cin.fail() << endl << endl;
string s;
while (cin >> s)
{
cout << s << endl;
}
return 0;
}
解释:istream& operator>>(int i),>> 运算符重载的返回值是 istream 对象, istream 对象可以调用operator bool 转换为 bool 值。
如图所示: 
不过这种转换是显式类型转换(explicit),也是为了类型转换安全。
输出结果:
,我持续的输入,要结束需要输入Ctrl+Z 换行, Ctrl+Z 用于告诉程序输入已经完成,类似于在文件末尾添加⼀个标记,但是呢,本质在底层是将 cin 的 eofbit 和 failbit 标志位设置了,而 流一旦发生错误就不能再用了,清理重置⼀下就能再次使用了,仅需在string s;前加一个cin.clear();
四.string IO流
1.基本概念
<1>.ostringstream是string的写入流,把各种数据拼接成字符串 ,ostringstream是ostream的派生类;istringstream是string的读出流,把字符串当作数据源 ,istringstream是istream的派生类;stringstream是ostringstream和istringstream的派生类,既可以读也可以写,这里使用stringstream 会很方便,但是我们需要加个sstream这个头文件
<2>. stringstream系列底层维护了⼀个string类型的对象用来保存结果,使用方法跟上面的文件流类 似,只是数据读写交互的都是底层的string对象。
<3>.stringstream 最常用的方式是通过已经重载好的 >> 和 << 运算符进行数据读写 ,进行数据和string之间的IO转换。
2.代码演示
有关内置类型的转换:
cpp
#include<iostream>
#include<sstream>
#include<string>
using namespace std;
int main()
{
// 数字转字符串
ostringstream os;
int num = 123;
os << num;
string str = os.str();
cout << str << endl;
// 字符串转数字
istringstream is("520");
is >> num;
cout << "num=" << num << endl;
return 0;
}
注意:毕竟ostringstream与istringstream都是封装的类,都是可以构造与初始化的,而**os.str();**是
的作用是:把当前流缓冲区里所有的内容,打包成一个完整的 std::string 返回给string。
有关自定义类型的转换,加一个如下的代码:
cpp
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 << endl;
return out;
}
cpp
int main()
{
int i = 123;
Date d = { 2025, 4, 10 };
ostringstream oss;
oss << i << endl;
oss << d << endl;
string s = oss.str();
cout << s << endl;
return 0;
}
输出结果:
,这里解答一个问题:为什么会有三个空行?
答案:其实前面也说了,endl是**"\n"+刷新,会自动在结尾加一个"\n",我们向oss中输入的其实是:"123\n2025 4 10 \n\n",那么输出时就会有三个空行了。**
cpp
int main()
{
istringstream iss("100 2025 9 9");
int j;
Date x;
iss >> j >> x;
cout << j << endl;
cout << x << endl;
return 0;
}
输出结果:
,如果将x中的Date换成int其实并不会报错。
五.文件IO流
1.基本概念
<1>.ofstream是输出文件流 ,也就是写文件的流 ,ofstream是ostream的派生类;ifstream是输入文件流 ,也就是读文件的流,ifstream是istream的派生类;fstream是ifstream和ofstream的派生类, 既可以读也可以写。
<2>.文件流对象可以在构造时打开文件,也可以调用open函数打开⽂件,打开文件的mode有图中 的几种。in为读打开;out为写打开;binary以⼆进制模式打开;ate打开后立即寻位到流结尾; app每次写入前寻位到流结尾;trunc在打开时清除流的内容;这些值是ios_base中定义的成员变 量继承下来的,并且他们也是组合的独立⼆进制位值,需要组合时,可以按位或(|)到⼀起,他们之间的区别,具体参考下面的代码演示,如果你想要了解模拟文件打开模式标志位的代码,可以看如下的代码:
cpp
#include <iostream>
#include <string>
using namespace std;
class MyIO
{
public:
static const int in = 1 << 0; // 00001 - 读
static const int out = 1 << 1; // 00010 - 写
static const int trunc = 1 << 2; // 00100 - 截断(清空)
static const int app = 1 << 3; // 01000 - 追加
static const int ate = 1 << 4; // 10000 - 初始定位到末尾
};
// 模拟文件流类
class MyFileStream
{
public:
string content = "Hello World! Old Data..."; // 模拟硬盘上的原始文件
// 模拟 open 函数
void open(const string& filename, int mode) {
// 1. 处理 trunc (截断/清空)
// 规则:只有在允许写入(out)且指定了 trunc 时,才清空内容
if ((mode & MyIO::out) && (mode & MyIO::trunc)) {
}
// 2. 处理 app (追加)
// 规则:如果是追加模式,虽然指针初始在0,但逻辑上写操作会强制在末尾
if (mode & MyIO::app) {
}
// 3. 处理 ate (At The End)
// 规则:只要设置了 ate,打开那一刻指针就在末尾,但之后可以 seek 回去
else if (mode & MyIO::ate) {
}
// 4. 权限检查示例
if ((mode & MyIO::in) && (mode & MyIO::out)) {
}
else if (mode & MyIO::in) {
}
else if (mode & MyIO::out) {
}
}
};
int main() {
MyFileStream fs;
// 默认行为是 out | trunc
fs.open("test1.txt", MyIO::out | MyIO::trunc);
// 行为是 out | app
fs.open("test2.txt", MyIO::out | MyIO::app);
// 行为是 in | out | ate
fs.open("test3.txt", MyIO::in | MyIO::out | MyIO::ate);
return 0;
}
也可以看图理解:


<3>.文件流打开后如果需要可以主动调用close函数关闭 ,也可以不关闭,因为流对象析构函数中会关闭。
**<4>.**文件流打开文件失败或读写失败,也会使用IO流状态标记,我们调用operatorbool或operator!判断即可。
<5> ifstream文件流的读数据主要可以使用get/read/>>重载 ,ofstream文件流写数据主要可以使用put/write/<<重载,具体主要参考下面代码的演示。
2.代码演示
2.1.向文件里写数据
cpp
#include<iostream>
#include<fstream>
#include<string>
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();
return 0;
}
2.2.向文件里读数据
cpp
int main()
{
ifstream ifs("test.txt");
char ch;
while (ifs.get(ch))
{
cout << ch ;
}
ifs.close();
return 0;
}
2.3.打开文件
cpp
#include<iostream>
#include<fstream>
#include<string>
using namespace std;
int main()
{
ofstream ofs;
int x = 111;
double y = 1.11;
//app不能移动文件指针,永远是在文件尾写
ofs.open("test.txt", ios_base::out | ios_base::app);
ofs.seekp(0, ios_base::beg);
ofs << "1111111" << endl;
ofs << x << " " << y << endl;
// ate可以移动文件指针,写到其他位置
ofs.open("test.txt", ios_base::out | ios_base::ate);
ofs.seekp(0, ios_base::beg);
ofs << "1111111" << endl;
//虽然单个out就可以清除内容,但是 out|trunc更明确的表要清除内容的⾏为
//ofs.open("test.txt", ios_base::out);
ofs.open("test.txt", ios_base::out | ios_base::trunc);
ofs << "xxxx";
ofs.close();
return 0;
}
注意:ofs.seekp(0, ios_base::beg); 的作用是将文件流的写入指针(Put Pointer)移动到文件的开头位置,文件流的接口比较多,部分内容了解一下即可。
建议:C++的IO库的许多接口是不用学的,仅要按需学几个常用的即可。