《UNIX环境高级编程》 第5章 标准I/O库

时间:2021-10-14 22:17:10

标准I/O库

5.1 引言

不仅是UNIX,很多其他操作系统都实现了标准IO库,这个库是由ISO C标准说明的。SUS对ISO C标准进行了扩展,定义了另外一些接口。标准IO库处理很多细节,如缓冲区分配、以优化的块长度执行IO等。

5.2 流和FILE对象

在第3章中,所有的IO函数都是围绕文件描述符的。当打开一个文件时,即返回一个文件描述符,然后该文件描述符就用于后续的IO操作。而对于标准IO库,他们的操作是围绕流(stream)进行的。
当用标准IO库打开或创建一个文件时,我们已使一个流与一个文件相关联,fopen返回一个FILE对象指针(称为文件指针)。该对象通常是一个结构,它包含了标准IO库为管理该流所需要的所有信息,包括了用于实际IO的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区中的字符数以及出错标志等。
(注:简而言之,文件指针将流和文件关联起来了,文件指针包含了管理流和文件所需要的信息。)


流的概念有些抽象,比较清楚的解释如下:
http://blog.csdn.net/u011334105/article/details/21293201
总结了一下流的主要功能:
1.为了屏蔽各个存储设备的个异性,使用一种标准的、统一的方式来操作设备(或文件)。使得一切的设备或文件的操作都通过流(stream)来进行,而不需要关心底层的实现。流就是一个中间层。
2.为了更加高效的使用系统,减少read、write的次数,通过流的缓冲功能提高操作效率。


对于ASCII字符集,一个字符用一个字节表示。对于国际字符集,一个字符可以用多个字节表示。
流的定向(stream’s orientation)决定了所读写的字符是单字节还是多字节。标准IO文件流可以用于单字节或多字节(“宽”)字符集。当一个流最初被创建时,它并没有被定向。若在一个未定向的流上使用一个多字节IO函数,则将该流的定向设置为多字节定向的。若在一个未定向的流上使用一个单字节IO函数,则将该流的定向设置为字节定向的。
只有两个函数可以改变流的定向。freopen函数清除一个流的定向;fwide函数可用于设置流的定向。

#include <stdio.h>
#include <wchar.h>
int fwide(FILE *fp,int mode);//改变流的定向

*mode为负,fwide将试图使指定的流是字节定向的。
*mode为正,fwide将试图使指定的流是宽字节定向的。
*mode为0,fwide将不试图使指定流的定向,但返回该流的定向值。
注意:fwide并不改变已定向的流的定向。

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

对一个进程预定义了3个流,分别是标准输入、标准输出和标准错误。这些流引用的文件与在3.2节中提到的文件描述符STDIN_FILENO、STDOUT_FILENO、STDERR_FILENO所引用的文件相同。
这3个标准IO流通过预定义在stdio.h头文件中的文件指针stdin、stdout、stderr加以引用。(通过文件指针引用了流)
C语言中stdin、stdout、stderr所引用的流不需要进行open()操作,流自动打开。

5.4 缓冲

标准IO库提供缓冲的目的是尽可能减少使用read和write调用的次数。它也对每个IO流自动地进行缓冲管理。标准IO提供以下3种类型的缓冲:
1.全缓冲。在这种情况下填满标准IO缓冲区后才进行实际IO操作。对于驻留在磁盘上的文件通常是由标准IO库实施全缓冲的。在一个流上执行第一次IO操作(read、write)时,相关标准IO函数通常调用malloc获得需要的缓冲区。
术语冲洗(flush)表示缓冲区的写操作。缓冲区可以自动冲洗(例如:缓冲区填满时),或者可以调用fflush函数冲洗一个流。
2.行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准IO库执行IO操作。
行缓冲有两个限制:第一,缓冲区满时还未到换行符也进行IO操作。第二,如果通过标准IO库从一个不带缓冲或带行缓冲的流中获取数据,那么就会冲洗所有行缓冲输出流。
3.不带缓冲。标准IO库不对字符进行缓冲存储。应该是直接使用read或write函数直接操作。stderr是不带缓冲的。指向终端设备的流是带行缓冲的,否则是全缓冲的。
对任何一个给定的流,如果我们不喜欢这些系统默认,可以使用以下函数更改缓冲类型。

#include <stdio.h>
void setbuf(FILE *restrict fp,char *restrict buf);//打开或关闭缓冲机制
int setbuf(FILE *restrict fp,char *restrict buf,int mode,size_t size);

