APUE学习笔记:第五章 标准I/O库

时间:2022-12-26 10:08:19

5.1 引言

标准I/O库处理很多细节,例如缓冲区分配,以优化长度执行I/O等。这些处理不必担心如何使用正确的块长度。这使得它便于用户使用,但是如果不较深入地了解I/O库函数的操作,也会带来一些问题

 

5.2 流和FILE对象

对于ASCII字符集,一个字符用一个字节表示。对于国际字符集,一个字符可用多个字节表示。标准I/O文件流可用于单字节或多字节字符集。

流的定向决定了所读、写的字符是单字节还是多字节的。当一个流最初被创建时,它并没有定向。如若在未定向的流上使用一个多字节I/O函数,则将该流的定向设置为宽定向的。若在未定向的流上使用一个单字节I/O函数,则将该流的定向设置为字节定向的。

 

只有两个函数可改变流的定向。freopen函数清除一个流的定向;fwide函数设置流的定向

#include<stdo.h>
#include<wchar.h>

int fwide(FILE *fp,int mode);
    
      返回值:若流是宽定向的则返回正值,若流是字节定向的则返回负值,未定向则返回0

根据mode参数的不同值,fwide函数执行不同的工作:

-如若mode参数值为负,fwide将试图使指定的流是字节定向的

-如若mode参数值为正,fwide将试图使指定的流是宽定向的

-如若mode参数值为0,fwide将不试图设置流的定向,但返回标识该流定向的值

(fwide并不改变已定向流的定向,且fwide无出错返回。若想知道是否出错,则可调用fwide前先清楚errno,从fwide返回时检查errno的值)

 

5.3 标准输入,标准输出和标准出错

对一个进程预定义了三个流,并且这三个流可以自动地被进程使用,它们是:标准输入,标准输出,标准错误

这三个标准I/O流通过预定义文件指针stdin、stdout和stderr加以引用。这三个文件指针同样定义在头文件<stdio.h>中

 

5.4 缓冲

标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数。它也对每个I/O流自动地进行缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。

标准I/O提供了三种类型的缓冲:

(1)全缓冲。这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc()获得缓冲区

    术语冲洗(flush)说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动冲洗(例如当填满一个缓冲区时),或者可以调用函数fflush冲洗一个流。

   (在UNIX环境中,flush有两种意思。在标准I/O库方面,flush(冲洗)意味着将缓冲区中的内容写到磁盘上。在终端驱动程序方面,flush(刷清)表示丢弃已存储在缓冲区中    的数据)

(2)行缓冲。在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作

    对于行缓冲有两个限制。第一,当缓冲区满即使没有换行符也进行I/O操作。第二,任何时候只要通过标准I/O库要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它要求从内核得到数据),那么就会造成冲洗所有行缓冲输出流。在(b)中括号中的说明,其理由是,所需的数据可能已在该缓冲区中,它并不要求在数据需要时才从内核读数据。很明显,从不带缓冲的一个流中进行输入(a)要求当时从内核得到数据

(3)不带缓冲。标准I/O库部队字符进行缓冲存储。例如,如果用标准I/O函数fputs写15个字符到不带缓冲的流中,则该函数很可能用write系统调用函数将这些字符立即写至相关  联的打开文件上标准出错流stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符)

 

setbuf和setvbuf可以更改一个流的缓冲类型

#include<stdio.h>

void setbuf(FILE *restrict fp,char *restrict buf);

int setvbuf(FILE *restrict,char *restrict buf,int mode,size_t size);

                        返回值:若成功则返回0,若出错则返回非0值

这些函数一定要在流已被打开后调用(因为每个函数都要求一个有效的文件指针作为它的第一个参数),而且也应该在对流执行任何一个其他操作之前调用

 

