【C++笔记】C++IO流的深度剖析
【C++笔记】C++IO流的深度剖析
🔥个人主页:大白的编程日记
🔥专栏:C++笔记
文章目录
- 【C++笔记】C++IO流的深度剖析
- 前言
- 一.I0库
- 1.I0继承家族类
- 2.I0流状态
- 3.管理输出缓冲区
- 4.标准IO流
- 5.文件IO流
- 6. string IO流
- 后言
前言
哈喽,各位小伙伴大家好!上期我们讲了模版的特化及其编译分离。今天我们来讲一下C++IO流的深度剖析。话不多说,我们进入正题!向大厂冲锋
一.I0库
1.I0继承家族类
- C++语言不直接处理输入输出,而是通过一族定义在标准库中的类型在处理IO。这些类型支持从设备中读取数据和向设备中写入数据的IO操作,设备可以是文件、控制台窗口等。
- 到目前为止,我们已经使用过的IO类型和对象都是操纵char数据的,默认情况下这些对象都是关联到用户的控制台窗口。但是实际中IO类不仅仅是从控制台窗口控制输入输出,还支持文件和string类的IO操作。其次IO类型使用模板实现的,还支持对wchar_t数据的输入输出。
- 通过下图1-1和1-2可以看到C++I0类型设计的是一个继承家族,通过继承家族类解决控制台/文件/string的IO操作。
https://legacy.cplusplus.com/reference/
https://zh.cppreference.com/w/cpp/io
2.I0流状态
-
I0操作的过程中,可能会发生各种错误,10流对象中给了四种状态标识错误,可以参考下图2-1和2-2进行理解。goodbit表示流没有错误/eofbit表示流到达文件结束/failbit表示IO操作失败了/badbit表示流崩溃了出现了系统级错误。
-
一个常见的lO流错误是cin>>i,i是一个int类型的对象,如果我们在控制台输入一个字符,cin对象的failbit状态位就会被设置,cin就进入错误状态,一个流一旦发生错误,后续的IO操作都会失败,我们可以调用cin.clear()函数来恢复cin的状态为goodbit。
-
badbit表示系统级错误,如不可恢复的读写错误,通常情况下,badbit一旦被设置了,流就无法再使用了。
-
failbit表示一个逻辑错误,如期望读取一个整形,但是却读取到一个字符,failbit被设置了,流是可以恢复的,恢复以后可以继续使用。
-
如果到达文件结束位置eofbit和failbit都会被置位。如果想再次读取当前文件,可以恢复一下流的
状态,同时重置一个文件指针位置。 -
goodbit表示流未发生错误。
-
也可以用setstate和rdstate两个函数来控制流状态,eofbit/failbit/badbit/goodbit是ios_base基类中定义的静态成员变量,可以直接使用的,并且是他们是可以组合的位运算值,具体使用细节可以参考文档。
#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读取失败,流状态被标记failbit
cin >> i;
cout << i << endl;
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.bad() << endl; cout << cin.fail() << endl << endl;
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;
}
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.bad() << endl;
cout << cin.fail() << endl << endl;
cin >> i;
cout << i << endl;
return 0;
}
3.管理输出缓冲区
- 任何输出流都管理着一个缓冲区,用来保存程序写的数据。如果我们执行os<<“helloworld”;字符串可能立即输出,也可能被操作系统保存在缓冲区中,随后再输出。有了缓冲区机制,操作系统就可能将多个输出操作组合成为一个单一的系统级写操作。因为设备的写操作通常是很耗时的,允许操作系统将多个输出操作组合为单一的设备写操作肯可能带来很大的性能提升。
- 会触发缓冲区刷新,将数据真正的写到输出设备或文件的原因有很多,如:<1>程序正常结束;<2>缓冲区满了;<3>输出了操纵符endl或flush会立即刷新缓冲区<4>我们使用了操纵符unitbuf设置流的内部状态,来清空缓冲区,cerr就设置了unitbuf,所以cerr输出都是立即刷新的。<5>一个输出流关联到另一个流时,当这个流读写时,输出流会立即刷新缓冲区。例如默认情况下cerr和cin都被关联到cout,所以读cin或写cerr时,都会导致cout缓冲区会被立即刷新。
- tie可以支持跟其他流绑定和解绑,具体参考文档
https://legacy.cplusplus.com/reference/ios/ios/tie/
void Prin()
{
FILE* file;
char buffer[1024]; // 假设每行数据不超过1024个字符
// 打开文件,"r" 表示以只读方式打开
file = fopen("test.txt", "r");
if (file == NULL) {
perror("Error opening file");
return ;
}
// 读取并打印文件内容
while (fgets(buffer, sizeof(buffer), file)) {
printf("%s", buffer);
}
// 关闭文件
fclose(file);
}
void func(ostream& os)
{
os << "hello world";
os << "hello bit";
// "hello world"和"hello bit"是否输出不确定
system("pause");
// 遇到endl,"hello world"和"hello bit"⼀定刷新缓冲区输出了
//os << endl;
int i;
cin >> i;
Prin();
cin.tie(nullptr);
os << "hello cat";
cin >> i;
Prin();
//os << flush;
//int i;
//cin >> i;
// "hello cat"是否输出不确定
system("pause");
}
int main()
{
ofstream ofs("test.txt");
//cin绑定到ofs,cin进⾏读时,会刷新ofs的缓冲区
cin.tie(&ofs);
func(ofs);
//func(cout);
//unitbuf设置后,ofs每次写都直接刷新
return 0;
}
int main()
{
//在io需求比较高的地方,如部分大量输入的竞赛题中,加上以下几行代码可以提高C++IO效率
//并且建议用'n‘替代endl,因为endl会刷新缓冲区
//关闭标准C++流是否与标准C流在每次输入/输出操作后同步。
ios_base::sync_with_stdio(false);
//关闭同步后,以下程序可能顺序为bac
std::cout << "a\n";
std::printf("b\n");
std::cout << "c\n";
//解绑cin和cout关联绑定的其他流
cin.tie(nullptr);
cout.tie(nullptr);
return 0;
}
4.标准IO流
- C++标准IO流前面已经使用得比较多了,C++标准IO流默认是关联到控制台窗口的。cin是istream类型全局对象,cout/cerr/clog是ostream类型的全局对象,内置类型这两个类都直接进行了重载实现,所以可以直接使用,自定义类型就需要我们自己重载<<和>>运算符。
- ostream和istream是不支持拷贝的,只支持移动(外部不能使用,因为是保护成员)。
- istream的cin对象支持转换为bool值,进行条件逻辑判断,一旦被设置了badbit或failbit标志位,
就返回false,如果是goodbit就返回true。 - ostream和istream还有不少其他接口,实践中相对用得比较少,需要时大家查查文档。
5.文件IO流
- ofstream是输出文件流,也就是写文件的流,ofstream是ostream的派生类;ifstream是输入文件
流,也就是读文件的流,ifstream是istream的派生类;fstream是ifstream和ofstream的派生类,
既可以读也可以写。
https://legacy.cplusplus.com/reference/fstream/
https://zh.cppreference.com/w/cpp/io - 文件流对象可以在构造时打开文件,也可以调用open函数打开文件,打开文件的mode有5-1图中的几种。in为读打开;out为写打开;binary以二进制模式打开;ate打开后立即寻位到流结尾;app每次写入前寻位到流结尾;trunc在打开时舍弃流的内容;这些值是ios_base中定义的成员变量继承下来的,并且他们也是组合的独立二进制位值,需要组合时,可以或到一起。他们之间的区别,具体参考下面的代码演示。
- 文件流打开后如果需要可以主动调用close函数关闭,也可以不关闭,因为流对象析构函数中会关闭。
- 文件流打开文件失败或读写失败,也会使用lO流状态标记,我们调用operator bool或operator!判断即可。
- ifstream文件流的读数据主要可以使用get/read/>>重载,ofstream文件流写数据主要可以使用put/write/<<重载,具体主要参考下面代码的演示。
- 相比c语言文件读写的接口,C++fstream流功能更强大方便,使用<<和>>进行文件读写很方便,尤其是针对自定义类型对象的读写。
#include <iostream>
#include <thread>
#include<iostream>
#include<fstream>
#include<string>
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调用operatorbool函数语法逻辑上实现转换为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;
}
}
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 ServerInfo
{
// ⼆进制读写时,这⾥不能⽤string,否则写到⽂件中的是string中指向字符数组的指针
// 若string对象析构后再去⽂件中读取string对象,string中读到是⼀个野指针。
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);
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.1111111111111111111111", 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;
}
6. string IO流
- ostringstream是string的的写入流,ostringstream是ostream的派生类;istringstream是string的
的读出流,istringstream是istream的派生类;stringstream是ostringstream和istringstream的派
生类,既可以读也可以写。这里使用stringstream会很方便。 - stringstream系列底层维护了一个string类型的对象用来保存结果,使用方法跟上面的文件流类
似,只是数据读写交互的都是底层的string对象。 - stringstream最常用的方式还是使用<<和>>重载,进行数据和string之间的IO转换。
- string流使用str函数获取底层的string对象,或者写入底层的string对象,具体细节参考下面代码理解。
#include<iostream>
#include<sstream>
#include<string>
using namespace std;
int main()
{
//int i = 123;
//Date d = { 2025, 4, 10 };
//stringstream oss;
//oss << i << endl;
//oss << d << endl;
//string s = oss.str();
//cout << s << endl;
//stringstream iss("100 2025 9 9");
iss.str("100 2025 9 9");
//int j;
Date x;
//iss >> j >> x;
//cout << j << endl;
//cout << x << endl;
int a = 1234;
int b = 5678;
string str;// 将⼀个整形变量转化为字符串,存储到stri类对象中
stringstream ss;
ss << a << b;
ss >> str;
ss.clear();
ss.str("");
double dd = 12.34;
ss << dd;
ss >> str;
cout << str << endl;
//cout << ss.fail() << endl;
//cout << ss.bad() << endl;
// 注意多次转换时,必须使⽤clear将上次转换状态清空掉
// stringstreams在转换结尾时(即最后⼀个转换后),会将其内部状态设置为badbit和failbit
// 因此下⼀次转换是必须调⽤clear()将状态重置为goodbit才可以转换
// 但是clear()不会将stringstreams底层字符串清空掉, str给⼀个空串可以清掉底层的字符串
return 0;
}
#include<iostream>
#include<sstream>
#include<string>
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; // 聊天信息
};
int main()
{
//结构信息序列化为字符串
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;
return 0;
}
后言
这就是C++IO流的深度剖析。大家自己好好消化!今天就分享到这!感谢各位的耐心垂阅!咱们下期见!拜拜~