C++IO流

流的概念

"流"即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据(其单位可以是bit,byte,packet)的抽象描述。
C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象的比喻为"流"。

它的特性是:有序连续、具有方向性

为了实现这种流动,C++定义了I/O标准类库,这些每个类都称为流/流类,用以完成某方面的功能

C++的IO流

C++的标准流

可以看到C++提供了四个全局的标准流对象:cin、cout、cerr、clog

  • cin进行标准输入即数据通过键盘输入到程序中
  • cout进行标准输出,即数据从内存流向控制台(显示器)
  • cerr用来进行标准错误的输出
  • clog进行日志的输出

注意:

  1. cin为缓冲流。键盘输入的数据保存在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,会留在那儿慢慢用,如果输入错了,必须在回车之前修改,如果回车键按下就无法挽回了。只有把输入缓冲区中的数据取完后,才要求输入新的数据。
  2. 输入的数据类型必须与要提取的数据类型一致,否则出错。出错只是在流的状态字state中对应位置位置,程序继续。
  3. 空格和回车都可以作为数据之间的分格符,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用cin输入,字符串中也不能有空格。回车符也无法读入。
  4. cin和cout可以直接输入和输出内置类型数据,原因:标准库已经将所有内置类型的输入和输出全部重载了。
  5. 对于自定义类型,如果要支持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++根据文件内容的数据格式分为二进制文件文本文件。采用文件流对象操作文件的一般步骤:

  1. 定义一个文件流对象
  • ifstream ifile(只输入用)
  • ofstream ofile(只输出用)
  • fstream iofile(既输入又输出用)
  1. 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系
  2. 使用提取插入运算符对文件进行读写操作,或使用成员函数进行读写
  3. 关闭文件

也就是说整体上我们对文件分二进制操作和文本操作,来各自演示一下吧。首先先定义一个结构体:

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
张三:>晚上一起看电影吧
-------------------------------------------------------
相关推荐
草莓熊Lotso1 小时前
C++ STL map 系列全方位解析:从基础使用到实战进阶
java·开发语言·c++·人工智能·经验分享·网络协议·everything
草莓熊Lotso1 小时前
《算法闯关指南:优选算法--模拟》--41.Z 字形变换,42.外观数列
开发语言·c++·算法
AA陈超7 小时前
ASC学习笔记0020:用于定义角色或Actor的默认属性值
c++·笔记·学习·ue5·虚幻引擎
coderxiaohan8 小时前
【C++】仿函数 + 模板进阶
开发语言·c++
思成不止于此10 小时前
深入理解 C++ 多态:从概念到实现的完整解析
开发语言·c++·笔记·学习·多态·c++40周年
布丁写代码11 小时前
GESP C++ 一级 2025年09月真题解析
开发语言·c++·程序人生·学习方法
喵个咪13 小时前
Qt 优雅实现线程安全单例模式(模板化 + 自动清理)
c++·后端·qt
欧阳x天13 小时前
C++入门(一)
c++
小张成长计划..13 小时前
【C++】:priority_queue的理解,使用和模拟实现
c++