1、简介
在本章,我们描述了标准输入输出库。由于这个库也被许多非UNIX的操作系统实现了,所以这个库由 ISO C
标准指定。其它的接口被 SUS
定义,作为 ISO C
标准的扩展。
标准输入输出库处理了诸如缓存分配,以及分配优化的大小的块进行 I/O
,这样我们就不必担心如何使用正确的块大小进行 I/O
了(如原书3章9节所描述的那样)。这样,这个库就很容易使用了,但是同时,也引入了我们没有预料到的其他一些问题。
标准 I/O
库由Dennis Ritchie在大约1975年编写,它作为Mike Lesk编写的可移植 I/O
库的主要版本.并且在30年的期间内,对它改动非常少。
译者注
原文参考
2、流和文件描述符号
我们使用系统调用来操作文件的时候,是通过文件描述符号来标识这个文件,如果我们使用标准库函数来进行文件操作的我们通常是把一个 stream
和这个文件相关联了,然后通过这个 stream
对这个流进行操作,也即,标准 I/O
库是以流( stream
)为中心进行操作的。
当我们使用库函数 fopen
打开一个文件的时候,会返回一个指向 FILE
对象的指针,这个对象包含了 I/O
库管理 stream
需要的所有信息,例如被操作的文件的描述符号,指向这个 stream
的缓存的指针,以及缓存的大小(注意这个缓存和系统调用时可能经过的内核的缓存不同,经过内核的缓存更接近下层,在第3章8节讨论 I/O
效率时的最后也提到过,但这属于内核驱动范围了,可以暂时忽略),当前缓存中的字符数目,错误标志等等。我们把这个 FILE
对象做为参数传递给标准输入输出库函数,就可以实现相应的操作了(使用库函数操作,也不用费心去关心输入输出缓存设置为多大才好了)。
应用程序不用检验 FILE
对象。引用一个流,只需将 FILE
指针作为参数传递给每个标准 I/O
函数。本书中,我们称指向 FILE
的指针(类型为 FILE*
)为文件指针。
通常,有单字节和多字节字符。 stream
的 orientation
用于决定读写的是单字节字符还是多字节字符。在最开始一个 stream
创建的时候,它是没有 orientation
的,这时候,如果使用了一个多字节 I/O
函数对这个 stream
进行操作(例如 <wchar.h>
中的函数),那么这个 stream
就被设置为 wide-oriented
的;同理如果用单字节的 I/O
函数对这个 stream
进行操作,那么这个 stream
就被设置为 byte-oriented
的。一旦设置了 stream
的方向,除非被关闭,否则就不能改变了。当一个 stream
被 set
的时候,只有两个函数可以改变这个 stream
的 orientation
。它们是: fwide
和 freopen
.
freopen
原型如下:
FILE *freopen(const char *path, const char *mode, FILE *stream);
它会把 stream
的 orientation
给 clear
.把原来的 stream
给关掉,然后把参数指定的新的 stream
和这个文件相互关联。
fwide
原型如下:
#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp, int mode);
这里,如果 mode
是负数,那么设置成 byte-oriented
;如 mode
是正数,那么设置成 wide-oriented
;如果 mode
是0那么不会做设置,但也返回当前流的 orientation
值。
需要注意的是 fwide
不会改变已经被 oriented
的 stream
,也不会返回 error
(也就是那个流刚开始被创建之后没有被使用过就没有被设置过,这个时候则可以用这个函数来设置)。
译者注
原文参考
3、标准输入、标准输出和标准错误
有三个重要的流: stdin
, stdout
, stderr
表示标准输入,输出和错误流,它们对应的文件描述符号是: STDIN_FILENO
, STDOUT_FILENO
,和 STDERR_FILENO
.
译者注
原文参考
4、关于库函数和 buffer
使用标准输入输出( Standard I/O
)库函数进行操作的目的就是为了使用最小数目的的 read
和 write
系统调用(通过自动管理设置多大的缓存等等)。 Standard I/O
一般选择合适的大小来分配缓存。可能是 BUFSIZ
常量的值(在 <stdio.h>
中定义),也可能是 stat
结构中的 st_blksize
成员。
(1)三种类型的缓存
-
Fully Buffered
.在这种情况下当stand i/o
的缓存被填满的时候才会发生i/o
.一般使用standard i/o
操作磁盘上面的文件的时候使用的是这种类型的buffer
.这里需要注意在UNIX环境中,刷新(
flush
)这个术语有两种意思。在标准I/O
库方面,刷新意味着将缓存中的内容写到磁盘上(该缓存可以只是局部填写的)。在终端驱动程序方面(例如在第11章中所述的tcflush
函数),刷新表示丢弃已存在缓存中的数据。这里的刷新(flush
)是指标准I/O
缓存的写操作。缓存可由标准I/O
例程自动地刷新(例如当填满一个缓存时) ,或者可以调用函数fflush
刷新一个流。 -
Line Buffered
.在这种情况下,当输入或者输出的时候遇到了一个换行符号的时候才会发生i/o
.一般终端的标准输入和标准输出会用到这种类型的缓存。对于这种
Line Buffered
,需要注意的是:Line Buffer
的缓存大小是固定的,当遇到换行符号之前缓存被填满了的时候,也会发生i/o
。还有一个就是:当从一个unbuffer
的方式进行请求输入的时候;以及当从Line Buffered
请求输入的时候;这两种情况下也都会刷新(flushed
)行缓存输出的缓存。 Unbuffered
.standard i/o
不会缓存字符。一般标准错误流会使用这种类型的缓存,这样可以保证信息尽可能快地出现。
ISO C
规定:
-
standard input /output = 当引用的不是交互式的设备的时候,是 =fully buffered
. -
standard error
一定不能是fully buffered
.
但是,上面的规定却不是非常确定的,一般而言:
-
standard error
是unbuffered
. - 其他的
stream
如果它们引用的是terminal
设备,那么就是line Buffered
;否则是fully buffered
.
(2)设置缓存
如果不是用默认的缓存类型,有两个可以设置缓存的函数:
#include <stdio.h>
void setbuf(FILE *restrict fp, char *restrict buf);
int setvbuf(FILE *restrict fp, char *restrict buf, int mode, size_t size);
这两个函数必须在 stream
被 open
之后,并且任何 i/o
操作之前,被调用。
使用 setbuf
我们可以设置和取消 buffer
.如果设置 buffer
,指向 buf
的指针的数据的大小必须是 BUFSIZ
,一般这样 stream
就是 fully buffered
.但是如果 stream
是和 terminal
相关联的也有些系统会把 buffered
设置为 line buffered
的。如果取消 buffer
,那么 buf
参数是 NULL
的。
使用 setvbuf
,我们可以精确地指定我们想要的 buffer
类型。我们通过 modde
参数来进行指定:
-
_IOFBF:
表示fully buffered
. -
_IOLBF:
表示line buffered
. -
_IONBF:
表示unbuffered
.
如果我们指定为 unbuffered
,那么 buf
和 size
参数会被忽略.
如果指定了 fully buffered
或者 line buffered
那么:我们可以通过 buf
和 size
来设置缓存和大小。如果这时候的 buf
参数是 NULL
那么 standio = 会自动分配一个合适的大小,例如 =BUFSIZ
.
注意,你需要保证在 stream closed
的时候, buf
是存在的。如果我们自行指定分配的 buffer
是一个函数内部的局部变量,我们需要在这个函数返回之前把这个 stream
给 closed
了,另外,因为某些实现使用这个 buffer
的一部分做为记录的空间,所以我们存放在这个 buffer
中的数据应该小于它的实际大小。一般我们应该让系统自己选择分配合适的缓存和大小。
有些C库使用 stat
结构的 st_blksize
作为最优输入输出缓存大小,通过后面的讨论我们能够看出, GNU C
库就是使用这个方法。
关于setbuf和setvbuf函数
+--------------------------------------------------------------------------------------------------------+
| Function | mode | buf | Buffer and length | Type of buffering |
|----------+--------+-----------+--------------------------------------+---------------------------------|
| | | non-null | user buf of length BUFSIZ | fully buffered or line buffered |
|setbuf | |-----------+--------------------------------------+---------------------------------|
| | | NULL | (no buffer) | unbuffered |
|----------+--------+-----------+--------------------------------------+---------------------------------|
| | | non-null | user buf of length size | |
| |_IOLBF |-----------+--------------------------------------|fully buffered |
| | | NULL | system buffer of appropriate length | |
| |--------+-----------+--------------------------------------+---------------------------------|
| setvbuf | | non-null | user buf of length size | |
| |_IOFBF |-----------+--------------------------------------|line buffered |
| | | NULL | system buffer of appropriate length | |
| |--------+-----------+--------------------------------------+---------------------------------|
| | _IONBF | (ignored) | (no buffer) | unbuffered |
+--------------------------------------------------------------------------------------------------------+
我们需要注意,如果我们以一个函数中的自动变量为标准输入输出分配缓存,那么我们需要在返回函数之前关闭流。另外,有些实现使用这个缓存的部分内容作为内部索引之用,所以实际存放于缓存中的数据会比缓存的大小要小。一般来说,我们让系统自己选择缓存大小,以及自动分配缓存。这样,当我们关闭流的时候,标准 I/O
库会自动释放缓存。
我们可以用 fflush
函数将流刷新,其声明如下:
#include <stdio.h>
int fflush(FILE *fp);
返回:如果成功返回0,如果错误返回 EOF
。
fflush
会导致在 stream
中没有被 written
的数据被提交到 kernel
中去。特别地,如果 fp
为 NULL
,那么这个函数会导致所有的输出流被刷新。