【C++】IO流

时间:2024-09-30 19:15:15

目录

1、C语言的输入与输出

2、流是什么

3、C++IO流

3.1 输入输出流

面向对象的好处

标识

缓冲区

3.2 文件流

3.3 字符流

序列化和反序列化结构数据


1、C语言的输入与输出

C语言中我们用到的最频繁的输入输出方式就是scanf ()与printf()。 scanf(): 从标准输入设备(键盘)读取数据,并将值存放在变量中。printf(): 将指定的文字/字符串输出到标准输出设备(屏幕)。注意宽度输出和精度输出控制。C语言借助了相应的缓冲区来进行输入与输出。如下图所示:

对输入输出缓冲区的理解:
1.可以屏蔽掉低级I/O的实现,低级I/O的实现依赖操作系统本身内核的实现,所以如果能够屏蔽这部分的差异,可以很容易写出可移植的程序。

2.可以使用这部分的内容实现“行”读取的行为,对于计算机而言是没有“行”这个概念,有了这部分,就可以定义“行”的概念,然后解析缓冲区的内容,返回一个“行”。

2、流是什么

“流”即是流动的意思,是物质从一处向另一处流动的过程,是对一种有序连续且具有方向性的数据( 其单位可以是bit,byte,packet )的抽象描述。

C++流是指信息从外部输入设备(如键盘)向计算机内部(如内存)输入和从内存向外部输出设备(显示器)输出的过程。这种输入输出的过程被形象的比喻为“流”。

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

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

3、C++IO流

C++系统实现了一个庞大的类库,其中ios为基类,其他类都是直接或间接派生自ios类

C++使用面向对象的方式进行了处理,设计出了一套面向对象的IO流

3.1 输入输出流

面向对象的好处

为什么C语言已经有了输入输出流,C++还要设计一套面向对象的输入输出流呢?
C语言的输入输出流针对内置类型是够用的,但是针对自定义类型是无法直接输出的,C++可以通过运算符重载来输出自定义类型。内置类型可以直接输入输出也是因为已经写成了成员函数

operator<<是流插入,operator>>是流提取

cin是istream类型的对象,cout、cerr、clog都是ostream类型的对象

标识

在一些题中,会要求多行输入

int main()
{
	// C语言方式
    char str[10];//定义二维数组,保存多个字符串
    int i = 0;
    while (scanf("%s", str[i]) != EOF)
    {
        i++;
    }
    // C++方式
    string s;
    while(cin>>s)
    { }
	return 0;
}

我们来看看为什么为什么cin>>s的返回结果能作为能否进入while的依据呢?

cin>>s,流提取string,调用了string的operator>>

cin传给is,s传给str,并返回一个istream类型的对象,也就是返回一个cin,那istream类型的对象又是如何作为判断依据的呢?

istream类重载了一个operator bool,所以cin>>str实际上调用了两个函数 

int main()
{
    // C++方式
    string s;
    //while(cin>>s)
    while(operator>>(cin, s).operator bool())
    { }
	return 0;
}

在上面这个程序中,若要结束循环有两种方式,ctrl+z+换行或ctrl+c

为什么输入这两个就可以结束while循环呢?

实际上,IO流设置了4个标志,这4个标志是用一个int的不同位来存储的

一般出错是failbit,若出现非常非常严重的错误是bdabit,eofbit主要用于文件,goodbit说明没错误

istream的operator bool的返回值会查看failbit和badbit,若这两个标识都没有被设置则返回true。输入ctrl+z+换行会设置failbit,ctri+c是直接结束进程

有4个函数可以读取4个标志,标志被设置了就返回true

int main()
{
	string str;
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;
	while (cin >> str)
	{
		cout << str << endl;
	}
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;
	return 0;
}

在上面这个程序中,使用ctrl+z+回车来结束循环可以观察4个标识的变化情况

会发现,当输入ctrl+z+回车后,failbit和eofbit被设置成了1

int main()
{
	string str;
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;
	while (cin >> str)
	{
		cout << str << endl;
	}
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;
	while (cin >> str)
	{
		cout << str << endl;
	}
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;
	return 0;
}

控制台是一个文件,流提取就相当于读文件。我们再使用上面这个程序来检查一下标识被设置过后,还能否使用cin来进行流提取

很明显,标识被设置过后,就无法使用cin来进行流提取了

此时可以使用clear来清除错误标识,并将标识设置为goodbit

int main()
{
	string str;
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;
	while (cin >> str)
	{
		cout << str << endl;
	}
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;
	cin.clear();
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;
	while (cin >> str)
	{
		cout << str << endl;
	}
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;
	return 0;
}