可以使用setbuf函数打开或关闭缓冲机制。为了带缓冲进行I/O,参数buf必须指向一个长度为BUFSIZ的缓冲区(定义在<stdio.h>中)。通常在此之后该流就是全缓冲的,但是如果该流与一个终端设备相关,那么某些系统也可将其设置为行缓冲。为了关闭缓冲,将buf设置为NULL

使用setvbuf,我们可以精确地指定所需的缓冲类型。这是用mode参数实现的:

            _IOFBF  全缓冲

            _IOLBF  行缓冲

            _IONBF     不带缓冲

 

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

#include<stdio.h>

int fflush(FILE *fp);

                返回值:若成功则返回0,若出错则返回EOF

此函数使该流所有未写的数据都被传送至内核。作为一个特例,如若fp是NULL,则此函数将导致所有输出流被冲洗

 

5.5 打开流

下列函数打开一个标准I/O流

#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 filedes,const char *type);
    
            三个函数返回值:若成功则返回文件指针,若出错则返回NULL

 

(1)fopen打开一个指定的文件。

(2)freopen在一个指定的流上打开一个指定的文件,若该流已经打开,则先关闭该流。若该流已经定向,则freopen清除该定向。此函数一般用于将一个指定的文件打开为一个预定义的流:标准输入,标准输出或标准出错

(3)fdopen获取一个现有的文件描述符,并使一个标准的I/O流与该描述符相结合。此函数常用于由创建管道和网络通信函数返回的描述符。因为这些特殊类型的文件不能用标准     I/O   fopen函数打开,所以我们必须先调用设备专用函数以获取一个文件描述符,然后fdopen使一个标准I/O流与该描述符相关联

(参数type指定读写方式:r或rb(读,文件必须已存在)、w或wb(删除之前的内容,写)、a或ab(在文件末尾写)、r+或r+b或rb+(读 写,文件必须已存在)、w+或w+b或wb+(删除之前的内容,读写)、a+或a+b或ab+(读写,只能在末尾写))

 

调用fclose关闭一个打开的流

#include<stdio.h>

int fclose(FILE *fp);

                            返回值:若成功则返回0,若出错则返回EOF

在该文件被关闭之前,冲洗缓冲区中的输出数据。丢弃缓冲区中的任何输入数据。如果标准I/O库已经为该流自动分配了一个缓冲区,则释放此缓冲区

 

5.6 读和写流

一旦打开了流,则可在三种不同类型的非格式化I/O中进行选择,对其进行读,写操作:

(1)每次一个字符的I/O。

(2)每次一行的I/O

(3)直接I/O

 

1.输入函数

以下三个函数可用于一次读一个字符

#include<stdio.h>

int getc(FILE *fp);

int fgetc(FILE *fp);

int getchar(void);

        //三个函数的返回值:若成功则返回下一个字符,若已达到文件结尾或出错则返回EOF

函数getchar等价于getc(stdin);

前两个函数的区别是getc可被实现为宏,而fgetc则不能实现为宏

注意,不管是出错还是到达文件尾端,这三个函数都返回同样的值。为了区分这两种不同的情况,必须调用ferror或feof

#include<stdio.h>

int ferror(FILE *fp);

int feof(FILE *fp);

                  //两个函数返回值:若条件为真则返回非0值,否者返回0
void clearerr(FILE *fp);

在大多数实现中,每个流在FILE对象中维持了两个标志:出错标志和文件结束标志(调用clearerr则清楚这两个标志)

 

从流中读取数据后,可以调用ungetc将字符再压回流中

#include<stdio.h>

int ungetc(int c,FILE *fp);

                    //返回值:若成功则返回c,若出错则返回EOF

压送回流中的字符以后又可从流中独处,但读出字符的顺序与压送回的顺序相反。(回送的字符不必一定是上一次读到的字符。不能回送EOF。但是当已经到达文件尾端时,仍可以回送一字符。下次读将返回该字符,再次读则返回EOF。之所以这样所的原因是一次成功的ungetc调用会清除该流文件的结束标志)

 

2.输出函数

对应于上面所述的每个输入函数都有一个输出函数

