标准I/O库(2013.10.17)
参考资料:《Unix环境高级编程》第2版 第5章 标准I/O库 (109页-130页)
一、要点总结
1. 标准I/O库会处理一些细节,如缓冲区分配,方便了用户使用,但是了解地深入一些总是有好处的。
2. 标准I/O库操作围绕流进行,当用标准I/O库打开一个文件时,其实就已经关联一个流了。
3. 流的定向(stream's orientation):决定了流使用的是单字节字符集还是多字节(宽)字符集。初始时,流是没有定向的。你在流上使用什么样的I/O函数,就决定了什么样的定向:单字节I/O函数就对应字节定向,多字节I/O函数就对应宽定向。当然,也可以通过fwide函数设置。
4. 通过fopen函数以流的方式打开一个文件,返回一个指向FILE对象的指针。FILE对象里面包含了管理该流的相关信息:文件描述符,指向缓冲区的指针,缓冲区长度,当前缓冲区的字符数和出错标志等。我们就以指向FILE对象的指针(也称为文件指针)引用一个流。
5. 与三个标准流对应的文件指针分别为:stdin,stdout和stderr。
6. 标准I/O的三种缓冲:
全缓冲:填满缓冲区再做实际操作。在流上执行第一个I/O操作时,通常由malloc函数分配缓冲区。对于标准I/O来说,冲洗(flush)操作将缓冲区中的内容写到磁盘上,可以通过fflush函数实现。
行缓冲:读写时遇到换行符就执行I/O操作。通常用于终端流,如标准输入和标准输出。如果填满了缓冲区,即使没遇到换行符也进行I/O操作。另外,从一个不带缓冲的流或一个行缓冲流中输入时,会造成所有行缓冲输出流的flush动作。
不带缓冲:标准出错流stderr是不带缓冲的,使得出错信息尽快显示出来。
7. setbuf函数可以设置缓冲区,大小为BUFSIZ,该常量定义于stdio.h中。如果缓冲区指针参数为NULL,则关闭缓冲。如果要自定义缓冲区类型和大小,则使用setvbuf函数,可以查阅相关文档。另外,如果指定了缓冲,但缓冲区指针为NULL,则自动分配BUFSIZ大小的缓冲区。自动分配的缓冲区在流关闭时会自动释放。
8. fflush函数将流中所有未写的数据都传到内核。如果传入的FILE指针为空,则导致所有的输出流被冲洗(而不是什么都不做)。
9. 打开流的三种方法:
fopen函数:直接指定文件名和打开类型。
freopen函数:在指定的流上打开文件,多传入一个FILE指针。(流已打开,则先关闭;流已定向,则先清除;)
fdopen函数:使用标准I/O流打开一个文件描述符代表的文件。
打开类型的含义是对该I/O流的读写方式。你可以为读而打开,为读和写而打开,为写而创建,为在文件尾写而打开等等。具体的15种类型见113页表格5-2。注意:对于fdopen函数来说,由于文件已经打开(既然文件描述符已经获得),就不能再让流以截短文件而打开或创建该文件。如果有多个进程用标准I/O添写方式(type=a或a+)打开了同一文件,那么可以保证每个进程的数据都正确地写入文件。这样的结果相当于用O_APPEND标志打开文件的效果。另外,以写的方式打开流时,一定会擦除文件以前的内容。若向只在尾端写,则需以添写方式打开。
一个流使用fclose函数关闭。关闭前,冲洗缓冲区中输出数据(对于输出流),丢弃缓冲区中的输入数据(对于输入流)。
10. 读和写
标准I/O分为格式化和非格式化两种,格式化I/O函数即printf和scanf,非格式化I/O又分为以下三种:
一次读写一个字符。
一次读写一行。使用函数fgets和fputs实现。要将最大行长传入fgets。
直接I/O。使用fread和fwrite函数实现。这两个函数每次读写一个对象或结构,经常用于二进制文件。
对于输入来说,可以使用以下三个函数读取一个字符:
getc,fgetc和getchar
前两者都需要一个流作为参数,最后一个相当于getc(stdin)。前两者的区别在于getc可以用宏来实现,而fgetc不行。这说明getc的调用可能快一些,而fgetc有函数地址,可以作为参数传递。注意,这三个参数的返回值不是char类型,而是int型。因为,函数返回的不仅是所有可能的字符值,还可能是已出错或到达文件尾端的指示值EOF,而EOF通常是一个有符号整数-1。
这里还有一个缺陷,对于读取出错和到达文件末尾的情况,函数的返回值相同,无法区分。可以使用ferror和feof函数解决这个问题,二者分别返回表示是否出错和是否到达文件末尾的整数值(非0或0)。
还有一个重要的函数ungetc。它可以将字符再压回到流中。一次只能压回去一个。回送的字符不用是上一次读到的字符。它会清除文件结束标志。该函数的应用是,当我们需要知道下一个字符是什么以决定如何处理当前字符时,可以先将下一个字符读出用于判断,再压送回流中(实际上是缓冲区中)。
对于输出,有putc,fputc和putchar三个函数对应,其间的区别于输入函数相仿,这里不赘述。
11. 行操作
fgets和gets函数都可以读取一行。区别是:首先,fgets接受特定流的输入,而gets从标准输入读取;其次,fgets需要制定缓冲区的大小,而gets不需要;第三,fgets会将换行符存入缓冲区,而gets不会。
有几点要注意:由于gets没有指定缓冲区大小,所以读取有可能造成缓冲区溢出。fgets指定了缓冲区大小,如果缓冲区小于一行的大小,则读不满一行,下次读取仍读这一行。读取的每一行要有一个终止符null。
对应的写操作是fputs和puts函数。它们将一个字符串写入流中。一个区别是:fputs不要求字符串中有换行符,而puts会将一个换行符写到标准输出。
总的来说,推荐使用fputs和fgets,在操作时记得处理换行符(fgets会读入不能忘记处理,fputs要主动输出)。
12. 二进制I/O
以fread为例,该函数从一个文件fd,读取若干块二进制数据,到一块缓冲区中,参数指明了缓冲区的地址,一块数据的大小,读取元素的个数,以及读取的文件描述符。返回读取到元素的个数。
若读取时到达文件尾端或出错,则返回值可能小于参数中读取元素的个数。具体是那种情况,可通过feof或ferror函数判定。若是写入时返回值不到要求的元素个数,则出错。
fread和fwrite用于异构系统之间读写时可能会有问题:同一个结构中,成员的偏移量可能因编译器和系统而异(对齐);多字节整数和浮点数的二进制格式在不同体系结构机器上也可能是不同的(有效位位置)。
13. 流定位
ftell和fseek分别用于获取流当前的位置和设置新的位置,位置的类型为long。ftello和fseeko与前面二者类似,只是位置类型为off_t。fgetpos和fsetpos函数的特殊之处在于将获取和设置的位置存放在一个fpos_t类型的变量中。
注意几点:
需要移植到非UNIX系统的程序要使用fgetpos和fsetpos,因为它们是ISO C引入的。
二进制文件的位置以字节为计量单位。而对于文本文件,设置偏移只能从文件起始位置开始,且偏移值只能是0或ftell的返回值。因为不同系统中,文本文件的存放格式可能不同。
14. 格式化I/O
格式化I/O函数就是我们熟知的printf和scanf及其相关的函数,具体参考书中介绍,没什么特别提到的。
15. 临时文件
ISO C标准库提供了两个创建临时文件的函数:tmpnam和tmpfile。前者指定一个char*参数,存放生成临时文件的路径名,并返回。若指定的参数为NULL,那么将路径名放入静态区,每次调用都会重写该区,并返回指向静态区的指针。若参数不是NULL,那么路径名最长为L_tmpnam,该值定义在头文件stdio.h中。
后者没有参数,创建一个二进制临时文件,并返回流指针FILE*。这种文件在关闭或程序结束时会自动删除。具体的实现是,先调用tmpnam函数产生一个路径名,然后用该路径名创建一个文件,并unlink之。这样解除链接的文件在关闭时会被删除。临时文件创建在/tmp路径下,文件名通常由随机字母组成。
另一个常用的函数是tempnam,它与tmpnam类似,但可以指定创建的路径和文件名的前缀。关于创建文件所在的路径,按一下顺序进行选择:
a. 环境变量TMPDIR
b. 指定的路径参数
c. stdio.h中定义的P_tmpdir字符串
d. 本地目录,通常是/tmp
环境变量中的路径如果不存在,则跳过。可以通过在程序名前指定“TMPDIR=”设置环境变量。
还有一个函数是mkstemp,指定一个路径名,返回一个文件描述符。但是该函数创建的文件不能自动删除。
另外,tempnam和tmpnam在返回唯一路径名和应用程序用该路径名创建文件的中间有一个间隔,这个间隔中其他进程可能创建同名文件,而tmpfile和mkstemp不会有这个问题。
二、程序代码
1. getc和putc示例
//usage of getc and putc
int c;
while((c = getc(stdin)) != EOF)
if(putc(c, stdout) == EOF)
err_sys("output error");
if(ferror(stdin))
err_sys("input error");
2. fgets和fputs示例
//usage of fgets and fputs
char buf[MAXLINE];
while(fgets(buf, MAXLINE, stdin) != NULL)
if(fputs(buf, stdout) == EOF)
err_sys("output error");
3. 打印流的缓冲标志
FILE* fp;
fputs("enter any character\n", stdout);
if(getchar() == EOF)
err_sys("getchar error");
fputs("one line to standard error\n", stderr);
pr_stdio("stdin", stdin);
pr_stdio("stdout", stdout);
pr_stdio("stderr", stderr);
if((fp = fopen("/etc/motd", "r")) == NULL)
err_sys("fopen error");
if(getc(fp) == EOF)
err_sys("getc error");
pr_stdio("/etc/motd", fp);
exit(0);
void pr_stdio(const char* name, FILE* fp)
{
printf("stream = %s, ", name);
if(fp->_IO_file_flags & _IO_UNBUFFERED)
printf("unbuffered");
else if(fp->_IO_file_flags & _IO_LINE_BUF)
printf("line buffered");
else
printf("fully buffered");
printf(", buffer size = %d\n", fp->_IO_buf_end - fp->_IO_buf_base);
}
4. 创建临时文件的两种方法示例
char name[L_tmpnam], line[MAXLINE];
FILE *fp;
printf("%s\n", tmpnam(NULL));
tmpnam(name);
printf("%s\n", name);
if((fp = tmpfile()) == NULL)
err_sys("tmpfile error");
fputs("one line of output\n", fp);
rewind(fp);
if(fgets(line, sizeof(line), fp) == NULL)
err_sys("fgets error");
fputs(line, stdout);
5. tempnam函数
if(argc != 3)
err_quit("usage: a.out <directory> <prefix>");
printf("%s\n", tempnam(argv[1][0] != ' ' ? argv[1] : NULL, argv[2][0] != ' ' ? argv[2] : NULL));