setbuf可以打开或关闭缓冲机制。为了带缓冲进行IO,参数buf必须指向一个长度为BUFSIZ的缓冲区(定义在stdio.h中)。这样此流就是全缓冲的,如果该流与一个终端设备相关,那也可能是行缓冲的。将buf设置为NULL则关闭缓冲。
setvbuf可以精确地说明所需要的缓冲类型。使用mode参数指定:

mode 类型
_IOFBF 全缓冲
_IOLBF 行缓冲
_IONBF 不带缓冲

如果指定不带缓冲的流则可忽略buf和size;
如果指定全缓冲或行缓冲则可用buf和size选择缓冲及长度;
如果是带缓冲的,当buf是NULL则标准IO库将自动地分配适当长度的缓冲区,适当长度是由常量BUFSIZ所指定的。

任何时候,我们都可以强制冲洗一个流。

#include <stdio.h>
int fflush(FILE *fp);

5.5 打开流

#include <stdio.h>
FILE *fopen(const char *restrict pathname,const char *restrict type);//打开一个指定的文件。
FILE *freopen(const char *restrict pathname,const char *restrict type,FILE *restrict fp);//在一个指定的流上打开一个指定的文件。
FILE *fdopen(int fd,const char *type);//使一个文件描述符与一个标准IO流相结合。

type参数指定对IO流的读写方式,ISO C规定type参数可以有15种不同的值:

type 说明 open标志
r或rb 为读而打开 O_RDONLY
w或wb 把文件截断至0,或为写而创建 O_WRONLY|O_CREAT|O_TRUNC
a或ab 追加:为在文件尾写而打开,或为写而创建 O_WRONLY|O_CREAT|O_APPEND
r+或r+b或rb+ 为读和写而打开 O_RDWR
w+或w+b或wb+ 把文件截断至0,或为读和写而打开 O_RDWR|O_CREAT|O_TRUNC
a+或a+b或ab+ 为在文件尾读和写而打开或创建 O_RDWR|O_CREAT|O_APPEND

使用字符b作为type的一部分,使得标准IO系统可以区分文本文件和二进制文件。因为UNIX内核不区分这两种文件。
注意:
1.在指定w或a类型创建一个新文件时,我们无法说明该文件的访问权限位。
2.除非流引用终端设备,否则按系统默认,流打开时是全缓冲的。若流引用终端设备,则该流时行缓冲的。一但打开了流,那么在对该流执行任何操作之前,都可以使用setbuf或setvbuf函数来改变缓冲的类型。


使用fclose函数关闭一个打开的流。

#include <stdio.h>
int fclose(FILE *fp);

在该文件被关闭前,冲洗缓冲中的输出数据。

5.6 读和写流

一旦打开了流,则可以在3种不同类型的非格式化IO(即按原数据操作,区别于格式化IO如printf和scanf)中进行选择,对其进行读、写操作。
1.每次一个字符的IO。若流有缓冲,则标准IO函数处理所以缓冲。
2.每次一行的IO。使用gfgets和fputs。每行都以一个换行符终止。
3.直接IO。使用fread和fwrite函数,每次IO操作读或写某个数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中每次读或写一个结构。
1.输入函数
一下3个函数用于一次读一个字符。

#include <stdio.h>
int getc(FILE *fp);//可以实现为宏
int fgetc(FILE *fp);//不能实现为宏
int getchar(void);//getchar()相当于getc(stdin)

(1)getc的参数不应当是具有副作用的表达式,因为它可能会被计数多次。
(表达式的副作用是指:表达式在求值过程中要改变该表达式中作为操作数的某个变量的值。)
(2)因为fgetc一定是一个函数,所以可以得到其地址。这就允许将fgetc的地址作为一个参数传递给另一个函数。
(3)调用fgetc的时间可能比getc长,因为调用函数的时间通常长于宏。
这三函数在返回一个字符时,将unsigned char类型转换为int。是为了可以返回文件尾和出错标志,因此不能将返回值放在char变量中。
注意:不管是出错还是到达文件尾,这3个函数返回同样的值,因此使用ferror和feof这两个函数来区分:

#include <stdio.h>
int ferror(FILE *fp);
int feof(FILE *fp);

void clearerr(FILE *fp);

大多数实现中,为每个流在FILE对象中维护了两个标志:
*出错标志
*文件结束标志
调用clearerr可以清除这两个标志。
从流中读取数据以后,可以调用ungetc将字符再压送回流中。

