流的四种状态
IO 操作与生俱来的一个问题是可能会发生错误,一些错误是可以恢复的,另一些是不可以的。在C++ 标准库中,用 iostate 来表示流的状态,不同的编译器 iostate 的实现可能不一样,不过都有四种状态:
badbit 系统级的错误,如不可恢复的读写错误。通常情况下一旦 badbit 被置位,流就无法再使用了。
failbit 表示发生可恢复的错误,如期望读取一个int数值,却读出一个字符串等错误。这种问题通常是可以修改的,流还可以继续使用。
eofbit 表示到达流结尾位置, 流在正常输入输出的情况下结束,会被置为eofbit状态。
goodbit 表示流处于有效状态。流在有效状态下,才能正常使用。如果 badbit 、 failbit 和 eofbit 任何一个被置位,则流无法正常使用。
标准输入输出流
对系统指定的标准设备的输入和输出。即从键盘输入数据,输出到显示器屏幕。这种输入输出称为标准输入输出,简称标准 I/O。
C++标准库定义了三个预定义的标准输入输出流对象,分别是
std::cin
、std::cout
和
std::cerr
。它们分别对应于标准输入设备(通常是键盘)、标准输出设备(通常是显示器)和标准错误设备(通常是显示器)。
标准输入流
istream 类定义了一个全局输入流对象,即 cin ,
代表的是标准输入,它从标准输入设备(键盘)获取数据,程序中的变量通过流提取符
>>
(输入流符号) 从流中提取数据。
流提取符 >>
从流中提取数据时通常跳过输入流中的空格、 tab
键、换行符等空白字符。只有在输入完数据再按回车键后,该行数据才被送入键盘缓冲区,形成输入流,提取运算符
>>
才能从中提取数据。需要注意保证从流中读取数据能正常进行。
下面来看一个例子,每次从 cin 中获取一个字符:
1 |
|
如果没有进行正确的输入,输入流会进入 failbit 的状态,无法正常工作,需要恢复流的状态。
查看C++参考文档,需要利用 clear 和 ignore
函数配合,实现这个过程:
1 |
|
缓冲机制
在标准输入输出流的测试中发现,流有着缓冲机制。缓冲区又称为缓存,它是内存空间的一部分。也就是说,在内存空间中预留了一定的存储空间,这些存储空间用来缓冲输入或输出的数据,这部分预留的空间就叫做缓冲区。缓冲区根据其对应的是输入设备还是输出设备,分为输入缓冲区和输出缓冲区。
输入或输出的内容会存在流对象对应的缓冲区,在特定情景下会从缓冲区释出。
为什么要引入缓冲区?
比如我们从磁盘里取信息,我们先把读出的数据放在缓冲区,计算机再直接从缓冲区中取数据,等缓冲区的数据取完后再去磁盘中读取,这样就可以减少磁盘的读写次数,再加上计算机对缓冲区的操作大大快于对磁盘的操作,故应用缓冲区可大大提高计算机的运行速度。
又比如,我们使用打印机打印文档,由于打印机的打印速度相对较慢,我们先把文档输出到打印机相应的缓冲区,打印机再自行逐步打印,这时我们的 CPU 可以处理别的事情。因此缓冲区就是一块内存区,它用在输入输出设备和 CPU 之间,用来缓存数据。它使得低速的输入输出设备和高速的CPU 能够协调工作,避免低速的输入输出设备占用 CPU,解放出 CPU,使其能够高效率工作。
缓冲区要做哪些工作?
从上面的描述中,不难发现缓冲区向上连接了程序的输入输出请求,向下连接了真实的 I/O 操作。作为中间层,必然需要分别处理好与上下两层之间的接口,以及要处理好上下两层之间的协作。
输入或输出的内容会存在流对象对应的缓冲区,在特定情景下会从缓冲区释出。
缓冲机制
缓冲机制分为三种类型:全缓冲、行缓冲和不带缓冲。
- 全缓冲:在这种情况下,当填满缓冲区后才进行实际 I/O 操作。全缓冲的典型代表是对磁盘文件的读写。
- 行缓冲:在这种情况下,当在输入和输出中遇到换行符时,执行真正的 I/O 操作。这时,我们输入的字符先存放在缓冲区,等按下回车键换行时才进行实际的 I/O 操作。典型代表是 cin。
- 不带缓冲:也就是不进行缓冲,有多少数据就刷新多少。标准错误输出 cerr 是典型代表,这使得出错信息可以直接尽快地显示出来。
标准输出流
如下几种情况会导致输出缓冲区内容被刷新:
- 程序正常结束
马上输出了 1025 个 a
- 缓冲区满
马上输出了 1024 个 a,等待2秒后输出了最后一个 a
(在实验环境中 cout 对象的默认缓冲区大小是 1024 个字节,缓冲区满了还继续传输内容,就会刷新出了当前缓冲区中所有内容,后面还有一个字符,就要等程序正常结束时刷新出来)
- 使用操纵符显式地刷新输出缓冲区,如 endl
加上 endl 这种操作符,直接输出了 5 个 a,等待 2 秒程序结束;如果不加 endl,等待 2 秒程序结束时才会输出 5 个 a
endl : 用来完成换行,并刷新缓冲区
flush : 用来直接刷新缓冲区的,cout.flush()
文件输入输出流
文件输入流
文件输入流对象的创建
首先我们要明确使用文件输入流的信息传输方向:文件 --》 文件输入流对象的缓冲区 --》 程序中的数据结构
根据上述的说明,我们可以将输入流对象的创建分为两类:
- 可以使用无参构造创建ifstream对象,再使用open函数将这个文件输入流对象与文件绑定(若文件不存在,则文件输入流进入failbit状态)
1 |
|
- 也可以使用有参构造创建ifstream对象,在创建时就将流对象与文件绑定,后续操作这个流对象就可以对文件进行相应操作
1 |
|
用这个输入流去读取文件中的内容,默认以换行符或空格作为间隔符,一次读取一个字符串。
1 |
|
根据不同的情况,对文件的读写操作,可以采用不同的文件打开模式。文件模式一共有六种,它们分别是:
in : 输入,文件将允许做读操作;如果文件不存在,打开失败
out : 输出,文件将允许做写操作;如果文件不存在,则直接创建一个
app : 追加,写入将始终发生在文件的末尾
ate : 末尾,写入最初在文件的末尾
trunc : 截断,如果打开的文件存在,其内容将被丢弃,其大小被截断为零
binary : 二进制,读取或写入文件的数据为二进制形式
按行读取
使用 ifstream 类中的成员函数 getline,这种方式是兼容 C 的写法,但我们通常不推荐,而是采用 <string> 提供的 getline 方法,工作中更常用。
1 |
|
默认是换行符分割,你可以通过第三个参数指定分隔符。下面看看常见使用方法:
1 |
|
读取指定字节数
read函数 + seekg函数 + tellg函数 组合起来实现。
1 |
|
我们有这样一种应用场景,就是希望知道文件的字节大小,好让我们一下子申请对应空间大小的数组,而不需要返回申请空间,而是一下子就申请完成。我们的做法如下:
1 |
|
文件输出流
文件输出流对象的创建
文件输出流的作用是将流对象保存的内容传输给文件,ofstream 对象的创建与 ifstream 对象的创建类似。
1 |
|
通过输出流运算符写内容
1 |
|
特别注意:为了实现在文件流结尾追加写入内容的效果,可以在创建流对象时指定打开模式为
std::ios::app
。因为默认情况不是追加模式,创建方式会使打开模式默认为
std::ios::out
,每次都会清空文件的内容。
通过write函数写内容
除了使用输出流运算符<< 将内容传输给文件输出流对象(传给 ofstream 对象就是将内容传到其绑定的文件中),还可以使用write函数进行传输。
1 |
|
工具:动态查看指令
为了更方便地查看多次写入的效果(动态查看文件的内容)可以使用指令
1 |
|
字符串输入输出流
通常就是用来进行类型转换,其他类型转换为字符串(输出流),字符串转换为其他类型(输入流)。
C++ 对字符串进行操作的流类型有三个:
istringstream (字符串输入流)
ostringstream (字符串输出流)
stringstream (字符串输入输出流)
它们的构造函数形式都很类似:
1 |
|
字符串输入流
将字符串的内容传输给字符串输入流对象,再通过这个对象进行字符串的处理(解析)
创建字符串输入流对象时传入 C++ 字符串,字符串的内容就被保存在了输出流对象的缓冲区中。之后可以通过输入流运算符将字符串内容输出给不同的变量,起到了字符串分隔的作用。
1 |
|
因为输入流运算符会默认以空格符作为分隔符,字符串123 456
中含有一个空格符,那么传输时会将空格前的
123 传给 num,空格后的 456 传给 num2,因为 num 和 num2 是 int
型数据,所以编译器会以 int 型数据来理解缓冲区释出的内容,将 num 和 num2
赋值为 123 和 456。
字符串输出流
通常的用途就是将各种类型的数据转换成字符串类型。
1 |
|
将字符串、int 型数据、字符串、int 型数据统统传给了字符串输出流对象,存在其缓冲区中,利用它的 str 函数,全部转为 string 类型并完成拼接。