#include<stdio.h>

int putc(int c ,FILE *fp);

int fputc(int c,FILE *fp);

int putchar(int c);


                三个函数返回值:若成功则返回c,若出错则返回EOF

 

 

5.7 每次一行I/O

下面两个函数提供每次输入一行的功能

#include<stdio.h>

char *fgets(char *restrict buf ,int n,FILE *restrict fp);

char *gets(char *buf);


            //两个函数返回值:若成功则返回buf,若已达到文件结尾或出错则返回NULL

gets是一个不推荐使用的函数。其问题是调用这在使用gets时不能指定缓冲区的长度。这样就可能造成缓冲区溢出,写到缓冲区之后的存储空间中,从而产生不可预料的后果

 

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

#include<stdio.h>

int fputs(const char *restrict str,FILE *restrict fp);

int puts(const char *str);


                  //两个函数返回值:若成功则返回非负值,若出错则返回EOF

函数fputs将一个以null符终止的字符串写到指定的流,尾端的终止符null不写出(这并不一定是每次输出一行)

puts将一个以null符终止的字符串写到标准输出,终止符不写出。但是,puts然后又将一个换行符写到标准输出。

 

5.8 标准I/O的效率

实例:5_1 用getc和putc将标准输入复制到标准输出

 1 #include"apue.h"
 2 int main()
 3 {
 4     int c;
 5     while((c=getc(stdin))!=EOF)
 6         if(putc(c,stdout)==EOF)
 7             err_sys("output error");
 8     if(ferror(stdin))
 9         err_sys("input error");
10     exit(0);
11 }

 

实例:5_2 用fgets和fputs将标准输入复制到标准输出

 1 #include"apue.h"
 2 
 3 int main()
 4 {
 5     char buf[MAXLINE];
 6     while(fgets(buf,MAXLINE,stdin)!=NULL)
 7         if(fputs(buf,stdout)==EOF)
 8         err_sys("output error");
 9     if(ferror(stdin))
10         err_sys("input error");
11     exit(0);
12 }

 

同read、write相比可知,系统调用与普通的函数调用相比通常需要花费更多的时间

 

5.9 二进制I/O

如果输入数据中包含有null字节或换行符,则fgets也不能正确工作。因此,提供了下列两个函数以执行二进制I/O操作

#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);

                //两个函数的返回值:读或写的对象数

这些函数有两种常见用法:

(1)读或写一个二进制数组

float data[10];
if(fwrite(&data[2],sizeof(float),4,fp)!=4)
    err_sys("fwrite error");

(2)读或写一个结构

struct{
        short count;
        long total;
        char name[NAMESIZE];
}item;
if(fwrite(&item,sizeof(item),1,fp)!=1)
        err_sys("fwrite error");


5.10 定位流

有三种方法定位标准I/O流:

(1)ftell和fseek函数(他们都假定文件的位置可以存放在一个长整形中)

(2)ftello和fseeko函数。(可以使文件偏移量不必一定使用长整形。它们使用off_t数据类型代替了长整形)

