流的概念
"流"即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据(其单位可以是bit,byte,packet)的抽象描述。
C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象的比喻为"流"。
它的特性是:有序连续、具有方向性
为了实现这种流动,C++定义了I/O标准类库,这些每个类都称为流/流类,用以完成某方面的功能
C++的IO流

C++的标准流
可以看到C++提供了四个全局的标准流对象:cin、cout、cerr、clog
- cin进行标准输入即数据通过键盘输入到程序中
- cout进行标准输出,即数据从内存流向控制台(显示器)
- cerr用来进行标准错误的输出
- clog进行日志的输出
注意:
- cin为缓冲流。键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,会留在那儿慢慢用,如果输入错了,必须在回车之前修改,如果回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据。
- 输入的数据类型必须与要提取的数据类型一致,否则出错。出错只是在流的状态字state中对应位置位置,程序继续。
- 空格和回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用cin输入,字符串中也不能有空格。回车符也无法读入。
- cin和cout可以直接输入和输出内置类型数据,原因:标准库已经将所有内置类型的输入和输出全部重载了。
- 对于自定义类型,如果要支持cin和cout的标准输入输出,需要对<<和>>进行重载。
cin的返回值
在我们刷题遇到多组输入的时候通常会这样写:
cpp
while(cin>>...)
{
//...
}
那么有没有考虑过这是如何实现的呢?
首先我们看看cin的返回值是什么:

cin是istream的对象,调用的自然是istream的流提出重载,可以看到返回的是cin的引用。

可以看到,istream还重载了bool(),也就是cin本身是可以隐式类型转换成bool的:
cpp
int main()
{
cout << static_cast<bool>(cin) << endl;
return 0;
}
1
初始默认是true。那么什么时候会转换成false呢?来看看cin有多少种状态标志位 :

这意味着,cin会管理着一个一个iostate成员,这个成员的不同位置被赋予了不同的意义,先来看看哥哥标志位值是多少:
cpp
int main()
{
cout << cin.rdstate() << endl;
cout << ios_base::goodbit << endl;
cout << ios_base::eofbit << endl;
cout << ios_base::failbit << endl;
cout << ios_base::badbit << endl;
return 0;
}
0
0
1
2
4
从图片可以看到,当cin读取到文件结尾,eofbit就会标上,读取到逻辑错误failbit就会标上,读取到硬件损坏badbit就会标上。然后什么都没有发生就是goodbit。
不同标志位逻辑对应后面五种函数的返回值,operator bool的逻辑则是:

当返回!(fail()|bad()).
因此我们在cin的时候大概只会引起good()和fail()的改变:
cpp
int main()
{
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.fail() << endl;
cout << cin.bad() << endl << endl;
int x;
while (cin >> x)
{
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.fail() << endl;
cout << cin.bad() << endl << endl;
}
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.fail() << endl;
cout << cin.bad() << endl << endl;
cin >> x;
return 0;
}
1
0
0
0
12
1
0
0
0
x
0
0
1
0
可以看到正常输入整型,不会有任何改变。但如果输入x这种非整型,failbit就会标记上。此外还污染了缓冲区,导致最下面那行cin>>x也读取不到。
如果想恢复cin的iostate,可以使用clear函数:
cpp
int main()
{
int x;
while (cin >> x)
{
}
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.fail() << endl;
cout << cin.bad() << endl << endl;
cin.clear();
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.fail() << endl;
cout << cin.bad() << endl << endl;
cin >> x;
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.fail() << endl;
cout << cin.bad() << endl << endl;
return 0;
}
x
0
0
1
0
1
0
0
0
0
0
1
0
但是很遗憾,由于缓冲区还是污染状态,所以再次读取还是会发生错误,因此我们还需要清理缓冲区:
cpp
int main()
{
int x;
while (cin >> x)
{
}
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.fail() << endl;
cout << cin.bad() << endl << endl;
cin.clear();
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.fail() << endl;
cout << cin.bad() << endl << endl;
cin.get();
cin >> x;
cout << cin.good() << endl;
cout << cin.eof() << endl;
cout << cin.fail() << endl;
cout << cin.bad() << endl << endl;
return 0;
}
x
0
0
1
0
1
0
0
0
12
1
0
0
0
注意看,用了cin.get()提取了错误的字符。
C++的文件流
C++根据文件内容的数据格式分为二进制文件 和文本文件。采用文件流对象操作文件的一般步骤:
- 定义一个文件流对象
- ifstream ifile(只输入用)
- ofstream ofile(只输出用)
- fstream iofile(既输入又输出用)
- 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系
- 使用提取 和插入运算符对文件进行读写操作,或使用成员函数进行读写
- 关闭文件
也就是说整体上我们对文件分二进制操作和文本操作,来各自演示一下吧。首先先定义一个结构体:
cpp
struct ServerInfo
{
char _ip[32];
int _port;
Date _date;
};
这里的Date在以往文章已经实现了,并且重载了<<和>>.
再定义一个ConfigManager类:
cpp
struct ConfigManager
{
public:
ConfigManager(const char* filename = "test.txt")
:_filename(filename)
{}
void WriteBin(const ServerInfo& info)
{
ofstream ofs(_filename, ios_base::out | ios_base::binary);
ofs.write((const char*)&info, sizeof(ServerInfo));
}
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._ip << " " << info._port << " " << info._date;
}
void ReadText(ServerInfo& info)
{
ifstream ifs(_filename);
ifs >> info._ip >> info._port >> info._date;
}
private:
string _filename; // 配置文件
};
二进制读写
来尝试二进制写:
cpp
int main()
{
ServerInfo winfo = { "192.0.0.1", 80, { 2022, 4, 10 } };
// 二进制读写
ConfigManager cf_bin("test.bin");
cf_bin.WriteBin(winfo);
return 0;
}