若不想使用ctrl+z+回车结束。第一种方法是自己设置一个if,break,当输入某一条指令时就break;第二种方法是使用setstate来设置错误标识,让这个流彻底不能用

缓冲区
int main()
{
	int i = 0;
	cin >> i;
	cout << i << endl;
	cin >> i;
	cout << i << endl;
	return 0;
}

对于上面这一段代码,为什么输入11s后直接就连续两次输出呢?
输入的东西会被先放到缓冲区,缓冲区就内存上的一段数组,缓冲区中的东西都是以字符形式存放的。输入11s后,缓冲区中就有11s,第1个i从缓冲区读取,读了11后遇到s,s不是int,所以就读取了11,再转换为int,所以第1个i就是11。下一个i读取时,因为缓冲区还剩下一个s,不是空的,所以不会输入,因为i是int,而s是char,读取失败,就将标识修改了。所以,要整型一定要输入整型

int main()
{
	int i = 0;
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;

	cin >> i;
	cout << i << endl;
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;

	cin >> i;
	cout << i << endl;
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;
	return 0;
}

假设对于上面程序像下面这样输入

输入全是字符,此时第一个cin就读取失败了,导致标识被修改了,这样第二个cin就完全没有操作,此时可能会想到在第一个cin后clear清空标识,但是这样是不行的,因为标识虽然清空了,但是缓冲区内的ss仍然在,再读取依然会出错。所以,此时不仅仅要清空标识,还要清空缓冲区

清空缓冲区可以使用get函数,一次从缓冲区读取1个字符

int main()
{
	int i = 0;
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;

	cin >> i;
	cout << i << endl;
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;

	if (cin.fail())
	{
		cin.clear(); // 清空标识
		char ch;
		while (cin.get(ch)) // 清空缓冲区
		{
			if (ch == '\n') break;
		}
	}

	cin >> i;
	cout << i << endl;
	cout << cin.good() << " ";
	cout << cin.eof() << " ";
	cout << cin.bad() << " ";
	cout << cin.fail() << " ";
	cout << endl;
	return 0;
}

在一些算法题的答案中,通常会看到下面的代码

int main()
{
	ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
	return 0;
}

现在就来解释一下这段代码的含义。

C和C++有各自的缓冲区,以输出为例,printf和cout不是直接输出到控制台,而是先到缓冲区,换行就会刷新缓冲区,为了保持同步,C++刷新时,C语言也会刷新,所以效率底,ios::sync_with_stdio(0)就是为例关闭这种同步刷新。C++的输入、输出也有绑定关系,cin从缓冲区提取东西后,cout的缓冲区也会刷新,后两句就是为了解除这种绑定

3.2 文件流

这一列就是文件流的头文件,4种标识是定义在ios_base中的,所以文件流也有标识

ofstream对应的是写文件,ifstream对应的是读文件

#include<fstream>
int main()
{
	ofstream ofs;
	ofs.open("test.txt");
    //ofstream ofs("test.txt"); // 这一步与上面两步效果相同
	ofs.put('x');
	ofs.write("hello\n", 6);
	ofs << "7777777777" << endl;
	ofs.close();
	return 0;
}

open是打开文件,打开后默认是写
write(" ", n)是将所给字符串的前n个字符写到文件中。但是使用put和write是不好的,因为若想将整型写入文件中,使用put和write还需要先将整型转为字符或字符串。所以,使用operator<<是最好的,operator<<本质也是转为字符串
实际上是可以不使用close的,因为ofs是一个对象,有析构函数,close主要用于提前关闭文件
也可以在ofs对象构造时就给文件名

ifstream是读,读主要分为两种,读文本文件或读二进制文件。文本文件就是一些常规字符,二进制文件有特殊字符。

首先看文本文件

int main()
{
	ifstream ifs("test.cpp");
	while (!ifs.eof())
	{
		char ch = ifs.get();
		cout << ch;
	}
	return 0;
}

再看二进制文件

int main()
{
	ifstream ifs("C:\\Users\\48117\\Desktop\\排序.png", ios_base::in | ios_base::binary);
	while (!ifs.eof())
	{
		char ch = ifs.get();
		cout << ch;
	}
	return 0;
}

ifstream对象的第二个参数默认是in,若要读二进制文件,需要再加一个binary
此时不能使用cout,因为有可能打印某个特殊字符打印不出来,导致文件流被设置了,可以用一个n计数看看读到了多少字符

