目录
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;elsereturn 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; // idDate _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也是有局限的,如写和读的顺序不一样就会出错