可以看到写到文件里的二进制文件是一堆乱码,看来只有计算机知道自己写的是什么了,尝试读一下看看能否识别吧:
cpp
int main()
{
ServerInfo rbinfo;
ConfigManager cf_bin("test.bin");
cf_bin.ReadBin(rbinfo);
cout << rbinfo._ip << endl;
cout << rbinfo._port << endl;
cout << rbinfo._date << endl;
return 0;
}
192.0.0.1
80
2022 4 10
读的没毛病哦。二进制读写还是十分简单粗暴的。但实际上还是有点缺陷的,比如我们将ServerInfo的_ip类型改为string:
cpp
struct ServerInfo
{
//char _ip[32];
string _ip;
int _port;
Date _date;
};
重新读写一次会发生什么呢?写的时候自然没有问题,读的时候可就不一定了:

可以看到程序直接报错了。仔细想想是为什么呢?
问题肯定出在了string,二进制写是粗暴的浅拷贝。但我们的string浅拷贝的可是一个地址,这时候再读出来可就是野指针了。
文本读写
相比较二进制读写,文本读写其实更加方便,这是由于C++有>>和<<操作符.来试行一下吧:
cpp
int main()
{
ServerInfo winfo = { "192.0.0.1xxxxxxxxxxxx", 80, { 2022, 4, 10 } };
// 文本读写
ConfigManager cf_text("test.text");
cf_text.WriteText(winfo);
return 0;
}

可以看到文本写将所有流都转化成了字符流,因此在文本中我们也是能看到的哦.
cpp
int main()
{
ConfigManager cf_text("test.text");
ServerInfo rtinfo;
cf_text.ReadText(rtinfo);
cout << rtinfo._ip << " " << rtinfo._port << " " << rtinfo._date << endl;
return 0;
}
192.0.0.1xxxxxxxxxxxx 80 2022 4 10
非常完美不是么。而且使用起来也很方便。
C++字符串流
C++的字符串流可以将数据都转化成字符串传输。来看看具体怎么使用吧。首先定义一个ChatInfo结构体:
cpp
struct ChatInfo
{
string _name; // 名字
int _id; // id
Date _date; // 时间
string _msg; // 聊天信息
};
cpp
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;
// 字符串解析成结构信息
ChatInfo rInfo;
istringstream iss;
iss.str(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;
}
张三 135246 2022 4 10 晚上一起看电影吧
-------------------------------------------------------
姓名:张三(135246) 2022 4 10
张三:>晚上一起看电影吧
-------------------------------------------------------
可以看到确实是转换成了字符串进行传输。而且实际使用也不用分ostringstream 和 istringstream那么麻烦,stringstream类具备这两个类的功能:
cpp
int main()
{
// 结构信息序列化为字符串
ChatInfo winfo = { "张三", 135246, { 2022, 4, 10 }, "晚上一起看电影吧" };
stringstream oss;
oss << winfo._name << " " << winfo._id << " " << winfo._date << " " << winfo._msg;
string str = oss.str();
cout << str << endl << endl;
// 字符串解析成结构信息
ChatInfo rInfo;
stringstream iss;
iss.str(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;
}
张三 135246 2022 4 10 晚上一起看电影吧
-------------------------------------------------------
姓名:张三(135246) 2022 4 10
张三:>晚上一起看电影吧
-------------------------------------------------------