第五章-标准I/O库

流和FILE对象

当使用标准I/O打开一个文件时,就用一个流与该文件关联了,一个流被创建时是没有定向的,即没有告知其读写的是单字节还是多字节(宽字符,wchar),若第一次使用的是多字节函数则定向为宽定向,若使用的是单字节函数则定向为字节定向。

fwide并不改变已定向流的定向,mode为负数则定向为字节定向,为正数则定向为宽定向,为0则不改变并返回该流定向的值。

当我们用fopen打开文件,返回指向FILE结构的指针,该结构包含了管理流的所有信息,后续的所有标准I/O操作都会用到该指针来操作流。把FILE指针成为文件指针,也可以理解成这才是真正的流对象。

标准输入、输出、标准错误

对每个进程预定义了这三个流,这三个标准I/O通过预定义了stdin,stdout,stderr文件指针加以运用。这些指针存在于头文件<stdio.h>中

标准输入、输出、错误其实默认关联到我们的显示器和键盘了,所以对他们进行操作就是对这些设备进行操作。

缓冲

分为三种:

全缓冲

只有在填满了缓冲区后,才进行实际的I/O操作(read,write)。可以调用fflush函数,冲洗刷新一个流,即使其没填满缓冲区也会执行I/O。

行缓冲

遇到换行符的时候,执行实际的I/O操作,若遇到换行符之前已经填满缓冲区了,也会执行实际的I/O。一般对于一个终端都是行缓冲。

若从不带缓冲的流或者带行缓冲的流中得到输入数据,会立刻冲洗刷新所有行缓冲输出流。

不带缓冲

标准I/O库不对字符进行缓冲(fput函数),标准错误就是不带缓冲的,所以能够立刻执行实际I/O,尽快显示错误信息。

很多系统默认若标准输入和标准输出指向交互式设备,其是行缓冲的,否则是全缓冲的,而标准错误是不带缓冲的。

可以自己设置自己的缓冲区,我们打开流默认用的时标准I/O缓冲区,

setbuf是设置一个流的缓冲区,其缓冲类型系统会进行判断(是否为终端设备),若关闭缓冲则buf为NULL;

setvbuf能够精确设置缓冲类型,mode有_IOFBF全缓冲,_IOLBF行缓冲,_IONBF不带缓冲。

合适长度是常量BUFSIZE指定的值。

某些实现中会将管理操作信息也放到缓冲区中,所以实际存放有效数据的字节数会少于给定的缓冲区大小。

应由系统选择缓冲区的大小并自动分配缓冲区,在这种情况下,在关闭流的时候,标准I/O库会自动释放缓冲区。若函数内部分配了一个缓冲区,需要在结束返回前关闭该流,这样系统会自动释放缓冲区。

缓冲区满了或者使用fflush冲洗刷新缓冲区了,则会进行write系统调用

打开流

fopen按照type模式打开文件,并关联到一个流。fdopen将文件描述符与一个流关联。freopen一般将一个预定义的流关联到指定文件,若该流已打开则先关闭,若已定向则清楚定向。

其中b用来区分二进制文件和文本文件,但由于unix系统不区分两者,所以指定b并无效果。

其中对于fdopen,使用a必须保证该文件存在,而使用w不会进行截断,因为该文件描述符已经被打开了。

由此可见,除去fdopen对于a和w的特殊性,一般的文件若用a和w模式打开,其不需要保证该文件已存在,所以会创建新文件,此时创建的文件访问权限设置了用户,组和其他的读写权限,可以通过umask进行创建屏蔽。

在调用fclose之后,所有未写的存在缓冲区的输出数据(写的数据)都会被冲洗刷新,而缓冲区的所有输入数据(读的数据)都会被丢弃,如果标准I/O为流自动分配了缓冲区,关闭后会自动释放缓冲区。

当一个进程正常退出时,所有未写缓冲数据都会被冲洗刷新,所有的标准I/O流都被关闭。

读和写流

输入函数(读)

#include<stdio.h>

int getc(FILE*fd);

​

int fgetc(FILE*fd);

​

int getchar(void);

getchar相当于getc(stdin);getc可被实现为宏,fgetc一定是个函数,所以一般fgetc比getc慢,因为通常调用函数比调用宏慢。返回的int是该字符对应的整型值,-1代表出错或者到达了EOF文件末尾。

在GNU C库中,宏被扩展为函数调用,所以此时的宏调用与函数调用效率上相近。

注意ferror和feof返回的值是用来做判断区分的。ungetc可以把任何一个字符压送回流,支持任意次数的回送,但每次只能回送一个字符。不能回送EOF字符,ungetc回送前会嗲用clearerr清除EOF标志,然后再补回EOF标志,所以即使当前文件已经EOF了,亦可以正常压送。流是一个队列结构,但压送回去不是压到队尾,是压到队头,所以可以getc判断是否是我们想要的字符后再放回去,所以我们压送的顺序和读出的顺序是相反的。

ungetc压送回流,只是把字符放到缓冲区了,并没有对文件进行实际的I/O

输出函数(写)

注意出错返回的是EOF而成功返回的是所写的字符的整型值。

每次一行I/O

不推荐使用gets,而且也不要使用它,因为它不能指定缓冲区长度,所以若所读的行字节大于缓冲区,则会溢出且发生不可预知的后果,曾经是个漏洞。

gets是从stdin读取一行,换行符不会放到缓冲区中;fgets从指定的流中读取,并指定了缓冲区的大小,所读取的字节不会超过n-1,因为每次读取都会在最后补空字符,换行符会放在缓冲区中,若该行长度大于n-1则下次调用会继续读取该行

这里其实不一定说一定要写一行,可以理解成一次写多个字符,换不换行自己选。