(3)fgetpos和fsetpos函数。(使用一个抽象数据类型fpos_t记录文件的位置。这种数据类型可以定义为记录一个文件位置所需的长度

 

需要移植到非UNIX系统上运行的应用程序应当使用fgetpos和fsetpos.

#include<stdio.h>

long ftell(FILE *fp);
                        //返回值:若成功则返回当前文件位置指示,若出错则返回-1

in fseek(FILE *fp,long offset,int whence);
                        //返回值:若成功则返回0,若出错则返回非0值

void rewind(FILE *fp);

off_t ftello(FILE *fp);
                            //返回值:若成功则返回当前文件位置指示,若出错则返回-1

int fseeko(FILE *fp,off_t offset,int whence);
                            //返回值:若成功则返回0,若出错则返回非0值

int fgetpos(FILE *restrict fp,fpos_t *restrict pos);

int fsetpos(FILE *fp,const fpos_t *pos);
                            //两个函数返回值:若成功则返回0,若出错则返回非0值

whence的值:SEEK_SET,SEEK_CUR,SEEK_END

fgetpos将文件位置指示器的当前值存入由pos指向的对象中。在以后调用fsetpos时,可以使用此值将流重新定位至该位置。

 

5.11 格式化I/O

1.格式化输出

执行格式化输出处理的是4个printf函数

#include<stdio.h>

int printf(const char *restrict format);

int fprintf(FILE *restrict fp,const char *restrict format,...);
                    //两个函数返回值:若成功则返回输出字符数,若输出出错则返回负值
int sprintf(char *restrict buf,const char *restrict format,...);

int snprintf(char *restrict buf,size_t n,const char *restrict format,...);
                   //两个函数返回值:若成功则返回存入数组的字符数,若编码出错则返回负值

printf将格式化数据写到标准输出,fprintf写至指定的流,sprintf将格式化的字符送入数组buf中。sprintf在该数组的尾端自动加一个null字节,但该字节不包括在返回值中。snprintf比sprintf安全,因为其不会发生溢出(详见http://www.ahathinking.com/archives/202.html)

 

2.格式化输入

执行格式化输入处理的是三个scanf函数

#include<stdio.h>

int scanf(const char *restrict format, ...);

int fcanf(FILE *restrict fp,const char *restrict format, ...);

int sscanf(const char *restrict buf, const char *restrict format, ...);

 //三个函数返回值:指定的输入项数;若输入出错或在任意变换前已到达文件结尾则返回EOF

scanf族用于分析输入字符串,并将字符序列转换成指定类型的变量。

 

5.12  实现细节

每个标准I/O流都有一个与其相关联的文件描述符,可以对一个流调用fileno函数以获得其描述符

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

                //返回值:与该流相关联的文件描述符

如果要调用dup或fcntl等函数,则需要此函数

 

实例:5_3 对各个标准I/O流打印缓冲状态信息

 1 #include"apue.h"
 2 void pr_stdio(const char *,FILE *);
 3 
 4 int main()
 5 {
 6     FILE *fp;
 7     fputs("enter any character\n",stdout);
 8     if(getchar()==EOF)
 9         err_sys("getchar error");
10     fputs("one line to standard error\n",stderr);
11     
12     pr_stdio("stdin",stdin);
13     pr_stdio("stdout",stdout);
14     pr_stdio("stderr",stderr);
15     
16     if((fp=fopen("/etc/host.conf","r"))==NULL)
17         err_sys("fopen error");
18     if(getc(fp)==EOF)
19         err_sys("getc error");
20     pr_stdio("/etc/host.conf",fp);
21     exit(0);
22 }
23 void pr_stdio(const char *name,FILE *fp)
24 {
25     printf("stream = %s, ",name);
26     /*
27      *The following is nonportable
28      */
29     if(fp->_IO_file_flags&_IO_UNBUFFERED)
30         printf("unbuffered");
31     else if(fp->_IO_file_flags&_IO_LINE_BUF)
32         printf("line buffered");
33     else
34         printf("fully buffered");
35     printf(", buffer size = %d\n",fp->_IO_buf_end - fp->_IO_buf_base);
36 }

从运行结果看出,该系统的默认情况是:当标准输入,输出连至终端时,它们是行缓冲的。行缓冲的长度是1024字节。(这并没有将输入,输出的行长限制为1024字节,这只是缓冲区的长度)。当将这两个流重新定向到普通文件时,它们就变成是全缓冲的,起缓冲长度是该文件系统优先选用的I/O长度

 

5.13 临时文件

ISO C标准I/O库提供了两个函数以帮助创建临时文件

#include<stdio.h>

char *tmpnam(char *ptr);
                    //返回值:指向惟一路径名的指针
FILE *tmpfile(void);
                //返回值:若成功则返回文件指针,若出错则返回NULL

tmpnam函数产生一个与现有文件名不同的一个有效路径名字符串。每次调用它时,它都产生一个不同的路径名,最多调用次数是TMP_MAX。TMP_MAX定义在<stdio.h>中

tmpfile创建一个临时二进制文件(类型wb+),在关闭该文件或程序结束时将自动删除这种文件

实例:5_4 tmpnam和tmpfile函数实例

 1 #include"apue.h"
 2 int main()
 3 {
 4     char name[L_tmpnam],line[MAXLINE];
 5     FILE *fp;
 6     printf("%s\n",tmpnam(NULL)); //first temp name
 7     tmpnam(name);        //second temp name
 8     printf("%s\n",name);
 9     if((fp=tmpfile())==NULL)        //creat temp file
10     err_sys("tmpfile error");
11     fputs("one line of output\n",fp);    //write to temp file
12     rewind(fp);            //then read it back
13     if(fgets(line,sizeof(line),fp)==NULL)
14         err_sys("fgets error");
15     fputs(line,stdout);        //print the line we wrote
16     exit(0);
17 } 

tmpfile函数经常使用的UNIX技术是先调用tmpnam产生一个惟一的路径名,然后用该路径名创建一个文件,并立即unlink它。(

 

SUS为处理临时文件定义了另外两个函数,他们是XSI的扩展部分,其中第一个是tempnam函数

#include<stdio.h>

char *tempnam(const char *directory,const char *prefix);

                    //返回值:指向唯一路径名的指针

tempnam是tmpnam的一个遍体,它允许调用这为所产生的路径名指定目录和前缀。

对于目录有4中不同的选着,按下列顺序判断其条件是否为真,并且使用第一个为真的作为目录:

(1)如果定义了环境变量TMPDIR,则用其作为目录

(2)如果参数directory非NULL,则用其作为目录

(3)将<stdio.h>中的字符串P_tmpdir用作目录

(4)将本地目录(通常是/tmp)用作目录

 

实例:5_5 演示tempnam函数

 1 #include"apue.h"
 2 
 3 int main(int argc,char *argv[])
 4 {
 5     if(argc!=3)
 6     err_quit("usage:a.out <directory><prefix>");
 7     
 8     printf("%s\n",tempnam(argv[1][0]!=' ' ? argv[1]:NULL,
 9         argv[2][0]!=' ' ? argv[2]:NULL));
10     exit(0);
11 }

 

XSI定义的第二个函数是mkstemp。它类似于tmpfile,但是该函数返回的不是文件指针,而是临时文件的打开文件描述符

#include<stdlib.h>

int mkstemp(char *template);

            //返回值:若成功则返回文件描述符,若出错则返回-1

它所返回的文件描述符可用于读、写该文件。临时文件名是用template字符串参数选择的。

与tempfile不同的是,mkstemp创建的临时文件不会自动被删除。若想从文件系统名字空间中删除该文件,则我们需要自行unlink它

使用tmpnam和temnam的一个不足之处是:在返回唯一路径名和应用程序用该路径名创建文件之间有一个时间窗口。在该时间窗口期间,另一个进程可能创建一个同名文件。tempfile和mkstemp函数则不会产生这种问题

 

5.14 标准I/O代替软件

标准I/O库并不完善

标准I/O库的一个不足之处是效率不搞,这与它需要复制的数据量有关。当使用每次一行函数fgets和fputs时,通常需要复制两次数据:一次是在内核和标准I/O缓冲之间,第二次是在标准I/O缓冲和用户程序中的行缓冲区之间。快速I/O库避免了这一点,其方法是读一行的函数返回指向该行的指针,而不是将该行复制到另一个缓冲区中。

标准I/O库另一种代替版本:sifo。这一软件包速度上与fio相近,通常快于标准I/O库

许多标准I/O库实现可用于C函数库中,这种C函数库是为内存较小的系统(如嵌入式)设计的。