【C++基础】第7章:深入 IO

深入 IO

1 IOStream 概述

在这里插入图片描述
在这里插入图片描述

1.1 IOStream 采用流式 I/O 而非记录 I/O ,但可以在此基础上引入结构信息

IOStream:采用流的方式来进行输入输出。

流可以认为是一个序列,序列中的每个元素是一个字节,换句话说,IOStream把输入输出看成一个字节流。相应地,我们认为,IOStream进行输出时,它认为是一串字节输出出去,输入时它读入的也是一连串的字节。这种输入输出的方式被称为流式IO

与之相对应的就是记录IO,什么是记录IO?

比如说数据库,特别是关系型数据库,很多时候是采用记录的方式去保存其中的一些数据,如数据库里可能会保存一个表,表里面表示了一本书的信息,包含这本书的单价、销量、利润等等的一系列信息。在数据库里面,当对一条记录进行读取时,我们基本上读取的单位是这样的记录,不会单独读取这本书的单价、销量等,我们只要把记录拿到,同时能看到的是这本书所有的信息。那么这样的输入输出操作就是典型的记录IO

IOStream 采用流式 I/O 而非记录 I/O。

因为字符是构成记录的基础,记录的本质底层还是字符序列。

1.2 流式 I/O要处理的两个主要问题

1.2.1 表示形式的变化:使用格式化 / 解析这样的操作在数据的内部表示与字符序列间转换

在这里插入图片描述

  1. 格式化:
    std::cout就是典型的IO流(输出流)。
    上图第5行编译成二进制文件之后,系统执行到第5行时,分配一块内存存储二进制数据,这个二进制数据表示了100这个数(计算机内部都是用二进制保存数据的)。如果把100转换成二进制,即01100100这样一个字节,int可能占n个字节(如在我电脑占4个字节),因此01100100前面需要补一系列的0(n个0):0000...00 011001000000...00 01100100int x = 100;之后,x在内存当中的二进制表示形式。接下来在第6行打印时,系统通过格式化的方式二进制表示形式格式化成3个字符(100是3个字符)。因此,把二进制形式(数据的内部表示)转换成字符序列表现形式,本质上就是格式化。这就是输出流所使用的表示形式的变换。

  2. 解析
    相应地,std::cin(输入流)也是一样。如我们需要用户输入一个数(100),cin要在它的内部把100这样的3个字符组成的序列转换成二进制表示形式,同时把这个二进制表示形式存到一个变量当中。这样的操作就是解析操作,将字符序列转换成内部表示。

表示形式的变化要注意的另外一点:
在这里插入图片描述
联合体union:使用一块内存,既表示x又表示y,在我的系统中,int和float所占的内存一样,都是4个字节。换句话说,通过这样的方式相当于定义了一块内存,这块内存占了4个字节,可以同时用来表示x和y。
11行:在内存中,首先把100转换成int型,再把int型所对应的二进制序列添加到这块内存中;
在这里插入图片描述
上图输出的两个结果都来源于同一块内存(因为我们使用联合体来定义),即系统在读取数据时,看到的是同一块内存中相同的二进制形式数据,在此基础上,13、14行打印出来的结果不一样,这是因为我们采用不同的类型。即我们使用不同的方式来解释这块内存中的二进制数,int型我们会采用int型的方式解释,float型我们会采用float型的方式解释,因为解释方式不同,相应地格式化处理的方法不同,则字符序列的结果也不同。故对于同样一块内存,如果我们的类型不同,输出流转换出来的字符序列也会不同。

与之类似,对于同样一块字符序列,如果最终类型不同,那么我们的输入流在进行解析时也会采用不同方式转换成不同结果。

即根据不同的输入内容,根据不同的类型,使用不同的格式化、解析方法,本质上就是IOStream要处理的主要问题。

1.2.2 与外部设备的通信:针对不同的外部设备(终端、文件、内存)引入不同的处理逻辑

在这里插入图片描述
上图std::cout是输出流,输出到终端。

IOStream除了能和终端进行通信之外,我们还能文件、内存进行通信。针对不同的外部设备(终端、文件、内存)引入不同的处理逻辑。但IOStream能把外部设备具体逻辑封装起来,对上层提供统一接口。

1.3 所涉及到的操作

从上到下:输出流的操作
从下到上:输入流的操作

  1. 格式化 / 解析
  2. 缓存
  3. 编码转换
  4. 传输

1.4 采用模板来封装字符特性,采用继承来封装设备特性

在这里插入图片描述
在这里插入图片描述
6行、7行等价:
在这里插入图片描述
6行:x定义的是文件输入流,它所处理的底层字符是char。

1.4.1 常用的类型实际上是类模板实例化的结果

2 输入与输出

2.1 输入与输出分为格式化与非格式化两类

非格式化:省略格式化和解析这个操作。

2.2 非格式化 I/O :不涉及数据表示形式的变化

实际上,任何输入输出类都会提供一些方法来支持非格式化的操作。

2.2.1 常用输入函数: get / read / getline / gcount

get:读取一个字符;
read:读取多个字符;
getline:读取一行信息;
gcount:返回在上一个非格式化输入操作所读取的字符的个数。

2.2.2 常用输出函数: put / write

put:写一个字符;
write:写多个字符。
在这里插入图片描述

2.2.3 非格式化和格式化的区别

下面通过一个例子看看非格式化和格式化的区别:

  1. 格式化:
    在这里插入图片描述
    上图,输入100(6行),系统输出100。

内部逻辑:6行是一个格式化操作,会读入100这3个字符,然后解析成二进制表示,并保存在int中。接下来在第7行打印x时,会将这个二进制表示的x进行格式化,输出到终端。

  1. 非格式化:
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    7行:希望读入4个字符,我们输入3个字符100,系统也是可以读取到,实际上每个字符都对应一个ASCI码(二进制数值:0~255),read会把这些ASCI码读取出来,接下来不对这些数值做任何解析操作,直接放到x的某个字节中。因为x是4个字节,输入100,系统正好读到1、0、0、回车,4个字符,系统把这四个字符对应的ASCI码原封不动地放到7行的x中,接下来第8行打印x时,使用格式化输出(第7行使用非格式化输入),因此系统会尝试把这样的二进制序列进行格式化输出(由二进制转换成人类可读的int)。

即系统识别到的输入时ASIC码,而不是100,故格式化输出的就是ASIC码格式化(由二进制转换成人类可读的int)后的数据(即把ASIC码当成二进型数据了)。

非格式化的另一个trick:
在这里插入图片描述
输入1,然后回车,系统并没有作出任何回应:(因为只输入了两个字节:1、回车)
在这里插入图片描述
再继续输入2、回车:
在这里插入图片描述
即上图代码的非格式化输入要求输入4个字符,输入2、回车之后,系统读完4个字符之后,会把1、回车、2、回车所对应的ASIC码对应的二进制数值保存在int x 的四个位上。

2.3 格式化 I/O :使用移位操作符来进行的输入 (>>) 与输出 (<<)

非格式化IO,我们通常使用类当中的方法来实现。如read、get方法等。而格式化 I/O 通常使用移位操作符来进行的输入 (>>) 与输出 (<<)。
在这里插入图片描述

2.3.1 C++ 通过操作符重载以支持内建数据类型的格式化 I/O