#include <stdio.h>
int ungetc(int c,FILE *fp);

压送回流的字符是先进后出的,还可以从流中取回。用ungetc压送回字符时,并没有写到底层文件中或设备上,只是将他们写回标准IO库的流缓冲区中。

2.输出函数
于输入函数一样,每个都对应一个输出函数:

#include <stdio.h>
int putc(int c,FILE *fp);//可以实现为宏
int fputc(int c,FILE *fp);//不能实现为宏
int putchar(int c);//putchar(c)相当于putc(c, stdout)

5.7 每次一行IO

#include <stdio.h>
char *fgets(char *restrict buf,int n,FILE *restrict fp);
char *gets(char *buf);//由于不指定缓冲大小,容易出错。基本上已经弃用!!

fgets必须指定缓冲区大小n,此函数一直读到下一个换行符位置,但不超过n-1个字符,读取的字符被送入buf;
若字符超过n-1,则只能得到一个不完整的行,下次将会继续读完该行。


fputs和puts提供每次输出一行的功能。

#include <stdio.h>
int fputs(const char *restrict str,FILE *restrict fp);
int puts(const char *str);
//成功返回非负值,错处返回EOF

函数fputs将一个以null字符结尾的字符串写到指定的流,null终止符不写出。注意这不是每次写一行,需要加换行符才能换行。
puts不像gets那样不安全,但还是尽量避免使用它。

5.8 标准IO的效率

APUE page133

5.9 二进制IO

以上函数以一次一个字符或一次一行的方式操作。如果进行二进制IO操作,那么我们更愿意一次读或写一个完整的结构。

#include <stdio.h>
size_t fread(void *restrict ptr,size_t size,size_t nobj,FILE *restrict fp);
size_t fwrite(const void *restrict ptr,size_t size,size_t nobj,FILE *restrict fp);
//两个函数返回实际读或写的对象数

对于读,如果出错或达到文件尾端,则返回值可以少于nobj,在这种情况下应该调用ferror或feof以判断是哪一种情况。
对于写,如果返回值少于要求的nobj,则出错。

5.10 定位流

有3种方法定位标准IO流
1. ftell和fseek函数。它们都假定文件的位置可以放在长整型中。
2. ftello和fseeko函数。它们使用off_t数据类型代替了长整型。
3. fgetpos和fsetpos函数。它们使用抽象数据类型fpos_t记录文件位置。这种类型可以根据需要定义为一个足够大的数,需要移植到非UNIX平台上可以使用两个函数。


#include <stdio.h>
long ftell(FILE *fp);//返回当前文件位置,出错返回-1L。
int fseek(FILE *fp,long offset,int whence);//定位到相对于whence的offset位置。
void rewind(FILE *fp);//定位到起始位置。

#include <stdio.h>
off_t ftello(FILE *fp);//除了返回值类型为off_t外,其他与ftell相同。
int fseeko(FILE *fp,off_t offset,int whence);//偏移量使用off_t类型表示,与fseek函数功能相同。

#include <stdio.h>
int fgetpos(FILE *fp,fpos_t *restrict pos);//将当前文件位置存储到pos对象中。
int fsetpos(FILE *fp,const fpos_t *pos);//设置当前文件位置为pos对象的值。

5.11 格式化IO

  1. 格式化输出
    格式化输出由5个printf函数来处理
#include <stdio.h>
int printf(const char *restrict format, ...);//将格式化数据写到标准输出。
int fprintf(FILE *restrict fp,const char *restrict format, ...);//将格式化数据写到指定的流。
int dprintf(int fd,const char *restrict format, ...);//将格式化数据写到指定文件描述符。
int sprintf(char *restrict buf,const char *restrict format, ...);//将格式化字符送入数字buf中,并在尾端加一个null字节,但该字节不计入返回值中。
int snprintf(char *restrict buf,size_t n ,const char *restrict format, ...);//sprintf函数可能会使buf溢出,使用snprintf可以指定buf的长度。其他与sprintf功能一样。

  1. 格式化输入
    执行格式化输入处理的是3个scanf函数。
int scanf(const char *restrict format,...);
int fscanf(FILE *restrict fp,const char *restrict format,...);
int sscanf(const char *restrict buf,const char *restrict format,...);

5.12 实现细节