puts对stdout写入一数据,fputs对给定流写入数据,尽量不用puts虽然不是安全问题,但是因为它会随后将换行符写到流中,避免记忆,统一对读写一行我们都用fgets和fputs,这样我们只需要记着自己需要处理换行符就行。

对于写入,我们写的是一个c_str,以空字符结尾,但空字符不写入流中,我们需要自己补换行符,才能实现流的换行。

系统调用比普通函数调用需要花费更多的时间

标准I/O的效率与直接调用read和write差不多

二进制I/O

对于fgets,遇到换行符或空字符会忽略,不会读取到该字符;对于fputs,遇到空字符就停止写入,空字符也不会写到二进制中,这种情况下如果想要进行二进制I/O就不能用这些函数。

ptr是要读写的对象的地址,size是该对象的大小,nobj是要读写的对象数,返回读写的对象数

对于数组,size是数组元素的大小,nobj是要读写的元素个数,对于结构体size是结构体的大小,nobj 是结构体的个数。

对于二进制文件的I/O需要注意在不同系统之间可能会出问题,一方面是不同的变量的字节数可能不同系统不一样以及结构体可能不同系统的编译器存放的顺序也不一样;另一方面可能不同系统的对齐方式不同,也会导致系统之间I/O有问题。需要有统一的调节,例如网络结构中的转换层,。

定位流

long ftell(FILE*fp);用于返回当前流的位置,对于二进制文件是返回字节位置

int fseek(FILE*fp,long offset,int whence);whence和lseek一样,有SEEK_SET,SEEK_CUR,SEEK_END三种,二进制文件三个都可以用,需要指定offset,此时与lseek基本一样,而文本文件只能使用SEEK_SET,且offset必须为0或者ftell的返回值。前两个函数出错都返回-1

void rewind(FILE*fp);将流定位到起始位置

函数ftello,fseeko,与前面功能一样,只是定位的位置类型不是long而是off_t类型。

fgetpos,fsetpos是ISO C引入的函数,且如果在不同的系统中使用定位流函数时,推荐使用该函数,该函数管理一个fpos_t的对象,表示文本位置指示器,

int fgetpos(FILEFP,fget_tpos); 将文本位置指示器的当前值存到pos中;int fsetpos(FILEfp,fpos_tpos);用pos中的值来定位流

格式化I/O

格式化输出

printf将格式化数据输出到标准输出,fprintf输出到指定流中,dprintf输出到指定文件描述符中;

sprintf和snprintf将数据写到缓冲区buf中,在最后会添上空字符,但其返回的字符数不包含空字符,sprintf有可能会溢出,snprintf指定写入多少个字符。

格式化字符串format注意是c_str,其中转换说明以%开始,其他不是转换说明的都照常输出。

格式化输入

格式化输入根据格式字符串的控制说明,将相应的值赋给后面的参数,这些参数要给出其地址,也即这些参数类型都是指针类型。*号用来抑制转换,按照转换说明对输入进行转换,但转换结果不会存放到对应参数中。m是赋值分配符,强制内存缓冲区分配空间以接纳转换字符串。

格式化输入,一旦输入不符合格式,就在该位置终止,停止后续的格式化输入了,不再读取剩余部分。

实现细节

标准I/O底层是调用系统I/O进行实际磁盘I/O的,所以每个流都关联到一个文件描述符,可以用fileno来得到流关联的文件描述符

int fileno(FILE*fp);

临时文件

char* tmpnam(char* ptr);如果ptr为NULL,则函数产生的唯一路径名会存在静态区中,后续的tmpnam调用会重写该区,返回该路径名字符串的指针;所以需要保存该路径的副本而不是保存它的指针。如果不是NULL,则将产生的路径名写到ptr对应的数组中,并返回ptr。

FILE* tmpfile();创建一个临时的二进制文件(wb+),关闭文件或者程序结束时会自动删除该文件。

注意返回的类型和值不一样,前者返回名字,后者返回文件描述符

template可以用于指定生成的临时文件的位置和名字:template="/usr/local/dirXXXXXXX",函数将占位符X进行替换,得到唯一的路径名。

mkdtemp生成临时目录,默认有用户的读写执行权限,可以通过创建屏蔽字进行屏蔽。

mkstemp生成临时文件,默认有用户的读写权限,也可通过创建屏蔽字进行屏蔽。

但这两者不会对文件进行自动删除,需要用户自己进行unlink。

tmpnam函数在返回路径名和创建文件之间有时间窗口,该窗口内其他进程有可能用改路径名创建了文件。而mkstemp和tmpfile不存在该问题,应该使用这两个。

标准I/O把数据缓存在内存中,缓冲区再与主存进行数据交换,因此每次一字符和一行字符的I/O更有效。

内存流

fmenopen可以指定一个缓冲区buf,和缓冲区大小size,缓冲区冲buf位置开始算起,type与基于文件的标准I/O相同,例如r,w,a,r+,w+,a+等。若buf为NULL,则系统自动分配一个缓冲区。

但对于内存流,a+,以追加写的方式打开内存流时,当前文件位置会定位到缓冲区中第一个null字符的位置,若没有null字符则定位到缓冲区的尾后位置。

追加写模式通过第一个NULL字节确定数据的尾端,所以内存流不适合于存储二进制数据(二进制数据在数据尾端之前可能有多个null字节)

标准I/O的缺点

之所以这里提到了标准I/O缓冲区和用户缓冲区,实际上是因为我们打开一个流的时候,系统给我们分配了一个缓冲区,这个就是标准I/O缓冲区,我们对这个流的读写,就会涉及到内核,标准I/O缓冲和用户缓冲之间的拷贝过程。读写一般都涉及了两次拷贝。