C++ IO流¶
约 3777 个字 373 行代码 1 张图片 预计阅读时间 17 分钟
C++系统中实现了一个庞大的类库,其中ios
为基类,其他类都是直接或间接派生自ios
类
在C++中,IO流分为三种:
- 标准输入输出流
- 文件读写流
stringstream
流
示意图如下:
标准输入输出流¶
根据上面的示意图,C++标准库提供了4个全局流对象cin
、cout
、cerr
、clog
,使用cout
进行标准输出,即数据从内存流向控制台(显示器)。使用cin
进行标准输入即数据通过键盘输入到程序中,使用cerr
用来进行标准错误的输出,以及使用clog
进行日志的输出
从上图可以看出,cout
、cerr
、clog
是ostream
类的三个不同的对象,因此这三个对象现在基本没有区别,只是应用场景不同
基本使用如下:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
在使用上面的标准输入输出流时需要注意:
cin
为缓冲流。所谓缓冲流,就是会等待键盘输入数据然后保存在缓冲区中,当要提取时,是从缓冲区中拿。如果一次输入过多,则只会提取需要的部分;如果输入错误,必须在回车之前修改,否则就会出现相关错误问题。因为存在缓冲区,所以只有把输入缓冲区中的数据取完后,在使用cin
对象才会要求输入新的数据。对于出错时(例如用于接收输入的变量类型与输入的数据类型不匹配),则会设置对应的状态位为1进行标记,除非错误标记被清除,否则一旦有错误标记,cin
无法继续读取- 空格和回车都作为数据之间的默认分隔符,分隔符不会被读入,所以多个数据可以在一行输入,也可以分行输入。但如果是字符型和字符串,则空格(ASCII码为32)无法用
cin
输入,字符串中也不能有空白字符(空格、制表符或者换行换行符),对于需要读取带空格的字符串可以使用cin
对象中的getline
函数一次获取一行数据并以回车为结束标记 !!! note 需要注意,此处提到的getline
函数是iostream中的成员函数,而不是string类中的友元函数 - 在C++标准库中,已经重载了内置类型的流提取和流插入运算符,但是对于自定义类型来说,如果需要使用标准输入输出就必须对流提取和流插入运算符做重载
C++ IO流的错误状态¶
在C++中,IO流错误状态可以分为四种:
goodbit
:正常读取标记,使用good()
函数可以获取到对应good
位是否为1eofbit
:读取到文件结尾结束标记,使用eof()
函数可以获取到对应eofbit
位是否为1failbit
:读取异常标记,一般是普通问题,例如读取时变量的类型与输入数据的类型不匹配,使用fail()
函数可以获取到对应failbit
位是否为1badbit
:IO流错误,一般出现均为IO流严重错误,使用bad()
函数可以获取到对应badbit
位是否为1
基本使用如下:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
上面的代码中,如果输入的内容只是一个正常的整数,那么只要在int
范围内就可以正常被变量a
接收,单数如果输入的是一个类似于2ll
的内容(包含非整数的内容)时,此时就会出现第二次输入无法触发,并且对应的failbit
标记被设置为1,但是第一次的cin
会读取到非数值前的最后一个数值(例如2ll
的2
),此时因为读取到了内容,所以只有goodbit
被设置为1
因为第二次读取时,failbit
被设置为1,所以在当前情况下,如果之后还有cin
进行读取,除非将错误标记清除并且可以读取到缓冲区的ll
,否则之后的会cin
会一直无法读取,可以考虑下面的清除方式:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
C++循环读取数据原理¶
有了前面对错误状态的认识,现在可以解释下面的代码可以正常执行的原因:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
在iostream
类中,有一个类型重载函数explicit operator bool() const
,其作用是将流对象强制转换为布尔类型从而进行判断,这个强制转换的过程可以理解为:当正常读取时,因为cin
对象的goodbit
位会被设置为1,否则其他位置为1,通过这个特性,只需要在goodbit
位被设置为1时,就返回true
,否则就返回false
在C语言中,也有对应类似的操作:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 |
|
此处C语言利用的则是scanf
的返回值特性
上面的操作也被称为持续读取输入,在一些需要处理IO的OJ题上经常碰到这种做法,对于这种循环输入,也可以使用一个约定俗成的快捷键终止读取:Ctrl+Z(不是Ctrl+C,该快捷键会直接结束进程,如果输入后面还有其他逻辑则无法执行)
C++标准输入输出流效率问题¶
因为C++需要兼容C语言的输入输出,但是C++和C语言各有各的缓冲区,如果在一个C++程序中,既使用了cout
,又使用了printf
,此时就会出现同步的问题,对于cin
和scanf
也是如此,所以为了输入输出效率有时会考虑将同步关闭,下面是解除同步的方式:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
上面的代码中,可以不需要写cout.tie(nullptr)
,因为cout
与cin
是绑定的,当使用cin
时,它会自动刷新cout
,解除这种绑定的目的是防止这种自动刷新以提高输入输出效率,所以一般情况下只需要使用一个tie
函数即可
C++文件读取写入流¶
C++根据文件内容的数据格式分为二进制文件和文本文件。采用文件流对象操作文件的一般步骤:
- 定义一个文件流对象,有两种主要方式:
- 单独创建读取和写入对象:使用
ifstream
创建读取对象:ifstream ifile
(只输入用);使用ofstream
创建写入对象:ofstream ofile
(只输出用) - 一次创建二用对象:使用
fstream
创建即可写入又可读取对象:fstream iofile
(既输入又输出用)
- 单独创建读取和写入对象:使用
- 使用文件流对象的成员函数打开一个磁盘文件,使得文件流对象和磁盘文件之间建立联系
- 使用提取和插入运算符对文件进行读写操作,或使用成员函数进行读写
- 关闭文件(一般来说,可以不用显示调用
close
函数)
文本文件¶
示例代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
|
Note
可以通过gcount
函数获取到getline
函数读取到的实际字符个数
上面的代码中,因为文件的写入读取流fstream
是iostream
的子类,所以也可以使用对应的流提取和流插入运算符,但是需要注意的是,在读取过程中,读取的是一个字符串时,必须确保字符串中没有默认分隔符,否则流提取运算符无法读取到分隔符后面的内容(例如上面代码中的hello world\n
使用流提取只会读取到hello
)
需要注意上面代码中使用的是iostream中的getline
函数的使用,该函数有两种形式:
istream& getline (char* s, streamsize n)
:这一种形式默认读取n
个字符到字符串s
中,并且默认分隔符为\n
,即读取到\n
就不会继续读取。所以上面的代码中hello world\n
的\n
无法被读取istream& getline (char* s, streamsize n, char delim )
:这一种形式前面的两个参数与第一种形式一致,但是分隔符为指定的delim
字符,如果在读取到的字符串中遇到了delim
时就直接停止并且不添加delim
到字符串中
上面getline
的两种形式,当读取的内容小于用于接收读取内容的容器大小,则会提前结束并设置eofbit
位。默认情况下,如果n
大于0,则会自动在存储的字符串后自动添加一个空字符\0
,即使提取的是空字符串也是如此
在istream头文件中,get
函数一共有四个版本,上面的代码只使用了其中的一个版本,四个版本原型如下:
int get()
:一次获取一个字符,返回对应字符的ASCII码值,否则返回eof
istream& get( char_type& ch )
:一个获取一个字符存储到char
类型的变量中,返回一个istream
对象istream& get (char* s, streamsize n)
:获取n
个字符存储到s
数组中,返回一个istream
对象,默认情况下,本函数的结束标志为\n
istream& get (char* s, streamsize n, char delim)
:获取n
个字符存储到s
数组中,返回一个istream
对象,默认情况下,本函数的结束标志为delim
与getline
函数一致,当读取的内容小于用于接收读取内容的容器大小,则会提前结束并设置eofbit
位。默认情况下,如果n
大于0,则会自动在存储的字符串后自动添加一个空字符\0
,即使提取的是空字符串也是如此
如果是string类的友元函数,则getline
函数的原型如下:
istream& getline (istream& is, string& str, char delim)
:获取以字符delim
结尾之前的字符串放入对象str
中istream& getline (istream& is, string& str)
:获取以字符'\n'
结尾之前的字符串放入对象str
中
使用string类的getline
函数则上面的代码可以修改为:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
gcount
函数不会返回字符串的内容长度,不同于iostream中的getline
函数,string的友元getline
函数因为存储的对象是string,所以getline
函数不会在字符串末尾自动添加\0
Note
这里需要注意,C++中的string底层因为本质还是字符串数组,而之所以getline
函数不会添加\0
,是因为string类本质上是通过内部的成员记录插入字符的个数进行字符管理,根据有效字符个数显示字符串
对于同一个流读取,与C语言相同,在第一次读取后如果还有其他读取,其他读取会从第一次读取时的光标位置继续读取,如果想指定具体位置,可以使用seekg
函数设置光标位置,该函数也存在两个版本:
istream& seekg (streampos pos)
:直接指定默认提供的位置:ios_base::beg
(文件开始位置)、ios_base::cur
(文件光标当前位置,直接使用与不适用seekg
函数一样)和ios_base::end
(文件末尾位置)istream& seekg (streamoff off, ios_base::seekdir way)
:该函数表示从文本文件way
位置开始向后偏移off
个字符后的光标位置,way
的取值与第一个版本的pos
取值相同
二进制文件¶
二进制文件与文本文件读取的方式基本一致,只是在打开文件或者创建读取/写入对象时需要指定使用binary
的方式打开
读取流构造定义如下:
C++ | |
---|---|
1 2 3 4 5 |
|
写入流构造定义如下:
C++ | |
---|---|
1 2 3 4 5 |
|
如果是文本文件,则第二个参数mode
可以不传递而直接使用缺省值,但是如果是二进制文件,就需要传递对应的模式,写入流:out | binary
,读取流:in | binary
除了有binary
模式,还有下面的几种模式:
ate
:从文件末尾开始app
:追加模式trunc
:覆写模式
基本使用如下(实现文件复制):
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
向文件中写入/读取对象内容(深浅拷贝问题)¶
以自定义Date
类为例:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
模拟配置文件属性:
C++ | |
---|---|
1 2 3 4 5 6 |
|
写入对象内容到配置文件/读取对象内容到配置文件工具类:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
测试代码:
C++ | |
---|---|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
|
上面的代码中,如果将配置文件中的char
数组改为string
类型,此时就可能会出现深浅拷贝问题,因为存储的字符串中的字符个数大于string
底层的buffer
缓冲区之后,string
就会在堆区开辟空间存储字符串,此时直接向文件中进行写入时,写入的就是对应指向堆区空间的指针,如果是读取和写入时两个单独的程序进程处理,那么此时写入时的堆区位置在程序结束就被销毁了,此时读取时读到的是一个就是一个野指针,从而导致读取失败程序崩溃
stringstream
流简单介绍¶
在C++中,可以使用stringstream
类对象来处理字符串与其他类型的转化问题
在程序中如果想要使用stringstream
,必须要包含头文件。在该头文件下,标准库三个类:istringstream
、ostringstream
和 stringstream
,分别用来进行流的输入、输出和输入输出操
作,下面主要介绍stringstream
一般情况下stringstream
可以用来做如下的事情:
-
将数值类型数据格式化为字符串
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
#include <iostream> #include <sstream> using namespace std; int main() { int a = 12345678; string sa; // 将一个整形变量转化为字符串,存储到string类对象中 stringstream s; s << a; s >> sa; s.str(""); // 清空底层string对象(将s中的内容用空字符串替换) s.clear(); // 清空s, 不清空会转化失败 double d = 12.34; s << d; s >> sa; string sValue; sValue = s.str(); // str()方法:返回stringsteam中管理的string类型 cout << sValue << endl; return 0; }
上面的代码中,需要使用
clear
函数,因为stringstream
在进行一次内容写入时(例如s>>sa
),会将其内部状态设置为badbit
,因此下一次转换是必须调用clear
函数将状态重置为goodbit
才可以转换,但是clear
不会将stringstream
对象底层字符串清空掉,如果不清空,在多次转换时,会将结果全部累积在底层string
对象中,所以需要使用void str (const string& s)
方法清空字符串Note
如果
str
函数不传递任何参数,则原型为:string str() const;
,相当于获取stringstream
存储的字符串 -
字符串拼接
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
#include <iostream> #include <sstream> using namespace std; int main() { stringstream sstream; // 将多个字符串放入 sstream 中 sstream << "first" << " " << "string,"; sstream << " second string"; cout << "strResult is: " << sstream.str() << endl; // 清空 sstream sstream.str(""); sstream << "third string"; cout << "After clear, strResult is: " << sstream.str() << endl; return 0; }
-
序列化和反序列化结构数据
Note
序列化:输出对象内容
反序列化:读取对象内容
C++ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
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; 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; }
需要注意,在上面的代码中,因为流插入和流提取默认分隔符为空格和换行,所以序列化和反序列化时可以不用保证流提取时和流插入时格式一致,但是需要保证流插入和流提取对应的变量顺序必须相同
使用stringstream
流的优势:stringstream
实际是在其底层维护了一个string类型的对象用来保存结果。stringstream
使用string
类对象代替字符数组,可以避免缓冲区溢出的危险,而且其会对参数类型进行推演,不需要格式化控制,也不会出现格式化失败的风险,因此使用更方便,也更安全