正如前述,在UNIX系统中标准IO库最终都要调用第3章中的IO函数。每个标准IO流都有一个与其相关的文件描述符,可以用fileno函数获得流的文件描述符。

#include <stdio.h>
int fileno(FILE *fp);//返回文件描述符

如果要调用dup或fcntl等函数,则需要此函数。为了了解你所使用的系统中标准IO库的实现,最好从stdio.h开始。
注意:1. 在UNIX系统中,标准IO的实现是因系统实现而异的。具体实现应该查看stdio.h文件。
注意:2. 每个进程,在使用不同的标准输入输出文件时(即重定向标准输入输出),缓冲类型和缓冲大小会自动调整。但是要先进行一次IO操作才会自动分配缓冲区,之后再根据不同的系统实现,查看相应的参数(如缓冲区大小,缓冲类型等)。

5.13 临时文件

#include<stdio.h>
chatr *tmpnam(char *ptr);
FILE *tmpfile(void);

tmpnam函数产生一个与现有文件名不同的有效路径名字符串。每次调用它时,都产生一个不同的路径名,最多调用的次数是TMP_MAX(定义在stdio.h中)。
若ptr是NULL,则产生的路径名存放在一个静态区中,并将指向该静态区的指针返回。后续继续调用该函数时,会重写该静态区。
若prt不是NULL则应该是指向长度为L_tmpnam个字符串的数组,所产生的路径名存放在该数组中,并返回该数组。
tmpfile创建一个临时的二进制文件(wb+类型),在关闭该文件或程序结束时将自动删除这种文件。(tmpfile的实现是先调用tmpnam产生一个唯一的路径名,返回使用该路径名创建一个文件,再unlink它;这样在程序退出或关闭文件时会自动删除文件。)


#include<stdlib.h>
chatr *mkdtemp(char *tmplate);//mkdtemp函数创建了一个目录,该目录有一个唯一的名字。
int mkstemp(char *tmplate);//mkstemp函数创建了一个文件,该文件有个一唯一的名字。

创建的文件夹或文件的名字是通过template字符串进行选择的。这个字符串是后6位设置为XXXXXX的路径名。函数将这些占位符替换成不同的字符来构建成一个唯一的路径名。
与tempfile不同,mkstemp创建的临时文件不会自动删除。如果希望从文件系统命名空间中删除该文件,必须自己对他解除连接。


注意一点,若mkstemp的template参数使用指针分配路径,那么路径字符串是存储在可执行文件的只读段的,因此,mkstemp想修改xxxxxx时,由于路径名时只读的,会发生segmentation fault。正确的做法是使用数组在栈上分配路径名字符串。

5.14 内存流

从上面可以看出,标准IO库把数据缓存在内存中,因此每次一次字符和每次一行的IO更有效。我们也可以调用setbuf或setvbuf函数让IO库使用我们自己的缓冲区。
SUS中支持内存流,虽然使用FILE指针进行访问,当其实并没有底层文件所有的IO都是通过在缓冲区与主存之间来回传送字节来完成的。(内存流并不适合存储二进制数,因为二进制数中有null字符,只适合存储字符)

#include <stdio.h>
FILE *fmemopen(void *restrict buf,size_t size,const char *restrict buf);

buf指向缓冲区的开始位置,若buf为空,fmemopen函数分配size字节的缓冲区,这种情况下,当关闭流时缓冲区会被释放。
size指定了缓冲区的大小;
type控制如何使用流,r、w、a、r+、w+、a+;

用于创建内存流的其他两个函数分别是open_memstream和open_wmemstream。

#include <stdio.h>
FILE *open_memstream(char **bufp,size_t *sizep); //面向字节的
#include <wchar.h>
FILE *open_wmemstream(char **bufp,size_t *sizep); //面向宽字节的

因为避免了缓冲区溢出,内存流非常适用于创建字符串。因为内存流只访问主存,不放我磁盘上的文件,所以把标准IO流作为参数用于临时文件的函数来说,会有很大的性能提升。

5.15 标准IO的替代软件

标准IO库并不完善,某些是由于基本设计的问题,但大多数是与不同的实现有关。
还有一些其他的IO库用于高效快速的IO操作。如uClibc C库和Newlib C库等。

5.16 小结

大多数UNIX应用程序都使用标准IO库。
本章说明了该库提供的很多函数以及某些实现细节和效率问题。
可以看到,标准IO使用了缓冲计数,而缓冲技术正是产生很多问题、引起混淆的地方。