包括Unix在内的很多操作系统都实现了标准I/O库,标准I/O库处理缓冲区分配、以优化的块长度执行I/O等很多细节,以便于用户使用,但同时也需要深入的了解I/O库函数的操作,避免带来问题。
标准I/O库的操作是围绕流(stream)进行的,使用标准I/O库打开或创建一个文件即使一个流和一个文件相关联。
ASCII字符集使用一个字节表示一个字符,国际字符集使用多个字节表示一个字符。标准I/O文件流可用于单字节或多字节(宽)字符集。流的定向决定了读写的字符是单字节还是多字节。新创建的流没有定向,先使用单字节I/O函数操作未定向的流,则将流的定向设置为字节定向,相反则设置为宽定向。freopen函数可以清除一个流的定向,fwide函数可以设置未定向流的定向。
标准I/O函数fopen返回一个指向FILE对象的指针(称为文件指针),为了引用流需要将该指针作为参数传递给每个标准的I/O函数。FILE结构体包含了标准I/O库管理流的所有信息:
一个进程预定义了3个可以自动被进程使用的流:标准输入、标准输出和标准错误,分别用文件指针stdin、stdout、stderr加以引用。
标准I/O库对每个I/O流自动进行缓冲管理,目的是尽可能减少使用read和write系统调用的次数。缓冲类型有如下三种:
全缓冲
填满标准I/O缓冲区后才进行实际I/O操作,驻留磁盘上的文件通常是全缓冲。缓冲区可由标准I/O函数自动冲洗(将缓冲区的内容写到磁盘上),也可以调用fflush函数冲洗。
行缓冲
输入和输出中遇到换行符后填满缓冲区时,标准I/O执行I/O操作。当流涉及到终端(标准输入和标准输出)时,通常使用行缓冲。
通过标准I/O库从内核系统调用(不带缓冲的流或者暂无缓冲的行缓冲流)而得到输入数据,会冲洗所有行缓冲输出流。
不带缓冲
标准I/O库不对字符进行缓冲存储。标准错误流stderr通常是不带缓冲的,以使错误尽快显示。
setbuf系列函数可以在流打开后执行其他操作钱调用而更改缓冲类型。缓冲的长度通常应由操作系统选择(BUFSIZ常量或stat结构的st_blksize字段),setvbuf函数时也可以手动指定。
fopen系列函数打开一个标准的I/O流:
打开流时的type参数hiding对流的读写方式,主要有15种值:
追加写类型打开一个文件后,每次写都将写到文件的当前尾端处,即使多个进程追加写方式打开同一个文件也能正确写入。
以读和写类型打开文件有下列限制:
使用fclose可以关闭一个打开的流,文件被关闭前,会冲洗缓冲中的数据,缓冲中的任何输入数据被丢弃,标准io库自动分配的缓冲区被释放。
打开流后可以使用以下3种不同类型的非格式化I/O进行读、写操作:
getc、fgetc、getchar(等于getc(stdin))可用于一次读一个字符,为了能在出错或达到文件末尾时返回EOF(-1),返回值会将unsigned char类型转换为int类型。如要区分是出错还是到达文件尾端,需要使用ferror或feof函数,它们分别读写FILE结构中的出错标志和文件结束标识;使用clearerr可以清除这两个标志。
从流中读取数据以后,可以调用ungetc将字符再压送回流中。压送回流的字符以后可以又从流中读出,但读出的顺序与压送回的顺序相反。每次只能回送一个字符,但允许任何次数的回送。回送的字符可以是EOF之外的任意字符,已到达文件末尾仍然可以回送字符并正常读取(成功ungetc回清楚文件结束标志)。回送的字符只是写回标准I/O的缓冲中而并没有写到底层文件或设备上。在切词或几号切分时会经常用到回送字符操作。
putc、fputc、putchar(等于putc(c, stdout))函数可用于一次写一个字符。
fgets、gets(不推荐使用)函数提供每次输入一行的功能,需要指定缓冲区地址。fgets函数需要指定缓冲区长度,缓冲区以null字符结尾,当行内容超过该长度时返回的是不完整的行,常常需要循环多次读取。
fputs、puts(避免使用)函数提供每次输出一行的功能。fputs将一个以null字符结尾的字符串写到指定的流,null字符不写出。如null字符前没有换行符则并不是输出一行。
getc和putc函数可以实现为宏,因此相比fget从和fputc效率可能更高。fgets和fputs为了比getc和putc高效,一般会采用汇编编写的memccpy函数。标准I/O库与直接调用read和write函数相比并不慢很多。
fread和fwrite函数可以执行二进制I/O操作,读写一个二进制数组或结构,返回读写的对象数。二进制I/O通常只能读在同一个系统上已写的数据,在一个系统上写的数据要在另一个系统上处理则不能正常工作,原因是:
有三种方法可以定位标准I/O流:
需要移植到非Unix系统上的程序应当使用fgetpos和fsetpos函数。
printf系列函数用来格式化输出,其中sprintf函数可能造成缓冲区溢出而推荐使用snprintf函数。 格式转换说明格式如下([]中是可选部分):
%[flags][fldwidth][precision][lenmodifier]convtype flags: 标志部分 ‘ 将整数按千位分组 - 字段内做对齐输出 + 总是显示带符号转换的正负号 ( ) 如第一个字符不是正负号则加上一个空格 # 指定另一种转换形式(如16进制加0x前缀) 0 添加前导0而不是空格进行填充 fldwidth: 最小字段宽度,空格填充 precision: 整型转换后最少输出数字位数、浮点数转换后小数点后最少位数、字符串转换后最大字节数 lenmodifier: 参数长度 hh signed或unsigned char类型输出 h signed或unsigned short类型输出 l signed或unsigned long或宽类型输出 ll signed或unsigned long long类型输出 j intmax_t或uintmax_t z size_t t prtdiff_t L long double convtype: 转换类型 d,i 有符号十进制 o 无符号八进制 u 无符号十进制 x,X 无符号十六进制 f,F 双精度浮点数 e,E 指数格式双进度浮点数 g,G 根据转换后的只解释为f、F、e或E a,A 十六进制指数格式双精度浮点数 c 字符(有长度修饰符l则为宽字符) s 字符串(有长度修饰符l则为宽字符) p 指向void的指针 n 到目前为止printf调用输出的字符数目 % 一个%字符 C 宽字符(lc) S 宽字符串(ls)
vprintf系列变体函数可以使用arg参数替换可变参数表(…)
scanf系列函数用来执行格式化输入处理,它们分析输入字符串并将字符序列转换成指定类型的变量,格式之后的各参数包含了变量的指针,转换结果对这些变量赋值。
格式转换说明 格式如下:
%[*][fldwidth][m][lenmodifier]convtype *: 抑制转换,执行转换但不设置结果 fldwidth: 最大宽度(最大字符数) lenmodifier: 参数长度,与格式化输出的参数长度相同 convtype: 转换类型,类似格式化输出的转换类型, d 有符号十进制,基数为10 i 有符号十进制,基数由输入格式决定 O 无符号八进制(输入可选地有符号) u 无符号十进制,基数为10(输入可选地有符号) x,X 无符号十六进制(输入可选地有符号) f,F, e,E, g,G, a,A 以上均为浮点数 c 字符(有长度修饰符l则为宽字符) s 字符串(有长度修饰符l则为宽字符) [ 匹配列出的字符序列,以]终止 [^ 匹配出列出字符意外的所有字符,以]终止 p 指向void的指针 n 到目前为止函数调用读取的字符数写入到指针所指向的无符号整型中 % 一个%字符 C 宽字符(lc) S 宽字符串(ls)
标准I/O库最终都要调用第3章中说明的I/O例程,每个标准I/O流都有一个语气相关联的文件描述符,可以对一个流调用fileno函数以获取其文件描述符。
第一个I/O操作通常造成为该流分配缓冲区。当标准输入、输出连接至终端时是行缓冲的,行缓冲的长度是1024字节;连接至文件时是全缓冲的,缓冲区长度是该文件系统优先选用的I/O长度(stat结构的st_blksize值)。标准错误不带缓存,普通文件是全缓冲的。
tmpname函数产生一个与现有文件名不同的一个有效路径名字字符串。每次调用都返回一个不同的函数名,最多调用TMP_MAX次。每次生成的文件名存储在固定的静态区,如需留存需要保存文件名复本(而非指针)避免被覆盖。tmpname不是一个原子操作,应尽量避免使用。
tmpfile创建一个临时二进制文件(类型wb+),在关闭该文件或程序结束时将自动删除这种文件。tmpfile通常的Unix实现是调用tmpname生成一个路径名,然后使用该路径名创建一个文件,并立刻unlink它。
mkdtemp和mkstemp函数可以指定文件名模板创建一个临时目录或文件,模板是后六位为”XXXXXX”字符串的文件名,如:/tmp/fileXXXXXX、/tmp/dirXXXXXX,成功后返回临时文件的路径,创建的文件不会自动删除。模板字符串需要使用字符数组而不是字符串指针,因为编译器只将指针本身在栈上,字符串存放在可执行文件的只读段,mkdtemp和mkstemp函数尝试修改文件名时会出现段错误。
标准I/O库把数据缓存在内存中以提高I/O效率,也可以通过调用setbuf或setvbuf函数让I/O库函数使用我们自己的缓冲区。SUSv4系统中支持了没有底层文件的标准I/O流:内存流。这些流仍然使用FILE指针进行访问,看起来像文件流,但更适用于字符串操作。
fmemopen函数允许调用者提供缓冲区用于内存流,和基于文件的标准I/O流差异如下:
无论何时以追加写方式打开内存流,当前文件位置设为缓冲区的第一个null字节,如缓冲区不存在null字节则当前位置设为缓冲区结尾的后一个字节。流不以追加写方式打开时当前位置为缓冲区的开始位置。内存流不适合存储二进制数据。
如果buf参数是一个null指针,打开流进行只读或者只写都没有任何意义。
任何适合需要增加流缓存区中的数据量并且调用fclose、fflush、fseek、feeko、fsetpos函数时都会在但却位置写入一个null
open_memstream和open_wmemstream函数创建面向字节或面向宽字节的流。与fmemopen函数的不同是:
使用缓冲区地址和大小需要遵循原则:
内存流避免了缓冲区溢出,非常适用于创建字符串,对于把标准I/O流作为参数用于临时文件的函数来说有很大的性能提升。
标准I/O库并不完善,某些不足之处属于基本设计但大多数是与各种不同的实现有关,标出I/O库的效率也不高,与它需要复制的数据量有关。
快速I/O库、sfio库、mmap函数、cClibc C库、Newlib C库等提供了各种替代性的I/O操作。
更多有关 《Unix环境高级编程 3》的读书笔记 ,请关注 :
http://tabalt.net/blog/advanced-programming-in-the-unix-environment-3-reading-notes/