int main()
{
	ifstream ifs("C:\\Users\\48117\\Desktop\\排序.png", ios_base::in | ios_base::binary);
	int n = 0;
	while (!ifs.eof())
	{
		char ch = ifs.get();
		n++;
	}
	cout << n << endl;
	return 0;
}

此时答案是488276,若不加后面的binary,答案是6

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)
	{}
	operator bool()
	{
		// 这里是随意写的,假设输入_year为0,则结束
		if (_year == 0)
			return false;
		else
			return true;
	}
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;
	return out;
}
struct ServerInfo
{
	char _address[32];
	int _port;
	Date _date;
};

假设现在要将ServerInfo类型的对象中的信息写入文件,再从文件中读取信息

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)); // 从文件中读取大小为sizeof(info)的内容放到info中
	}
    // C++文件流的优势就是可以对内置类型和自定义类型,都使用
    // 一样的方式,去流插入和流提取数据
    // 当然这里自定义类型Date需要重载>> 和 <<
    // istream& operator >> (istream& in, Date& d)
    // ostream& operator << (ostream& out, const Date& d)
	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; // 配置文件
};
int main()
{
	ServerInfo winfo = { "192.0.0.1", 80, { 2022, 4, 10 } };
	// 二进制读写
	ConfigManager cf_bin("test.bin");
	cf_bin.WriteBin(winfo);
	ServerInfo rbinfo;
	cf_bin.ReadBin(rbinfo);
	cout << rbinfo._address << " " << rbinfo._port << " "<< rbinfo._date << endl;
	// 文本读写
	ConfigManager cf_text("test.text");
	cf_text.WriteText(winfo);
	ServerInfo rtinfo;
	cf_text.ReadText(rtinfo);
	cout << rtinfo._address << " " << rtinfo._port << " " <<rtinfo._date << endl;
	return 0;
}

注意:使用write和read时要强转为char*
文本文件就是先转成字符串再写出去,二进制就是不管内存中是什么,直接写出去。文本文件和二进制文件内存中存的都是二进制

二进制读写时不能将字符数组改成string

struct ServerInfo
{
	string _address; // 会出错
	int _port;
	Date _date;
};

写没事,读会报错,因为当string中的字符没有超过16字节时,是存在_buff数组中的,若超过了16字节,是存在堆上的,此时string中是一个指针指向堆上的这块空间,所以将string写进磁盘文件时,是将指针写到了磁盘文件中,若读写在一个程序是没事的,但若是读写在两个程序,先写再读,写入的是一个指针,这个指针再写的程序结束后就已经没有任何意义了,再去读,读到的就是一个野指针,导致出错。若是字符数组,是将整个数组写进磁盘,再读出来,就不会有问题。
所以,二进制读写时设计深浅拷贝的类均不能用
文本文件在写进磁盘文件时都会先转成字符串,再将字符串写进磁盘文件,int转成字符写入,string转成底层的字符串写入

此时会有问题

(1)为什么Date类重载的operator<<是ostream,而ofstream对象却可以调用呢?
因为ofstream是ostream的子类
(2)为什么写时加入了空格,而读时不需要管空格呢?
因为C++设计的IO流默认是有多项值时空格或换行是默认分割

C++设计的IO流强大之处就在于,一个类重载了operator<<、operator>>,不仅在cout、cin时可以用,在文件读写时也可以用

3.3 字符流

这一列就是字符流的头文件

#include<sstream>
int main()
{
	int i = 123;
	Date d = { 2024,9,29 };
	ostringstream oss;
	oss << i << endl;
	oss << d << endl;

	string s = oss.str();
	cout << s << endl;

	istringstream iss(s);
	int j;
	Date x;
	iss >> j >> x;
	cout << j << " " << x << endl;
	return 0;
}

ostringstream类型的对象中会有一个string,oss<<就是将信息转成字符串后写入oss的string中,换行写会被写入的,ostringstream类型的对象内部的string可以使用str来获取。将信息转成字符串后写入oss的string后,可以使用istringstream类型的对象来提取oss对象里面的string的内容,提取单个结束是遇到空格或换行

也可像下面这样使用

int main()
{
	istringstream iss;
	iss.str("100 2024 9 29");
	//istringstream iss("100 2024 9 29"); // 这一句与上面两句相同
	int j;
	Date d;
	iss >> j >> d;
	return 0;
}

若读取失败,也可clear、get 

序列化和反序列化结构数据

序列化:将信息转为字符串,通过网络发送
反序列化:将字符串解析为信息

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;
}

这里信息就使用一个结构体代替

sstringstream也是有局限的,如写和读的顺序不一样就会出错