如下图,c++标准库对左移操作符进行重载。编译器会把下图橙色翻译成一个函数调用。函数的第一个参数是std::cout所对应的类型,第二个参数是float类型。std::cout << y会触发函数调用,传入的是std::cout,第二个参数是float类型的对象,编译器就能够知道要对float类型对象进行格式化操作,输出一个对人类友好的结果。
在这里插入图片描述
输出不同类型,输出的结果可能不同。如下图:
在这里插入图片描述
出现0和48是因为我们引入很多函数重载。在第6行调用std::cout << x,实际上触发了某个函数的调用,该函数的第一个参数是std::cout,第二个参数是char类型的对象。在8行,std::cout << y触发了另外一个函数调用,该函数的第一个参数是std::cout,第二个参数是int类型的对象。系统根据不同函数,引入不同逻辑,进而选择不同格式化方法。

2.3.2 可以通过重载操作符以支持自定义类型的格式化 I/O

2.4 格式控制

之前内部表示和字符序列的转换过程的转换逻辑都是一些缺省的逻辑,即c++内部自动选择了这样的逻辑。但实际上,除了缺省的逻辑,我们还可以通过格式控制来对转换逻辑进行修改。实际上,我们的输入输出这样的类型会提供一系列的函数来进行格式控制,我们通过调用这样的一些函数来去改变相应的类所对应的对象在输入输出时的一些具体的行为。

我们可以把格式控制的方法分为3类:

2.4.1 可接收位掩码类型( showpos )、字符类型( fill )与取值相对随意( width )的格式化参数

c++里,操作的基本数据单元是字节(一个字节占8位),但是在一些特殊情况下,我们只需要对一位或者几位来进行操作,通过修改一位或者几位的值来表示要引入什么样的控制,而实际上,如何去修改某个字节的具体某一位呢?就是通过位掩码类型来进行操作。

位掩码类型、字符类型与取值相对随意的格式化参数有很多,在这里我们把每一种类型只选择一个有代表性的格式控制的相应的方法:

  1. showpos:
    6行:格式控制基本上还是通过方法的形式引入。setf中的f代表flag,一个flag通常代表一位(一个标志),setf即控制某一个std::cout内部所维护的状态,它的一个位,然后进行修改。
    在这里插入图片描述
    在这里插入图片描述
    为什么48前面多了+号。因为加了第6行,pos指+号。通过showpos来改变std::cout的行为。即相当于改变了格式化的行为。

但showpos并不会对7行的x有任何影响,因为x的类型是char,是一个字符,并不是整数,故并不存在正和负的概念。

但9行打印的是int型的整数,有正负概念,因此showpos会对9行的输出产生影响。

故格式控制并不是会对任何输出都产生影响。

  1. 取值相对随意( width )的格式化参数
    width接收一个整数。
    在这里插入图片描述
    上图,加了第7行width,输出结果的1往右移了。因为width(10):整个输出占10个字符,1即“ 1”(width只对字符起作用)

  2. 字符类型( fill )
    在这里插入图片描述
    上图,第7行导致字符1前面有9个空格,8行导致9个空格换成.。8行如果是std::cout.fill();,缺省情况下,才填充空格。

2.4.2 为什么fill和width对y不起作用?

注意 width 方法的特殊性:触发后被重置。

width会对写操作产生影响。进行写操作时,写当前这个东西,占多宽?我们需要读取一下width()括号内的值,但这里有个特殊行为,对于width这样的值,只要读完之后,就会把width值变成缺省值(0),0即代表原本的宽,不会在这个字符前面插入相应的空白。即上图,调用完第九行后,width(10)就会被重置,在11行时只输出+49。

如果就是要让+49前面有空格,那么加11行:
在这里插入图片描述

2.5 操纵符

2.5.1 简化格式化参数的设置

S
在这里插入图片描述
11、13行:把x、y的值打印出来。8、9、10、12行是对输入格式进行控制。代码太多

故c++在构造输入输出库时引入操纵符。实际上任何一个方法来设置输出格式,甚至是对输入的一些格式来进行设置时,c++都会有相应的操纵符来相应处理。如9行,使用setf来设置showpos这样的标志,对应的操纵符是std::showpos(放到格式化输出语句里):
在这里插入图片描述
在这里插入图片描述
49前面有+号了。即8行的std::showpos和9行的setf的功能一致。

