IO流是我们C++中最常接触的东西,典型的就如同头文件<iostream>和输入cin>>和输出cout<<。但是之前的我们对于IO流了解并不深刻,本期我们就来深入了解这个常见又陌生的概念
作者的个人gitee:楼田莉子 (riko-lou-tian) - Gitee.com喜欢请点个赞谢谢
目录
[C++ 文件打开模式汇总表格](#C++ 文件打开模式汇总表格)
[C++文件IO流中 put 和 write 的本质区别](#C++文件IO流中 put 和 write 的本质区别)
[String IO流](#String IO流)
IO继承家族类
C++语⾔不直接处理输⼊输出,⽽是通过⼀族定义在标准库中的类型在处理IO。这些类型⽀持从设 备中读取数据和向设备中写⼊数据的IO操作,设备可以是⽂件、控制台窗⼝等。
到⽬前为⽌,我们已经使⽤过的IO类型和对象都是操纵char数据的,默认情况下这些对象都是关联 到⽤⼾的控制台窗⼝。但是实际中IO类不仅仅是从控制台窗⼝控制输⼊输出,还⽀持⽂件和string 类的IO操作。
其次IO类型使⽤模板实现的,还⽀持对wchar_t数据的输⼊输出。
通过下图可以看到C++IO类型设计的是⼀个继承家族,通过继承家族类解决控制台/⽂ 件/string的IO操作。
官方文档:输入/输出 - C++ 参考

IO流状态
IO操作的过程中,可能会发⽣各种错误,IO流对象中给了四种状态标识错误,可以参考下图2-1和 2-2进⾏理解。goodbit表⽰流没有错误/eofbit表⽰流到达⽂件结束/failbit表⽰IO操作失败 了/badbit表⽰流崩溃了出现了系统级错误。

⼀个常⻅的IO流错误是cin>>i,i是⼀个int类型的对象,如果我们在控制台输⼊⼀个字符,cin对象 的failbit状态位就会被设置,cin就进⼊错误状态,⼀个流⼀旦发⽣错误,后续的IO操作都会失败, 我们可以调⽤cin.clear()函数来恢复cin的状态为goodbit。
badbit表⽰系统级错误,如不可恢复的读写错误,通常情况下,badbit⼀旦被设置了,流就⽆法再 使⽤了。
failbit表⽰⼀个逻辑错误,如期望读取⼀个整形,但是却读取到⼀个字符,failbit被设置了,流是 可以恢复的,恢复以后可以继续使⽤。
如果到达⽂件结束位置eofbit和failbit都会被置位。如果想再次读取当前⽂件,可以恢复⼀下流的 状态,同时重置⼀个⽂件指针位置。
状态函数 | 检查的标志位 (ios_base::iostate) | 状态为 true (即被设置) 的含义 |
常见的触发场景 | 如何重置状态 |
---|---|---|---|---|
good() |
goodbit (表示无任何标志位被设置) |
流处于正常 、就绪 状态。 没有错误发生,可以正常进行IO操作。 | 流刚被初始化或打开时。 成功完成上一次IO操作后。 | 无需重置。当其他错误标志被清除后,good() 会自动变为 true 。 |
eof() |
eofbit (End-of-File) |
到达了文件末尾或输入流的结束。 注意 :仅 表示"读到了结尾",并不代表流本身有错误。 | 读取文件时,遇到文件结束符(如 Ctrl+Z on Windows, Ctrl+D on Unix)。 从 std::cin 或 ifstream 读取数据,输入已经耗尽。 |
调用 clear() 方法可以清除该状态,使流恢复为 good() 状态。 |
fail() |
failbit |
发生了可恢复的 IO操作失败。 通常是与数据格式相关的逻辑错误,但流本身未被破坏。 | 1. 类型不匹配 :试图将 "hello" 读入一个 int 变量。 2. 期望读入数据但遇到文件尾 (eofbit 通常也会同时被设置)。 |
调用 clear() 方法清除状态标志,然后通常需要忽略/丢弃 错误输入(如 cin.ignore() )再继续操作。 |
bad() |
badbit |
发生了不可恢复的 、严重的 错误。 通常与流的完整性有关,流本身可能已损坏。 | 1. 设备读写错误(如磁盘损坏、无法写入)。 2. 流使用的缓冲区发生严重错误(如内存分配失败)。 | 调用 clear() 方法可能可以恢复,但取决于错误的严重性。很多时候问题无法通过程序解决。 |

也可以⽤setstate和rdstate两个函数来控制流状态.eofbit/failbit/badbit/goodbit是ios_base基类 中定义的静态成员变量,可以直接使⽤的,并且是他们是可以组合的位运算值,具体使⽤细节可以 参考⽂档。
cpp
#include <iostream>
using namespace std;
int main()
{
//cin四个状态
cout << cin.good() << endl;
cout<< cin.fail() << endl;
cout << cin.eof() << endl;
cout << cin.bad() << endl;
int i = 0;
// 输入一个字符或多个字符,cin读取失败,流状态被标记为failbit
cin >> i;
cout << i << endl;
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.bad() << endl;
cout << cin.fail() << endl << endl;
//当输入失败的时候cin也就失效了,无法接着进行输入
//cin.clear();
//即使clear了也只是把输入从缓冲区中指出,并不影响流的状态,
// 再次读入也会失败
if (cin.fail())
{
// clear可以恢复流状态位goodbit
cin.clear();
// 我们还要把缓冲区中的多个字符都读出来,读到数字停下来,
// 否则再去cin >> i还是会失败
char ch = cin.peek();
while (!(ch >= '0' && ch <= '9'))
{
ch = cin.get();
cout << ch;
ch = cin.peek();
}
cout << endl;
}
int x = 0;
cin >> x;
cout << x << endl;
return 0;
}
管理缓冲区
任何输出流都管理着⼀个缓冲区,⽤来保存程序写的数据
当我们执行
cpp
os<<"hello world";
字符串可能⽴即输出,也可能被操作系统保存在缓冲区中,随后再输出。
有了缓冲区机 制,操作系统就可能将多个输出操作组合成为⼀个单⼀的系统级写操作。因为设备的写操作通常是 很耗时的,允许操作系统将多个输出操作组合为单⼀的设备写操作肯可能带来很⼤的性能提升。
会触发缓冲区刷新,将数据真正的写到输出设备或⽂件的原因有很多,如:
<1>程序正常结束;
<2>缓冲区满了;
<3>输出了操纵符endl或flush会⽴即刷新缓冲区
<4>我们使⽤了操纵符unitbuf设置 流的内部状态,来清空缓冲区,cerr就设置了unitbuf,所以cerr输出都是⽴即刷新的。
<5>⼀个输 出流关联到另⼀个流时,当这个流读写时,输出流会⽴即刷新缓冲区。
例如默认情况下cerr和cin都 被关联到cout,所以读cin或写cerr时,都会导致cout缓冲区会被⽴即刷新。
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";
// "hello cat"是否输出不确定
system("pause");
}
int main()
{
//test1();
// 正确创建文件输出流对象
ofstream ofs("test.txt");
// unitbuf设置后, ofs 每次写都直接刷新
// ofs << unitbuf;
// cin绑定到ofs,cin进⾏读时,会刷新ofs的缓冲区
// cin.tie(&ofs);
// 将文件流传递给函数
func(ofs);
// 关闭文件流
//ofs.close();
return 0;
}
tie可以⽀持跟其他流绑定和解绑,具体参考⽂档:ios::tie - C++ 参考
unitbuf的官方文档:unitbuf - C++ 参考
在io需求⽐较⾼的地⽅,如部分⼤量输⼊的竞赛题中,加上以下⼏⾏代码可以提⾼ C++IO效率,并且建议⽤'\n'替代endl,因为endl会刷新缓冲区。
关闭标准C++流是否与标准C流在每次输⼊输出操作后同步。
cpp
ios_base::sync_with_stdio(false);
关闭同步后,以下程序可能顺序为b a c
cpp
// 关闭同步后,以下程序可能顺序为b a c
std::cout << "a\n";
std::printf("b\n");
std::cout << "c\n";
解绑cin和cout关联绑定的其他流的示例
cpp
// 解绑cin和cout关联绑定的其他流
cin.tie(nullptr);
cout.tie(nullptr);
标准IO流
C++标准IO流前⾯已经使⽤得⽐较多了,C++标准IO流默认是关联到控制台窗⼝的。cin是istream 类型全局对象,cout/cerr/clog是ostream类型的全局对象,内置类型这两个类都直接进⾏了重载实 现,所以可以直接使⽤,⾃定义类型就需要我们⾃⼰重载>运算符。
ostream和istream是不⽀持拷⻉的,只⽀持移动(外部不能使⽤,因为是保护成员)。
istream的cin对象⽀持转换为bool值,进⾏条件逻辑判断,⼀旦被设置了badbit或failbit标志位, 就返回false,如果是goodbit就返回true。
ostream和istream还有不少其他接⼝,实践中相对⽤得⽐较少,需要时⼤家查查⽂档。
cpp
#include <iostream>
#include <fstream>
#include <string>
#include <iomanip>
using namespace std;
int main()
{
int i = 0, j = 1;
// 持续的输入,要结束需要输入Ctrl + Z换⾏,
// Ctrl + Z用于告诉程序输入已经完成,类似于在文件末尾添加⼀个标记。
// istream& operator>>(int i),>>算符重载的返回值是istream对象,
// istream对象可以调用operator bool转换为bool值
// 本质在底层是将cin的eofbit和failbit标志位设置了,
// cin调用operator bool函数语法逻辑上实现转换为bool值
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;
// 流⼀旦发⽣错误就不能再⽤了,清理重置⼀下再能使⽤
cin.clear();
string s;
while (cin >> s)
{
cout << s << endl;
}
return 0;
}
文件IO流
ofstream是输出⽂件流,也就是写⽂件的流,ofstream是ostream的派⽣类;ifstream是输⼊⽂件 流,也就是读⽂件的流,ifstream是istream的派⽣类;fstream是ifstream和ofstream的派⽣类, 既可以读也可以写。
ofstream官方文档:ofstream - C++ 参考
ifstream官方文档:ifstream - C++ 参考
⽂件流对象可以在构造时打开⽂件,也可以调⽤open函数打开⽂件,打开⽂件的mode有下图中的⼏种。in为读打开;out为写打开;binary以⼆进制模式打开;ate打开后⽴即寻位到流结尾(打开后立即寻址到尾部在尾部写); app每次写⼊前寻位到流结尾(总是在尾部写);trunc在打开时舍弃流的内容。
C++ 文件打开模式汇总表格
模式标志 | 功能描述 | 详细说明 |
---|---|---|
in | 读取模式打开 | 打开文件用于读取操作,内部流缓冲区支持输入操作 |
out | 写入模式打开 | 打开文件用于写入操作,内部流缓冲区支持输出操作 |
binary | 二进制模式打开 | 以二进制模式而非文本模式进行文件操作,避免字符转换 |
ate | 初始位置到文件尾 | 打开后立即将文件指针定位到文件末尾 |
app | 追加模式 | 每次写入操作前自动定位到文件末尾,保证数据追加 |
trunc | 截断模式 | 打开文件时清空已有内容,文件大小变为0 |
C++文件IO流中 put 和 write 的本质区别
特性 | put |
write |
---|---|---|
操作单位 | 单个字符 | 数据块 |
参数类型 | char 或字符 |
const char* 和字节数 |
主要用途 | 输出单个字符 | 输出二进制数据或字符串块 |
效率 | 适合单个字符输出 | 适合批量数据输出,效率更高 |
类型安全 | 类型安全 | 无类型检查,直接内存操作 |
seekp函数
seekp
用于设置输出文件流(ostream)的写入位置指针
cpp
// 设置绝对位置
ostream& seekp(streampos pos);
// 设置相对位置
ostream& seekp(streamoff off, ios_base::seekdir dir);
基准位置常量
方向常量 | 值 | 说明 |
---|---|---|
ios::beg |
0 | 从流开头开始 |
ios::cur |
1 | 从当前位置开始 |
ios::end |
2 | 从流末尾开始 |
应用举例:定位
cpp
#include <fstream>
using namespace std;
int main() {
ofstream file("data.txt");
// 1. 绝对定位:从开头算起
file.seekp(10); // 定位到第10字节
// 2. 相对定位:从当前位置
file.seekp(5, ios::cur); // 向前移动5字节
// 3. 相对定位:从末尾
file.seekp(-3, ios::end); // 定位到倒数第3字节
file << "END";
file.close();
return 0;
}
随机访问文件
cpp
#include <fstream>
#include <iostream>
using namespace std;
struct Record {
int id;
char name[20];
};
int main() {
fstream file("records.dat", ios::binary | ios::in | ios::out);
// 写入多个记录
Record rec1 = {1, "Alice"};
Record rec2 = {2, "Bob"};
Record rec3 = {3, "Charlie"};
file.write(reinterpret_cast<char*>(&rec1), sizeof(Record));
file.write(reinterpret_cast<char*>(&rec2), sizeof(Record));
file.write(reinterpret_cast<char*>(&rec3), sizeof(Record));
// 随机修改第二个记录
Record updated = {2, "Robert"};
file.seekp(sizeof(Record)); // 跳到第二个记录位置
file.write(reinterpret_cast<char*>(&updated), sizeof(Record));
file.close();
return 0;
}
相关函数
函数 | 作用 | 适用流类型 |
---|---|---|
seekp |
设置写入位置 | 输出流(ofstream, fstream) |
seekg |
设置读取位置 | 输入流(ifstream, fstream) |
tellp |
获取当前写入位置 | 输出流 |
tellg |
获取当前读取位置 | 输入流 |
配合使用案例:
cpp
#include <fstream>
using namespace std;
int main() {
fstream file("test.txt", ios::in | ios::out);
// 获取当前写入位置
streampos pos = file.tellp();
cout << "当前位置: " << pos << endl;
// 移动到指定位置写入
file.seekp(100);
file << "插入的数据";
// 回到原位置
file.seekp(pos);
file.close();
return 0;
}
这些值是ios_base中定义的成员变 量继承下来的,并且他们也是组合的独⽴⼆进制位值,需要组合时,可以或到⼀起。他们之间的区 别,具体参考下⾯的代码演⽰。
cpp
void test5()
{
ofstream ofs("tstt.txt");
// 字符和字符串的写
//put 是面向字符的类型安全操作,write 是面向字节的原始内存操作。
/*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();
//// app和ate都是尾部追加,
// 不同的是app不能移动⽂件指针,永远是在⽂件尾写
//ate可以移动⽂件指针,写到其他位置
ofs.open("tstt.txt", ios_base::out | ios_base::app);
ofs << "1111111" << endl;
ofs.seekp(0, ios_base::beg);
ofs << x << " " << y << endl;
ofs.close();
ofs.open("tstt.txt", ios_base::out | ios_base::ate);
ofs << "1111111" << endl;
ofs.seekp(0, ios_base::beg);
ofs << x << " " << y << endl;
ofs.close();
// out和out | trunc都会先把数据清掉,再写数据(官⽅⽂档也明确是这样写的)
// https://en.cppreference.com/w/cpp/io/basic_filebuf/open
// 那么 trunc存在的意义是什么呢?
//out | trunc更明确的表达了⽂件中有内容时要清除掉内 容
//对于代码维护者和阅读者来说能清晰地理解这个⾏为,在⼀些复杂的⽂件系统环境或不同的
// C++⽂件流实现库中, out⾏为不完全等同于截断内容的情况(虽然当前主流实现基本⼀致),
// out|trunc更明确的表要清除内容的⾏为
ofs.open("test.txt", ios_base::out);
//ofs.open("test.txt", ios_base::out | ios_base::trunc);
ofs << "xxxx";
ofs.close();
}
void test6()
{
// 实现⼀个图⽚⽂件的复制,需要⽤⼆进制⽅式打开读写,第⼀个参数可以给⽂件的绝对路径
ifstream ifs("C:\\Users\\15699\\Pictures\\P站\\97512055_p0_master1200.jpg",
ios_base::in | ios_base::binary);
ofstream ofs("C:\\Users\\15699\\Pictures\\P站\\97512055_p0_master1200.jpg",
ios_base::out | ios_base::binary);
int n = 0;
while (ifs && ofs)
{
char ch = ifs.get();
ofs << ch;
++n;
}
cout << n << endl;
}
如果每次挪动指针用ate,不挪动指针每次尾部写用app
⽂件流打开后如果需要可以主动调⽤close函数关闭,也可以不关闭,因为流对象析构函数中会关 闭。
⽂件流打开⽂件失败或读写失败,也会使⽤IO流状态标记,我们调⽤operatorbool或operator!判 断即可。
ifstream⽂件流的读数据主要可以使⽤get/read/>>重载,ofstream⽂件流写数据主要可以使⽤ put/write/<<重载,具体主要参考下⾯代码的演⽰。
相⽐c语⾔⽂件读写的接⼝,C++fstream流功能更强⼤⽅便,使⽤>进⾏⽂件读写很⽅便,尤 其是针对⾃定义类型对象的读写。
String IO流
ostringstream是string的的写⼊流,ostringstream是ostream的派⽣类;istringstream是string的 的读出流,istringstream是istream的派⽣类;stringstream是ostringstream和istringstream的派 ⽣类,既可以读也可以写。这⾥使⽤stringstream会很⽅便。 • stringstream系列底层维护了⼀个string类型的对象⽤来保存结果,使⽤⽅法跟上⾯的⽂件流类 似,只是数据读写交互的都是底层的string对象。
stringstream最常⽤的⽅式还是使⽤>重载,进⾏数据和string之间的IO转换。
string流使⽤str函数获取底层的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;
}
struct ChatInfo
{
string _name; // 名字
int _id;
// id
Date _date; // 时间
string _msg; // 聊天信息
};
void test9()
{
// 结构信息序列化为字符串
ChatInfo winfo = { "张三", 135246, { 2022, 4, 10 }, "晚上一起看电影吧" };
ostringstream oss;
oss << winfo._name << " " << winfo._id << " " << winfo._date
<< " " <<winfo._msg;
string str = oss.str();
cout << str << endl << endl;
// 我们通过⽹络这个字符串发送给对象,实际开发中,信息相对更复杂,
//⼀般会选⽤Json、 xml等⽅式进⾏更好的⽀持
// 字符串解析成结构信息
ChatInfo rInfo;
istringstream iss(str);
iss >> rInfo._name >> rInfo._id >> rInfo._date >> rInfo._msg;
cout << "-------------------------------------------------------" << endl;
cout << "姓名:" << rInfo._name << "(" << rInfo._id << ") ";
cout << rInfo._date << endl;
cout << rInfo._name << ":>" << rInfo._msg << endl;
cout << "-------------------------------------------------------" << endl;
}
本期关于IO流相关的介绍到这里就结束了喜欢请点个赞支持一下谢谢
封面图自取:
