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