而width也有对应的操纵符:(std::setw(10)
2行:使用width操纵符首先要加头文件
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
fill也有相应的操纵符:
在这里插入图片描述
或:在这里插入图片描述
在这里插入图片描述
我们之前讨论输入输出时,实际上讨论的格式控制很多都是针对输出这个概念而言的。

接下来看看输入的相关内容。

2.5.2 触发实际的插入与提取操作

格式化的输入会涉及提取的操作(非格式化操作没有提取的操作)。提取操作会把相应的输入的字符序列转换成二进制表示。关于输入的提取有两点要注意:

  1. 提取会放松对格式的限制
    在这里插入图片描述
    上图代码,我们终端输入“ 10”,打印出来的还是“10”:
    在这里插入图片描述
    输入+10:
    在这里插入图片描述
    输入+010:
    在这里插入图片描述
    但是这种放松只是对部分数据类型有效:(提取一个数,会放松提取限制,但是提取字符串、字符,那么提取的行为可能会发生改变)
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述

  2. 提取 C 风格字符串时要小心内存越界
    在这里插入图片描述
    我们在第7行声明x对象时,只表明x是一个字符串,但是x的长度我们未知,为什么在8行读取abcdefg时,abcdefg能完整保存在string里面呢?
    实际上string是c++提供的抽象的数据类型,该数据类型内部提供自身机制来维护提供的缓存有多大,换句话说,只要输入的字符串不是特别长(超过内存承受范围),基本上x它的内部在进行输入时,它会开辟相应的空间,然后读取相应的数据,保存下来。这是c++提供的便利,但是这种便利如果换到c风格字符串时可能会出现问题。

如,我们对第7行进行修改:
7行:声明了一个数组,数组包含了5个元素,每个元素是char类型,本质上7行这个东西可以用作c语言的字符串(c语言经常使用这个字符数组来表示字符串(c++风格的字符串使用string,不会用char来表示)),编译运行:在这里插入图片描述
系统崩溃。
在第7行我们要构建char类型数组,数组包含5个元素,这个信息是在编译期就确定了,即在运行期是不能修改的,即不能根据我们的输入进行修改或扩展。接下来我们终端输入的是abcdefg,存储abcdefg这样的字符串,我们需要8个元素(abcdefg是7个字符,但是在c语言要表示字符串,我们需要知道字符串到什么时候结束,故要在后面加一个特殊字符,这个特殊字符所对应的ASIC编码是0,即abcdefg\0),但是7行的x只声明了5个空间。我们在8行,cin写的时候,并不会考虑7行的x有多少空间,故会造成内存越界。

如何避免内存越界?
8行的setw可以用于输入流也可以用于输出流。
用于输出流:用来表示输出的东西的宽度是多少
用于输入流:如果接收的是c风格的字符串,c风格的字符串最多能存储多少个元素
如下图:(由7行知,x最多存储5个元素,而我们最后一个元素一定要是结束符,故编译器会把abcd读到x的前4个元素,然后在x的最后一个元素插一个结束符)
在这里插入图片描述
在这里插入图片描述

3 文件与内存操作

之前使用cin和cout作为示例来阐述输入输出流。cin和cout本质上和终端相关。

而c++的IO流除了能向终端进行操作之外,还能对文件和内存进行操作。

在这里插入图片描述

3.1 文件操作

文件操作主要使用到三个类模板:basic_ifstream / basic_ofstream / basic_fstream

3.1.1 basic_ifstream / basic_ofstream / basic_fstream

在这里插入图片描述
basic_ifstream是一个类模板,在类模板里面basic_ifstream包含两个类型,其中第一个类型CharT代表处理的字符类型;第2个Traits类型代表了字符相关的一些属性。通常我们在使用basic_ifstream只需要给定CharT就行了,而CharT又会给定成char,代表了要处理的是字符:
在这里插入图片描述
我们通常会使用basic_ifstream来进行一些文件相关操作,因为它经常被使用,因此在c++中引入一个类型别名——ifstream,ifstream内部实现了一个输入文件流的功能。其中流里面的每个字符都是一个字节(一个char)。

与之类似,basic_ofstream也是一个类模板,定义basic_ofstream的类型别名为——ofstream。

与之类似,basic_fstream也是类模板。
那么为什么文件操作要包含3个类模板?

basic_ifstream用于输入;basic_ofstream用于输出;basic_fstream会打开一个文件,这个文件能够同时接收输入和输出两种操作。

一般情况下,我们会重点关注basic_ifstream、basic_ofstream。在本章我们会使用char来进行讨论,因此在后续程序的书写中我们主要使用ifstream、ofstream来构造相应的文件流。下面通过一个程序来引入文件流的概念,

3.1.2 文件流可以处于打开 / 关闭两种状态,处于打开状态时无法再次打开,只有打开时才能 I/O

  1. 文件输出流:
    16行:ofstream是basic_ofstream的别名,我们通过16行构造了一个对象——outFile

下图即使用文件输出流进行操作的示例:
在这里插入图片描述
没输出任何东西,为什么?
7行,cout和outFile输出Hello,没本质区别。都是使用格式化的方式来输出,但是cout的输出,会输出到终端上;但outFile会输出到文件里面。因此终端看不到。

应该这么看outFile的输出结果:
在这里插入图片描述

  1. 文件输入流:
    第6行构造了个输入流的对象——inFile,inFile指向我们之前构造的my_file文件(和my_file文件关联了)。
    在这里插入图片描述
    接下来我们通过inFile来读取一个字符串:

第9行使用inFile来读取一个字符串,inFile本来存在my_file文件当中,即相当于把my_file文件里面的内容(Hello)读取出来,在10行打印出来。
在这里插入图片描述
刚才1、2两个操作,我们在构造inFile对象和outFile对象时,在对象初始化(6/7行)时传入了一个字符串,这个字符串本质上是一个文件名称。

我们在构造输出流时,构造了my_file文件,这个文件会和outFile对象关联起来,接下来对outFile的所有操作本质上都会往my_file里面写;与之类似在第9行,我们也是关联了一个my_file文件(关联到输入流inFile),接下来对inFile读,本质上数据来源都是my_file里面的内容。
在这里插入图片描述
故,我们要想使用文件输入输出流,基本上都要在构造输出流对象和输入流对象时,都要和具体的文件(如my_file)关联起来。

一个文件流都是处于两种状态(打开或关闭)的,处于打开状态时,文件流是没办法再次打开的(一个流不能同时关联多个文件),只有打开时才能IO(才能进行输入输出)。

那么什么叫打开?什么叫关闭?

如上图,如果一个流(outFile)和某个文件(如my_file)关联起来(不能再关联另外一个文件),那么这个流就处于打开的状态;即只有流和文件关联起来,才能进行输入输出的操作。

那么对于给定的任何一个对象(如outFile),我们如何判断它目前处于打开状态还是关闭状态?

有人说看到上图6行后面写了my_file,就认为文件流处于打开状态。这样没问题,但是比如说把outFile传给了一个函数去使用时,它要确保我的输入是合理的,或者输出是正确的,那么这时候就要检测我的对象是处于打开还是关闭状态。

c++提供了is_open函数检测流是否处于打开状态(检测流是否和某个文件关联);
在这里插入图片描述
在这里插入图片描述
由上图,is_open这个函数会返回一个bool值。如果返回true,表示文件流处于打开状态。

如下代码(7行)可以判断文件是否处于打开状态:

6行:outFile和my-file关联起来了,执行到7行是,系统认为outFile处于打开的状态。故系统会输出1(对应true)。
在这里插入图片描述
在这里插入图片描述
实际上,我们也可以使用缺省的方式来构造6行的对象:
在这里插入图片描述
上图6行这样也相当于构造了一个输出流outFile,但是会输出0,表示outfile这个文件流处于关闭状态:(因为在构造outFile时并没有指定任何文件与之相关联,故7行返回fasle)
在这里插入图片描述
构造缺省的文件流有什么意义?

我们之前使用构造函数时,会传入一个字符串来表示outFile和那个文件相关联,这是一种构造方法。但还有其他构造以及使用输入流、输出流的方法:我们可以先使用缺省的方式构造一个对象,构造完后整个对象处于关闭状态,接下来,可以使用ofstream以及ifstream提供的open函数,open函数可以打开一个文件,同时和当前流相关联:
在这里插入图片描述
我们通过下图8行,可以把outFile这个对象变成打开状态,同时和my_file文件相关联:
在这里插入图片描述
在这里插入图片描述
与之类似,有open就有close,close即接触文件和对象的关联关系接触,让这个对象处于关闭状态:(10行)
在这里插入图片描述
综上,上面只讨论了ofstream,但ifstream和fstream同样有上面的性质。

关于文件流的打开和关闭状态,还有一个trick:

IOStream在进行输入输出时,会涉及到缓存,即我们不会把每一次的输入输出都直接写到相应的目标地址(不会每次都写到终端、文件、内存,因为这样非常耗时),我们会构造一个缓存(缓存存在于内存中),我们往内存中进行读写,它的速度相对比较快,当缓存满了,我们会把缓存中的内容一次性写到终端/文件/内存中,如之前的程序:

8行:系统不会把Hello直接写到文件my_file里面,而是写入缓存
9行:把缓存中的内容放入文件里面,再把文件关闭
在这里插入图片描述

通过这样的方法,能确保close时,缓存里面还有一些东西,但是也能确保缓存当中的东西能够完全地被放到相应的文件中。

但是我们之前是如下图这样来写上图程序:

但是我们还是能确保my_file已经包含Hello
在这里插入图片描述
我们在上图7行好像并没有对outFile进行显式关闭,为啥程序运行完之后,outFile能被正确关闭(即为什么位于缓存中的“Hello\n”能够被写入文件中?)。
实际上,outFile是std::ofstream这样的一个类型,ofstream是c++所提供的抽象数据类型,这个抽象数据类型是使用类模板来实现的(我们会在后续类和类模板时提到两个重要函数,一个是构造函数(构造对象),一个是析构函数(销毁对象)),对于ofstream、ifstream这样的流,这个对象在销毁时会调用析构函数,而析构函数里面会有一个具体的逻辑用于关闭文件流(判断文件流是否处于打开状态,如果处于打开状态,我们会自动关闭)。这也即上图7行为什么没有显示调用outFile.close,只是使用析构函数的逻辑让outFile自动销毁对象,系统则会在对象销毁的过程中自动判断如果文件处于打开状态,就会把文件关闭,关闭时就会把缓存当中的内容刷新到文件当中。

实际上,每个文件流都提供了open和close函数,但我们很少使用这两个函数。比如说我们想构造一个文件流进行输入或输出,通常我们会在构造文件流的时候传入参数(如“my_file”),来表示文件流所关联的文件(如outFile),通过这样的方式,只要文件流构造完了之后,就已经和对应的文件关联起来了,接下来我们就等待outFile使用完之后销毁,销毁时会自动调用close来完成一些收尾的构工作。如:

下图6、9行表示各有一堆代码,然后中间想构造一个文件流,我们希望这个文件流关联一个文件,然后输出一些东西,再自动销毁。假设我们希望在第9行文件流得到销毁,我们可以构造域来实现:outFile的生存周期是在语句体结束时。通过这样的方式就能精确控制outFile什么时候被销毁
在这里插入图片描述
以上是相对比较常用的构造和销毁文件流的方式,通常并不会使用open、close这样的函数。

3.2 文件流的打开模式

3.2.1 每种文件流都有缺省的打开方式

如ofstream,如果我们传入字符串作为文件名(const char* filename),实际上接收一个字符串的构造函数(basic_ofstream)实际上还包含了另外一个参数,这个参数还有另外一个缺省的实参(ios_base::out)。ios_base::out表明了ofstream的一种缺省打开方式,
在这里插入图片描述
相应地,ifstream也有缺省的打开方式(ios_base::in),fstream也有缺省的打开方式。

文件流的打开方式有如下几种:

in:ios_base::in;
out:ios_base::out
在这里插入图片描述
如下图:
在这里插入图片描述

6行构造了对象outFile,是ofstream类型的对象,ofstream等于调用下图蓝色的构造函数:
在这里插入图片描述
调用了这样的构造函数等于传入了一个my_file,等价于:(上上图6行只是省略了std::ios_base::out),std::ios_base::out实际上是outFile这个构造函数的缺省值
在这里插入图片描述
上图表示,outFile文件流关联一个叫my_file的文件,这样的文件使用std::ios_base::out的方式来进行打开。

与之类似,原先上图第10行写的是inFile关联文件my_file,它等价于:
在这里插入图片描述
注意:

上面六种打开方式也可以进行组合:
在这里插入图片描述
上图按位或方式组合:
在这里插入图片描述
表示使用读的方式打开文件,且起始位置位于文件末尾。

接下来我们来看看ate中的起始位置位于文件末尾是啥意思。ifstream或ofstream都是一个文件流(文件流是一个文件,这个文件里面有一个一个字符),ifstream是用来读取这个文件里面的字符,ofstream是往文件里面添加字符。但是无论是读还是添加(写),都涉及位置问题(从那个地方开始读,往哪个地方开始写)。通常使用in来读或out来写,起始位置都是文件开头。但有时候希望从文件末尾开始读写,那么就需要ate。

当然位于文件末尾,我们可能什么都读不到,但后续讨论文件流的移动,可以将文件流的位置进行移动,移动之后获取相应的位置来进行读取。

3.2.2 注意 ate 与 app 的异同

ate是起始位置位于文件末尾,接下来可以将读写的位置进行移动,如刚开始在文件末尾,可以移动其位置到文件中的某个位置再进行读写操作。
但app的话,只能在文件末尾写入,不能移动位置。

3.2.3 trunc

在这里插入图片描述
上图6行,打开一个文件用来写,同时会把文件之前的内容删掉,然后再写入Hello。

3.2.4 binary 能禁止系统特定的转换

实际上,c++的流是字符流,本质上并不会提供一些二进制的支持,binary 能禁止系统特定的转换。换句话说,如果不写binary,系统会引入一些特定转换,如在windows中,写一个回车(往文件里面输入-n),系统会把-n进行特定转换,变成\r或\n,\r或\n传过去之后,实际上是windows下的回车。如果加入binary,就能禁止windows内部进行这样的转换,这是写-n就不代表回车,只是-n的意思。

3.2.5 避免意义不明确的流使用方式(如 ifstream + out )

6行构造一个outFile,构造ofstream的目的即进行数据输出,但参数里面也可以写std::ios_base::in(输入):(但这样意思不明确,又输入又输出的)
在这里插入图片描述
即我们不要使用ifstream来打开out,也不要使用ofstream来打开in。

3.3 合法的打开方式组合

在这里插入图片描述
第二行,out | truncout的行为是等价的。
第三行,使用out | app,并不会像out | truncout一样,事先把文件清空。
第4、5行:给fstream用的。

3.4 内存流: basic_istringstream / basic_ostringstream / basic_stringstream

输入输出流重点关注两个问题:格式化问题,设备问题。

设备包括终端、文件、内存。

对于流这样的对象(组件)而言,要做的是和某个设备进行交互,从某个设备进行读写,实际上终端就是这样一种典型的设备。

而流也可以向一段内存中进行读写,相应地,以内存作为外部设备来进行读写的这样的流就被称作内存流。

我们使用3个类模板来进行内存流的操作: basic_istringstream / basic_ostringstream / basic_stringstream。

basic_istringstream :basic_istringstream 是一个类模板,charT:我们要处理什么类型的字符;Traits:字符相关的特性;Allocator:在内存中分配空间来存储字符时应该采用什么样的行为。
在这里插入图片描述
basic_istringstream 继承于basic_istream(输入流),在其基础上引入了一些内存相关的逻辑。通常在使用basic_istringstream 时,会把要处理的字符类型设置为char(字节),设置为char实际上就和文件流很像:
在这里插入图片描述
上图,basic_istringstream 也对应了一个类型别名istringstream,我们通过会使用istringstream 来进行基于字节的读取操作。

与之类似,basic_ostringstream也可以使用char 来进行实例化,生成的类型别名是ostringstream,我们通过会使用ostringstream 来进行基于字节的写操作:
在这里插入图片描述
与之类似,basic_stringstream也可以使用char 来进行实例化,生成的类型别名是stringstream,我们通过会使用stringstream来同时支持基于字节的读和写操作:
在这里插入图片描述

  1. 使用basic_ostringstream读取数据(输出流)
    首先要使用内存流,首先要引入内存流相关的头文件:
    在这里插入图片描述
    下图,
    在这里插入图片描述
    6行:声明对象obj1,6行即输出的内存流。
    7行:6行构造了一个内存流对象,接下来把1234写入这个对象obj1内部(使用格式化方式进行输出),即把1234放入一块内存中,对内存流进行一系列操作之后,下一步就是要找到这块内存,从这块内存中读取相应的数据。我们可以使用ostringstream的一个 方法——str:
    在这里插入图片描述
    str就是要获取底层所对应的内存。

下图8行,获取底层所对应的内存res:
在这里插入图片描述
在这里插入图片描述
下图9行:检查res内部包含啥。(res是一个字符串,存储了1234这4个字符,来自于obj1内部所保存的内存所对应的信息。那么obj1为什么能保存1234?因为第7行写入了1234这1个整数,注意,9行输出的是1234这4个字符,不是1234这个整数。那么为什么能把整数类型转化成字符串类型?这是因为7行使用了格式化输出操作,本质上第7行完成了整数转换成字符串的操作
在这里插入图片描述
实际上,使用ostringstream这样的输出流,实际上就像使用文件流,终端的输出流一样,没有本质差异。如把7行改为true,把一个bool值true写入obj1里面:
在这里插入图片描述
在这里插入图片描述
true输入到内存流obj1中,通过obj1.str方法获取了内存流所对应的内存,故true转换为1之后,最终放在res内部,并不是放到终端或文件中。

之前讨论过操纵符,格式化的一些方法,内存流、文件流也能实现类似功能。比如修改7行代码:(要加入iomanip头文件)
7行:输出的东西宽度为10,前面填充.
在这里插入图片描述
以上,和我们之前看到的用cout输出到终端的行为是一致的。只不过现在是使用内存流来进行输出,输出的结果首先会保存到res,即res中已经包含........10,接下来再把res输出到终端中。

综上,我们可以在内存流里做一些格式化或非格式化的输出操作(8行),最终通过调用.str来获取所对应的内存(9行),这段内存本质上会以字符串的方式来呈现,之后可以基于字符串来进行一系列操作。

  1. 使用basic_istringstream写入数据(输入流)
    11行,构造输入流对象obj2,需要给obj2一块内存res,因为这是内存流,输入是从内存中读取的,相应地,在构造输入流时,需要把要读取的那块内存读取出来(把res给到obj2里面);
    13行:使用obj2读取x
    14行:把x打印出来。
    在这里插入图片描述
    在这里插入图片描述
    为什么输出10?从obj2(res)中的res这段内存中读取了字符1和0,然后在内部完成转换解析,把1和0构成的字符10解析成10所对应的二进制表示形式,并存储在int中。接下来cout输出。换句话说obj2格式化输入x这样的过程,和之前使用cin右移x没有本质区别。

3.5 内存流也会受打开模式: in / out / ate / app 的影响

文件流引入6中打开模式:
在这里插入图片描述
内存流只会受到4种打开模式的影响: in / out / ate / app。
如下图,basic_istringstream有缺省打开方式是下图蓝色的in(和文件流很相似)
在这里插入图片描述
即,basic_istringstream缺省打开方式是in;basic_ostringstream缺省打开方式是out;basic_stringstream缺省打开方式是in | out;

内存流对ate和app的影响和文件流相似。下图蓝色构造了一个ostringstream(输出内存流),这个输出内存流给了两个参数,一个test,一个是std::ios_base::ate。test不像文件流里面,是一个文件名称,它表示内存流中初始的一些信息,std::ios_base::ate中的ate表示写的初始位置是位于整块内存的结尾处。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
即,使用ate来打开,打开的位置是位于文件的结尾处,接下来写入1,即往test后面添加1。

内存流有个很好的特点:它会自己对内存进行管理,如写入1(9行),发现内存不够了,会开辟一块更大一点的内存,把1放入。

不要std::ios_base::ate:
在这里插入图片描述
上图7行构造了输出内存流,没有使用ate作为打开模式,故在内存流构造好之后,输入的起始位置是在整块内存的第一个字节。

3.6 使用 str() 方法获取内存流底层所对应的字符串

如下图第9行:
在这里插入图片描述

3.6.1 小心避免使用 str().c_str() 的形式获取 C 风格字符串

合理:
在这里插入图片描述
在这里插入图片描述
不合理:(程序行为未定义)
在这里插入图片描述

3.7 基于字符串流的字符串拼接优化操作

不断地往string x里面添加字符,然后打印:
在这里插入图片描述
在这里插入图片描述
但上图代码性能差。实际上对于一个string,让它插入一个字符串,基本上它会先看string里面是否有足够的缓存区保存插入的字符。

即如果string要插入一个字符串,string已经包含一个缓存区,这个缓存区可能已经包含一系列内容,接下来插入新的字符串,会建立一块新的缓存区,然后把原始string里面包含的内容copy到新的缓存区里面,接下来把要加入的字符串再附加到新的缓存区里面,再将原始缓存区释放掉。故在string插入字符会涉及到内存开辟、拷贝、缓存释放的过程。如果代码里面有很多+=,就会不断的进行内存的开辟和释放,相对耗时。

优化:(用14~19行替换7~12行)
在这里插入图片描述
以上即基于字符串流的字符串拼接优化操作

为什么这样的方法会比基于字符串的拼接方法要好?

因为很多流都会在其内部维护一个缓存区(相对比较大),对这样的输出流进行输出操作,数据不是直接写到相应设备,它是先放到缓存区,当我们缓存区满了之后,会把缓存区的内容一次性刷新到我们的设备上面去。正是这样的行为,上图14行ostringstream里面有一个缓存区(相对比较大),我们写入Hello,world等,知道写满之后,缓存区里面的内容会一次性刷新到它所对应的内存,即此时才会进行内存开辟,copy,原始内存释放的过程。因为ostringstream的缓存区相对较大,很多时候我们写入东西时,并不会把内存占满,即不会刷新的行为,即不会进行内存开辟,copy,原始内存释放的操作,相应地,性能更高。

虽然可能ostringstream缓存区内存没满,但是我们调用19行的.str,会触发缓存区的刷新。故会将缓存区的内容放到目标内存里,接下来把目标内存作为一个字符串来返回。

4 流的状态

任何一种输出输入流,无论是关联到终端还是文件还是内存,实际上在流的内部都会维护一个状态,即流处于正常状态还是异常状态。通常来讲,刚构造或者刚使用流时,流都是处于正常状态;随着后续进行插入和提取的操作,有些插入、提取操作可能会失败,或者使得流产生异常状态,在系统中,无论是终端流还是内存流还是文件流,都会在其内部维护相应的对象来表明这个流当前状态是否正常。

4.1 iostate

下图是关于流的状态的一些信息。在流的内部会维护一个数据成员,该数据成员的类型是iostate,我们使用这样的数据成员来表示流处于什么样的状态。iostate是c++内部定义的类型,它可能是某个东西的类型别名(实际上是哪个东西的类型别名,这是编译器决定的额,c++标准并没有说明)。

在这里插入图片描述
针对这样的类型别名,定义了4中常量:
在这里插入图片描述
我们通过这些bit常量来描述流处于不同的异常状态,如下:iostate对应char(可能是char的别名),badbit对应0000,0001,failbit定义成0000,0010,eofbit定义成0000,0100,通过这样的定义,相当于引入了badbit,failbit,eofbit。

上述3种bit定义都有一个特性:基本上char占8个比特,这3个bit都会将某个比特设置为1,其他比特为0,实际上我们相当于通过这样的方式来描述了流的3种错误。这样的设置有一个好处,流中可能会同时产生几种错误,如可能同时存在badbit、failbit,我们可以使用badbit | failbit来描述(等于0000,0011),如果流中3种错误都存在,则用0000,0111来描述。即我们通过这3个具体数值就能描述流中的各种错误。

实际上,上述badbit,failbit,eofbit这3个东西被称为位掩码类型(每个东西的取值都会令某个位为1,其他的位都是0)。换句话说,流这个对象里面可能包含一个数据成员,该数据成员可能就是iostate这样的类型,如下图m_state这样的数据成员,这个数据成员的取值可能是badbit,failbit,eofbit,或3者按位或出来的值,用于表示相应的错误。
在这里插入图片描述
那么如何表示流没有错误呢?

将iostate类型的数据成员m_state表示为0000,0000,即goodbit:
在这里插入图片描述

4.2 检测流的状态

为什么流的异常状态要包含3种?

对应了3中不同类型的错误!

  1. badbit
    在这里插入图片描述
    上图,
    6行:定义了outFile这样的输出文件流的对象,但没有关联任何文件,是使用缺省构造来构造的
    7行:因为outFile没有关联到任何文件,因此这样的是不可能成功的,而且会造成副作用,会将outFile由一个正常状态变成异常状态
    那么这个异常状态是属于3种异常的哪一种?badbit!badbit表示不可恢复的错误。
    在这里插入图片描述
  2. failbit
    在这里插入图片描述
    输入10,上图程序能正常运行,x获取到的值是10;但如果输入hello,程序也能执行,但下图7行的读取失败,因为6行可知,希望读取的是一个整数,但是hello这个字符串没法转换成相应的整数值,故第7行的读取失败,这样的失败造成cin设置成failbit,即使用failbit来表示失败的信息:
    在这里插入图片描述
    以上即展示了输入流产生failbit,实际上输出流也会产生failbit。

下图这样的代码编译运行也会产生failbit:(因为第6行构造了outFile,outFile没有关联到任何文件,即它的当前状态就是处于关闭状态,然后第7行调用close再次对outFile进行关闭,故相应地会把outFile设置成failbit)
在这里插入图片描述
与badbit相比,failbit是一种可以恢复的错误。即如上图代码的cin,我们传入字符串hello导致failbit,但cin下次还能继续使用,还能读取整数。
在这里插入图片描述

  1. eofbit(下图蓝色部分的输出序列改为输入序列)
    在这里插入图片描述
    如开辟了文件输入流,对输入流进行不断地读取,读取到输入流的所有信息都读取完了,读取下一个没得读了,系统就会标志成eofbit。eofbit对内存流和终端流都是有效的。

什么情况下会产生eofbit?下面使用终端流进行展示:
我们不输入字符串也不输入数值,我们输入ctrl+c(wins系统)或ctrl+d(linux系统),整个程序会直接执行完毕了:
在这里插入图片描述
这是因为ctrl+c(wins系统)或ctrl+d(linux系统)意思是,我在终端里面的输入达到了终端输入的结尾,后面没有任何的终端输入了,相应的在上图输入ctrl+c(wins系统)或ctrl+d(linux系统),第7行时系统会认为读取完终端输入的所有内容了,故cin读取x会失败,相应地,cin会设置成eofbit。

4.2.1 使用good( ) / fail() / bad() / eof() 方法检测流的状态

如下图,调用cin.good、cin.fail、cin.bad、cin.eof的方法来返回流的状态的信息:
在这里插入图片描述

4.2.2 使用转换为 bool 值的方法检测流的状态(参考cppreference)

我们还可以把流转换为相应的bool值来判断流是正常还是异常:
在这里插入图片描述
上图的false和true代表eofbit、failbit、badbit是否被设置,每个bit都有被设置和不被设置两种情况。

右边指调用good( ) / fail() / bad() / eof() 方法,系统的返回值。
在这里插入图片描述
如果eofbit、failbit、badbit都为false,即表明流处于正常状态,因此调用good方法会返回true。operator bool(转换成bool值)也会返回true,fail() / bad() / eof() 返回false。

  1. 只有eofbit、failbit、badbit均没有被设置(均为false)时,good方法才会返回true;
  2. 如果badbit被设置了(为true),bad、fail方法会返回true(只要bad方法返回true,那么fail方法也会返回true);
  3. 如果eofbit被设置为true,表示走到流的结尾,但是failbit,badbit为false,operator bool(转换成bool值)返回的还是一个正常状态(true)

在这里插入图片描述
上图,输入10,返回10001,第一个1(布尔值)对应9行的good方法,即输入10,返回true,系统属于正常状态。中间3个0表示fail、bad、eof都返回false,表示没有这3种类型的错误。最后1表示把cin转换成相应的布尔类型,返回的是1。即cin处于正常状态。


在这里插入图片描述
上图终端输入ctrl+d,9行good返回false,因为当前系统的流不是正常状态;fail对应1,即cin里面failbit被设置了(因为尝试读取x,但没读取成功);badbit不会被设置,因为我们认为前面这种读取失败并不是不可恢复的错误,相应地去调用bad,会返回false;12行,系统已经走到流的结尾(输入了ctrl+d),相应地,eof会设置为true;13行,返回false,因为当前处于读取不成功的状态。

4.3 注意区分 fail 与 eof

4.3.1 可能会被同时设置,但二者含意不同

fail表示读取失败;
eof表示读取到文件未尾;

通常来讲,读到文件尾可能是读取失败的原因,因此fail 与 eof会被同时设置。但是二者含意不同,而且有些情况下,fail和eof不会被同时设置。

  1. fail和eof会被同时设置:(读一个字符时,读取长度固定的,读一个字符,要么读取成功,要么不成功)
    如下:
    7行:读取的是一个字符
    8行:打印fail和eof的信息
    在这里插入图片描述

  2. fail和eof不会被同时设置:(读一个整数,读取长度不固定,可以用一个字节表示一个整数,也可以用两个字节表示一个整数,当读取到输入的结尾,则eof会被设置为true,读取终止,虽然读取终止,但是已经读取到前两个字符:“1”、“0”,“1”、“0”会被转换成相应的整数值,因此fail不会被设置)
    在这里插入图片描述

4.3.2 转换为 bool 值时不会考虑 eof

4.4 通常来说,只要流处于某种错误状态时,插入 / 提取操作就不会生效

如双向流,eof被设置了,要想继续插入操作生效,我们需要改变流的状态,把流的eof状态去掉,然后才能进行插入提取的操作。那么如何改变流的状态?即以下的复位流状态

4.5 复位流状态

4.5.1 clear(清空/复位) :设置流的状态为具体的值(缺省为 goodbit )

clear并不是说把流的状态,把所有的错误都清空,clear的本质是设置流的状态为具体的值。
在这里插入图片描述
在这里插入图片描述
上图9行本质是会传入goodbit,把所有的错误的状态清空。在调用clear时不一定传入缺省值,我们也可以传入eof、bad、fail。本质即把流的状态设置为具体的值。

4.5.2 setstate :将某个状态附加到现有的流状态上

比如当前流的failbit被设置了,其他bit没有被设置(处于false错误),如果使用clear时,传入一个eof,在调用clear时,流的fail就被清空,它的eof就被设置了。

但是同样的一个流,使用setstate传入eof,在调用setstate之后,它的状态是fail和eof都被设置。

通常来讲,我们不会去调用流状态的复位,如调用文件流,读取文件时,理论上应该知道文件中的每一次读取的具体含义,不会产生badbit和failbit的错误,当我们走到文件结尾时,即这个文件不需要再去使用了,那么我们就将文件关掉,将文件对应的对象删除掉就可以了。因此很少需要对流的状态进行处理。

4.6 捕获流异常:exceptions方法

如果流出现异常错误时,会在其内部将相应的状态进行设置,接下来可以通过fail、good、eof等方法获取流当前状态。

我们还可以使用exceptions方法获取流当前状态。(exceptions这里面的异常和上述流的状态异常不太一样,这里的异常是c++专门的处理方法,即当程序处于非正常状态时,可以抛出异常,抛出的异常会导致原有的程序不会再执行原有程序的匹配序列了,而是跳转到异常处理的逻辑中执行)

相应的,在流里面我们也可以这样。我们可以在exceptions来定义。eof或bad或fail被设置,则会抛出异常,我们可以在相应的地方捕获异常
在这里插入图片描述
在这里插入图片描述
如上图中间代码,我们设置了failbit,换句话说,调用了exceptions方法之后,接下来尝试读取一个东西,如果读取这个东西解析失败了,整个程序就会抛出异常,那么程序回到catch这样的异常捕获代码中执行。

但对于流,通常不会使用异常处理的方法。

5 流的定位

无论是处理终端流还是文件流还是内存流,我们都可以认为是处理一连串的数据(流即一连串的字符),对一连串的字符进行提取或写入的操作会涉及到一个问题:往那个地方提取或写入?

就文件流而言,通常是从头到尾进行读取或写入。但是我们并不限制一定要从头到尾读写。

我们刚开始打开文件之后,可以跳到文件中间读取信息,之后再跳回文件开头读取信息。这样的操作也是可以的。为了支持上述操作,c++引入流的定位这个概念。

流的定位包含两部分:获取流位置、设置流位置

5.1 获取流位置

5.1.1 tellg() / tellp() 可以用于获取输入 / 输出流位置 (pos_type 类型 )

在这里插入图片描述
tellp是basic_ostream所定义的函数,即说明tellp是输出流所提供的方法。

tellp返回的类型是pos_type 类型(pos_type 一定得是整数,因为tellp获取文件的位置,是0,1,2,3这样获取,相应地,返回的pos_type 也得是整数类型)

5.1.2 两个方法可能会失败,此时返回 pos_type(-1)

为什么会失败?
在这里插入图片描述
之前讨论流的状态时知道流可能处于异常状态。这样的异常状态(如badbit被设置或failbit被设置),我们调用fail函数都会返回true,换句话说,只要流处于badbit或failbit状态,我们就没办法使用 tellg() / tellp()来获取相应的位置信息。

如何使用 tellg() / tellp()?

  1. 下图,我们可以使用tellp这样的方法来看一下当前的写入位置是什么,对6行执行,系统会输出0,即代表了当前可以写入的位置是0,换句话说,tellp返回的是当前可以写入的位置,而不是上一个写入的位置。7行往s里面输入“h”,即相当于h已经存到流里面了,接下来写入位置会往后挪一位,此时再去调用tellp(8行),系统会返回1,接下来写入字符串(11位),写入位置会往后再挪11位,再去调用tellp,系统返回13。。。即输出流写入位置不断往后移动。

在这里插入图片描述

  1. tellg的使用
    7行定义str,其中包含Hello, world,8行定义istringstream,用来输入的流,可以用tellg来获取当前位置;10行使用in来读取字符串,把这个字符串保存在9行的word这样的对象里面,接下来11行把word里面的内容打印出来,以及11行把tellg打印出来:
    在这里插入图片描述
    由上图,打印word时,出来的是Hello,(后面的world没有打印出来),这是标准输入在处理字符串的特定行为(标准输入在处理字符串时,会依次读取当前输入的信息,直到遇到分隔符(如空格)就停下来,然后只会把分隔符前面的信息送给当前字符串,接下来会从分隔符的位置开始读取)。故word打印出来的是Hello,,tellg打印出来的是6。tellg表示接下来要读取的字符:Hello, world的空格处(即第6位(从0开始计)),即从第6位开始读取。

5.2 设置流位置

在获取流的位置之后,我们可以去设置流的位置。即对一个流进行了一系列读取之后,可以通过设置流的位置来改变当前流已经读取或写入的位置,接下来会从新的位置读取或写入。

5.2.1 seekg() / seekp() 用于设置输入 / 输出流的位置

需要注意的一点,seekp() 用于设置输出流的位置,如把输出流的当前输出位置挪到当前流的前面(中间位置),而不是已知放在最后,接下来,流的行为是什么?是覆盖(即指向了一个原先已经输出过的位置,接下来在进行输出时,是覆盖之前的输出,而不是插入)!!!

因为实现覆盖这件事情比实现插入,要容易。要想实现插入,c++这种标准IO流是不直接支持的,需要编写相应的逻辑去实现相应功能。

tellg和seekg是用来获取输入流位置和设置输入流位置的;tellp和seekp获取输出流位置和设置输出流位置的。既然这样为什么不能在流里面就直接定义tell和seek,输入流提供tell、seek来获取位置、设置位置;输出流提供tell、seek来获取位置、设置位置,为什么要区分g和p呢?

这是因为任何流(如文件流,内存流)都包含输入流输出流,同时还包含双向流。双向流可以同时进行输入和输出(故本质上需要保留两个位置信息,一个位置信息表示当前输入读取的位置;另一个位置信息表示当前输出读取的位置信息),相应地,要进行输入输出,需要通过两个函数来区分当前要操作的是输入流还是输出流。

5.2.2 这两个方法分别有两个重载版本

  1. seekg
    在这里插入图片描述
    上图,seekg有两个函数声明。换句话说seekg是一个重载的方法。
5.2.2.1 设置绝对位置:传入 pos_type 进行设置

第一个版本接收pos_type这样的类型的对象,表示移动到一个绝对的位置;

5.2.2.2 设置相对位置:通过偏移量(字符个数 ios_base::beg ) + 流位置符号的方式设置
  1. ios_base::beg
  2. ios_base::cur
  3. ios_base::end

第二个版本接收了两个参数(第二个参数指基本位置(dir可以取3种值:beg、end、cur),即beg代表我是从流的开始位置作为基本位置,end指流的结尾作为基本位置,cur指流位置指示器的当前位置作为基本位置),在确定了基本位置之后,第一个参数是off_type off,即偏移量,即我们是通过基本位置(偏移量)来确定我们要移动的位置。

如我们第二个参数可以取beg,off取3,即表示我要把当前位置移动到流的开头往后流的第3个字节位置进行读取;
再如我们第二个参数可以取cur,off取-3(off_type可取正负),即表示从流当前位置开始往前移动3个位置,作为新的位置;
故第二个版本基本上是通过基本位置(偏移量)来进行移动;而第一个版本是通过绝对位置来进行移动。

例子:
8行:定义了一个输入流(in)
接下来11行读入word1(一个字符串,会读到Hello,
9行:seekg(0)是调用的版本1(如果不调用in.seekg(0),流的读取位置在“Hello, world”中的空格处;如果调用in.seekg(0),流的输入的当前位置在“Hello, world”的H那里,此时再去调用word2,然后进行读取,就能够获取到word2也是Hello,
在这里插入图片描述

  1. seekp
    6行:构造输出流,里面包含“hello, world”;
    7行:seekp(7)即把hello, world的当前输出位置挪到第7位(即w那里)
    8行:输出W,即把w换成W;
    9行:挪到流的最后一个位置
    10行:最后一个位置加!
    11行:seekp(0),挪到hello, world开头位置
    12行:将开头位置的h换为H
    在这里插入图片描述

6 流的同步

意图:现在屏幕上打出what’s your name,然后等待用户输入名称,然后保存在name这样一个c++字符串(8行)中:
在这里插入图片描述
那么它和我们之前说的缓存区有什么关系?

上图7行的std::cout是和终端相关联的输出流,现在把what’s your name放入std::cout里面,会把what’s your name放到缓存区里面,只有在缓存区积累到一定程度,缓存区的内容才会一次性输出到终端,假设缓存区比较长,what’s your name没有把缓存区占满,那么会一直在缓存区里放着,接下来执行第9行,等待用户输入一个名称,那么按照传统的这种基于缓存区的方法,会有一个问题:what’s your name还在缓存区,没有输出到终端,因此用户看到的是终端停在那了,在等待用户输入,但是这个输入到底是啥?用户不知道,即用户不明白为什么要输入?要输入什么东西?(因为终端没有显示what’s your name)

以上情况不是我们希望看到的,我们希望看到我们在等待用户输入之前,what’s your name就应该显示在终端。无论what’s your name是否占满了cout缓存区。

另一种情况,我们在写文件(有很多信息要写进去),文件也有缓存区,也是需要等待缓存区写满之后,才把缓存区的内容一次性读到文件里面。但是不断写,写到文件最后,可能剩一点尾巴,这点尾巴实际上并没有把缓存区占满,此时如果我们简简单单地文件关闭了,位于缓存区内部的这点尾巴没有写入文件中,造成文件失去了完整性。

实际上,无论是cout和cin交互的问题,还是刚才提到的文件的问题,都有一个特点:我们是有缓存区,但是我们不能采用系统缺省的行为(即缓存区满了之后,才把缓存区里面的内容放到外部设备上),我们需要一种方式来刷新缓存区,即缓存区还没满,但是缓存区里面的内容也能放在外部设备上,这样的刷新的过程就叫流的同步

6.1 基于 flush() / sync() / unitbuf 的同步

流会提供一系列方法来进行同步,如flush() / sync() / unitbuf。

6.1.1 flush() 用于输出流同步,刷新缓冲区

调用 flush,它会把缓存区里面的内容丢到外部设备上,相应地把缓存区情况。
其中调用flush刷新缓存区有两种方法:

  1. 所有的输出流都会提供flush方法,只要我们调用flush,就能完成输出缓存区的刷新
    在这里插入图片描述

  2. 我们也可以使用操纵符进行刷新缓存区在这里插入图片描述
    在这里插入图片描述
    故对于下图程序,在写完what’s your name之后,我们希望显式地对cout的缓存区内容刷新。有两种方法:
    在这里插入图片描述

  3. 在这里插入图片描述

  4. 在这里插入图片描述
    实际上,还有另外的方法进行缓存区刷新:

endl干了两件事情:

  1. 在what’s your name后写入回车;
  2. 刷新缓存区

6.1.2 sync() 用于输入流同步,其实现逻辑是编译器所定义的

在这里插入图片描述
关联数据:比如我的这个东西是文件流,从终端里面进行输入的,那么我的数据源就是文件或终端。
输入缓存区:构造了个流对象,流对象内部的缓存区

为什么一些情况下需要使用sync?

比如同一个文件,这个文件关联了两个流,一个输出流,一个输入流。我们的输出流不断地向文件里面写或者修改流里面的内容,输入流不断地读取。但是在一些情况下,在读之前,我们需要对输入的缓存区进行刷新,来确保我们能够读到之前输出流写到这里面的东西,此时需要调sync。

但上图sync最后有一句话:
在这里插入图片描述
为什么可以这样?

这和底层文件系统的实现有关,有的文件系统在实现完之后,即在输出完这个东西,就能自动把sync对应的缓存区刷新掉。而有些文件系统实现,想对输入缓存区进行同步,就必须要做一些实际操作。那么上图的操作做还是不做,实际上由库所定义的。

相对flush而言,sync用于输入流的同步,但是实际应用过程中,使用sync的可能性小。

6.1.3 输出流可以通过设置 unitbuf 来保证每次输出后自动同步

在这里插入图片描述
unitbuf和showpos很像,都是位掩码,用来定义位,把它设置为true或false。我们可以使用设置showpos的方法来设置unitbuf。

更常见的是使用操纵符:
在这里插入图片描述
无论是输入流的缓存区还是输出流的缓存区,都是比较大的(只有比较大的缓存区,才能存储足够的东西,在此基础上,缓存区一旦刷新,就能把这大批数据扔给终端,或者从终端获取大批数据)。

什么是unitbuf?
unit:单元
unitbuf:buf的长度只有一个字节。

本质上,unitbuf的含义是:任何写入的东西,只要进行写入,那么这个写入的东西至少要大于等于一个字符,换句话说,只要写入一个东西,它一定会立即被放置到终端里,因为缓存区太小了,这就叫automatic flushing
在这里插入图片描述
即上图,cout本身是有一个缓存区的,缓存区里面可能包含比较长的内容。接下来在进行后续处理时,使用操纵符为cout设置一个unitbuf(enable automatic flushing那一行),表明把缓存区关掉了,我们不再往缓存区里写东西。那么任何一个写入的操作,我们会直接把写入的信息扔到终端上。这样做的好处是信息能立即反映到终端上,不会有延迟,但是坏处是影响程序性能。


除了cout之外,还有一些其他的系统构造出来的对象,和屏幕关联,其中一个是cerr。但cout和cerr是有区别的,cout输出到的位置是标准输出,coerr输出到的位置是标准错误输出,通常来讲,标准输出和标准错误输出都会输出到屏幕,但是也不是一定。

cout和cerr还有一个区别:cout设置了缓存区,cerr在缺省情况下,会被设置成unitbuf,换句话说,cerr里面的内容直接会显式出来,不涉及缓存区刷新问题。这是因为cout和cerr的使用场景不同,cout的目的是一般意义上的输出,输出慢点没关系。但是输出到cerr里面的是错误信息,对于错误信息,我们希望及时反映到终端上,或者相应设备上。

6.2 基于绑定 (tie) 的同步

6.2.1 流可以绑定到一个输出流上,这样在每次输入 / 输出前可以刷新输出流的缓冲区

第一个流是c++的IO流(可以是输入流也可以是输出流),第二个流是输出流。

如下图,A:流对象;B:输出流;A绑定到C上,即A里面记录了C相关的信息。
在这里插入图片描述
同理,B也可绑定到C上:
在这里插入图片描述
即,多个流可以绑定到同一个输出流上,但一个流不能绑定到多个输出流上。

为什么要绑定?流可以绑定到一个输出流上,这样在每次输入 / 输出(第一个流(A、B)的输入输出)前可以刷新输出流(C)的缓冲区。即假设A绑定到C上,无论A是输入流还是输出流,接下来输入输出操作前,一定会刷新C的缓存区。

6.2.2 比如: cin 绑定到了 cout 上

如下图程序:
在这里插入图片描述
cout是输出流,cin是输入流,同时cin绑定到cout上,即cin在进行任何输出之前,都会对cout中的内容进行刷新。故What’ s your name一定能打印出来。What’ s your name一开始存储在cout的缓存区里,9行使用cin获取内部内容之前,会把cout的缓存区刷新。
在这里插入图片描述

6.3 与 C 语言标准 IO 库的同步

6.3.1 缺省情况下, C++ 的输入输出操作会与 C 的输入输出函数同步

6.3.2 可以通过 sync_with_stdio 关闭该同步

在这里插入图片描述
上图false,解除c和c++的标准 IO 库同步。系统能保证a一定出现在c前面,但是解除了c和c++的标准 IO 库同步,因此,使用c语言打印的b,应该出现在a,c的前面还是后面,这是不确定的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

cashapxxx

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值