当前位置:Linux教程 - Linux - UNIX环境高级编程(标准I/O库)

UNIX环境高级编程(标准I/O库)



        
    第五章 标准I/O库

    51 引言
    本章说明标准I/O库。因为不仅在Unix而且在很多操作系统上都实现此库,所以它由ANSI C

    标准说明。标准I/O库处理很多细节,例如缓存分配,以优化长度执行I/O等。这样使用户就

    不必担心如何选择使用正确的块长度(如39节中所述)。标准I/O库是在系统调用函数基础

    上构造的,它便于用户使用,但是如果不较深入了解库的操作,也会带来一些问题。
    标准I/O库是由Dennis Ritchie在1975年左右编写的。它是由Mike Lesk编写的可移植I/O库

    的主要修改版本。令人惊异的是,15年后制订的标准I/O库对它只作极小的修改。
    52〓流和FILE对象
    在第三章中,所有I/O函数都是针对文件描述符的。当打开一个文件时,即返回一个文件描

    述符,然后该文件描述符就用于后读的I/O操作。而对于标准I/O库,那么它们的操作则是围

    绕流(streams)进行的。(请勿将标准I/O术语流与系统V的STREAMS I/O系统相混淆。)当用标

    准I/O库打开或创建一个文件时,我们已使一个流与一个文件相结合。
    当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针。该对象通常是一个结构

    ,它包含了I/O库为管理该流所需要的所有信息:用于实际I/O的文件插述符,指向流缓存的

    指针,缓存的长度,当前在缓存中的字符数,出错标志等等。
    应用程序没有必要检验一个FILE对象。为了引用一个流需将FILE指针作为参数传递给每个标

    准I/O函数。在本书中,我们称指向FILE对象的指针类型为FILE*)为文件指针。
    在本章中,我们以Unix系统为例,说明标准I/O库。正如前述,此标准库已移框到除Unix以

    外的很多系统中。但是为了说明该库如何实现的一些细节,我们选择Unix实现作为典型进行

    介绍。
    53〓标准输入、标准输出和标准出错
    对一个进程予定义了三个流,它们自动地可为进程使用:标准输入、标准输出和标准出错。

    在32节中我们曾用文件描述符STDIN〖CD#2〗FILENO,STDOUT〖CD#2〗FILENO

    和STDERR〖CD#2〗FILENO分别表示它们。
    这三个标准I/O流通过予定义文件指针stdin,stdout和stderr加以引用。这三个文件指针同

    样定义在头文件中。
    54〓缓冲存储
    标准I/O提供缓冲存储的目的是尽可能减少使用read和write调用的数量。(回忆图31,其

    中显示了在不同的缓存长度情况下,为执行I/O所需的CPU时间量。)它也对每个I/O流自动地

    进行缓冲存储管理,避免了应用程序需要考虑这一点所带来的麻烦。不幸的是,标准I/O库

    令人最感迷惑的也是它的缓冲存储。
    标准I/O提供了三种类型的缓冲存储:
    1全缓冲。在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。对于驻在磁盘上的

    文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,通常调用malloc

    (第78节)标准I/O函数获得需使用的缓存。
    术语刷新(flush)说明标准I/O缓存的写操作。缓存可由标准I/O例程自动地刷新(例如当填满

    一个缓存时),或者可以调用函数fflush刷新一个流。值得引起注意的是在Unix环境中,刷

    新有两种意思。在标准I/O库方面,刷新意味着将缓存中的内容写到磁盘上(该缓存可以只是

    局部填写的)。在终端驱动程序方面(例如在第十一章中所述的teflush函数),刷新表示丢弃

    已存在缓存中的数据。
    2行缓冲。在这种情况下,当在输入和输出中遇到新行符时,标准I/O库执行I/O操作。这

    允许我们一次输出一个字符(用标准I/O fputc函数),但只有写了一行之后才进行实际I/O操

    作。当流涉及一个终端时(例如标准输入和标准输出),典型地使用行缓冲。
    对于行缓冲,有两个限制。第一个是:因为标准I/O库用来收集每一行的缓存的长度是固定

    的,所以只要填满了缓存,那么即使还没有写一个新行符,也进行I/O操作。第二个是:任

    何时候只要通过标准输入输出与要求从(a)一个不带缓冲的流,或者(b)一个行缓冲的流(它

    予先要求从系统核得到数据)得到输入数据,那么这就会造成刷新所有行缓冲输出流。
    在(b)中带了一个在括号中的说明的理由是,所需的数据可能已在该缓存中,它并不要求系

    统核在需要该数据才进行该操作。很明显,从不带缓冲的一个流中进行输入((a)项)要求当

    时从系统核得到数据。
    3不带缓冲。标准I/O库不对字符进行缓冲。如果用标准I/O函数写若干字符到不带缓冲的

    流中,则相当于用write系统的用函数将这些字符写全相比较的打开文件上。标准出错况std

    err通常是不带缓存后,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个新

    行字符。
    ANSI C要求下列缓冲特征:
    1当且仅当标准输入和标准输出并不涉及交互作用设备时,它们才是全缓冲的。
    2标准出错决不会是全缓冲的。
    但是,这并没有告诉我们如果标准输入和输出涉及交互作用设备时,它们是不带缓冲的还是

    行缓冲的,以及标准输出是不带缓冲的,还是行缓冲的。SVR4和43BSD的系统默认使用下

    列类型的缓冲:
    ·标准出错是不带缓存的。
    ·如若是涉及终端设备的其它流,则它们是行缓冲的;否则是全缓冲的。
    对任何一个给定的流,如果我们并不喜欢这些系统默认,则可调用下列两个函数中的一个更

    改缓冲类型:
    #include
    void setbuf(FILE *fp,char *buf);
    int setvbuf(FILE *fp,char *buf,int mode,size〖CD#2〗t size)

    ;
    返回:若成功为0,出错为非0
    这些函数一定要在流已被打开后调用(这是十分明显的,因为每个函数都要求一个有效的文

    件指针作为它们的第一个参数),而且也应在对该流执行任何一个其它操作之前调用。
    使用setbuf函数,我们可以打开或关闭缓冲机制。为了带缓存进行I/O,参数buf必须指向一

    个长度为BUFSIZ的缓存(该常数定义在中)。通常在此之后该流就是全缓冲的,但

    是如果该流与一个终端设备相关,那么某些系统也可将其设置为行缓冲的。为了关闭缓冲,

    将buf设置为NULL。
    使用setvbuf,我们可以精确地说明我们所需的缓冲类型。这是依靠mode参数实现的:
    〖CD#2〗IOFBF〓全缓冲
    〖CD#2〗IOLBF〓行缓冲
    〖CD#2〗IONBF〓不带缓冲
    如果我们指定一个不带缓冲的流,则忽略buf和size参数。如果我们指定全缓冲或行缓冲,

    则buf和size可以选择地指定一个缓存及其长度。如果该流量带缓存的,而buf是NULL,则标

    准I/O库将自动地为该分配适当长度的缓存。对于适当长度,我们指的是由struct结构中的

    成员st〖CD#2〗blksige所指定的值(见42节)。如果系统不能为该流失定此值(例如

    若此流涉及一个设备或一个管道),则分配长度为BUFSIZE的缓存。
    贝克莱系统首先使用st〖CD#2〗blksize表示缓存长度。较早的系统V版本使用标准I/

    O常数BUFSIZE

    (其典型值是1024)。即使43+BSD使用st〖CD#2〗blksize决定最佳的I/O缓存长度,

    它仍将BUFSIZE为1024。
    图51摘要列出了这两个函数的动作,以及它们的各个选择项。
    图51〓setbuf和setvbuf函数摘要
    要了解,如果在一个函数中分配一个自动变量类的标准I/O缓存,则从该函数返回之前,我

    们必须关闭该流。(我们将在78节对此作更多讨论。)另外,SVR4将缓的一部分用于它自己

    的管理操作,所以可以存放在缓存中的实际数据字节数少于size。一般而言,应由系统选择

    缓存的长度,并自动分配缓存。在这样处理时,标准I/O在我们关闭此流时将自动释放此缓

    存。
    任何时候,我们都可强制刷新一个流。
    #include
    int fflush(FILE *fp);
    返回:若成功为0,出错为EOF
    此函数使该流所有末写的数据都被传递至系统核。作为一种特殊情形,如若fp是NULL,则此

    函数刷新所有输出流。
    传送一个空指针以强迫刷新所有输出流,这是新由ANSI C引入的。非ANSI C库(例如较早的

    系统V版本和43BSD)并不支持此种特征。
    55〓打开一个流
    下列三个函数打开一个标准I/O流。
    #include
    FILE *fopen(const char *pathname,const char *type);
    FILE *freopen(const char *pathname,const char *type,FILE *fp);
    FILE *fdopen(int filedes,const char *type);
    三个函数的返回:若成功为文件指针,出错为NULL
    这三个函数的区别是:
    1 fopen打开路径名由pathname指示的一个文件。
    2 freopen在一个特定的流上由fp指示打开一个指定的文件(其路径名由pathname指示),

    如若该流已经打开,则先关闭该流。此函数典型用于将一个指定的文件打开为一个预定义的

    流:标准输入、标准输出、或标准出错。
    3 fdopen取一个现存的文件插述符(我们可能从open,dup,dup2,fcntl或pipe函数得到此文

    件插述符),并使一个标准的I/O流与该插述符相结合。此函数常用于由创建管道和网络通信

    通道函数获得的插述符。因为这些特殊类型的文件不能用标准I/O fopen函数打开,我们必

    须先调用设备—专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与该描述

    符相结合。
    fopen和freopen是ANSI C的所属部分。因为ANSI C并不涉及文件描述符,所以POSIX1具有

    fdopen。
    type参数指定对该I/O流的读、写方式,ANSI C规定type参数可以有15种不同的值,它们示

    于图52中。
    图52〓打开标准I/O流的type参数
    使用字符b作为type的一部分,使得标准I/O系统可以区分文本文件和两进制文件。因为Unix

    系统核并不对这两种文件进行区分,所以在Unix系统环境下指定字符b作为type的一部分实

    际上并无作用。
    对于fdopen,type参数的意义则稍有区别。因为该插述符已被打开,为写而打开并不截短该

    文件。例如,若该插述符原来是由open函数打开的,该文件那时已经存在,则其O〖CD#

    2〗TRUNC标

    志将决定是否截短该文件。fdopen函数不能截短它为写而打开的任一文件。)另外,标准I/O

    添加方式也不能用于创建该文件(因为如若一个插述符引用一个文件,则该文件一定已经存

    在)。
    当用添加类型打开一文件后,则每次写都将数据写到文件的当前尾端处。如若有多个进程用

    标准I/O添加方式打开了同一文件,那么来自每个进程的数据都将正确地写到文件中。
    43+BSD以前的贝克莱版本以及Kenighan和Ritchie〔1988〕177页上所示的简单版本并不能

    正确地处理添加方式。这些版本在打开流时,调用lsee,使文件位移量为文件结尾处。在涉

    及多个进程时,为了正确地支持添加方式,该文件必须用O〖CD#2〗APPEND标志打开

    ,我们已在3

    3节中对此进行了讨论。在每次写前,做一次lseek操作同样也不能正确工作(如同在311节

    中讨论的一样)。
    当以读和写类型打开一文件时(type中+号),具有下列限制:
    ·如果中间没有fflushfseek、fsetpos或rewind,则在输出的后面不能直接跟随输入。
    ·如果中间没有fseek、fsetpos或rewind,或者一个输出操作没有到达文件尾端,则在输入

    操作之后不能直接跟随输出。
    按照图52,我们在图53中摘要列出了打开一个流的六种不同的方式。
    图53〓打开一个标准I/O流的六种不同的方式
    注意,在指定w或a类型创建一个新文件时,我们无法说明该文件的存取数位。(在第三章中

    所述的open函数和creat函数则能做到这一点。)POSIX1要求以这种方式创建的文件具有下

    列存取数:
    S〖CD#2〗IRUSR|S〖CD#2〗IWUSR|S〖CD#2〗IRGRP|S〖CD#2

    〗IWGRP|S〖CD#2〗IROTH|S〖CD#2〗IWOTH
    除非一个流引用一个终端设备,否则按系统默认,它被打开时是全缓冲的。若一个流引用一

    终端设备时,该流是行缓冲的。一旦打开了流,那么在对该流执行任何操作之前,如果希望

    ,则可使用前节所述的setbuf和setvbuf改变缓冲的类型。
    调用fclose关闭一个打开的流。
    #include
    int fclose(FILE *fp);
    返回:若成功为0,出错为EOF
    在该文件被关闭之前,刷新在缓存中的输出数据。在缓存中的输入数据则被丢弃。如果标准

    I/O库已经为该流自动分配了一个缓存,则释放此缓存。
    当一个进程正常终止时(直接调用exit,或从main函数返回),则所有带来写的已在缓存中数

    据的标准I/O流都被刷新,所有打开的标准I/O流都被关闭。
    56〓读、写一个流
    一旦打开了一个流,则可在三种不同类型的非格式化I/O中进行选择,对其进行读、写操作

    。(在51)节中,我们说明了格式化I/O函数,例如printf和scanf。)
    1每次一个字符的I/O。一次读或写一个字符,如果流星带缓冲的,则标准I/O函数处理所

    有缓冲。
    2每次一行的I/O。使用fgets和fputs一次读或写一行。每行都以一个新行符终止。当调用

    fgets时,我们应说明我们能处理的最大行长。我们将在57节中说明这两个函数。
    3直接I/O。fread和fwrite函数支持这种类型的I/O。每次I/O操作读或写某种数量的对象

    ,而每个对象具有指定的长度。这两个函数常用于从二进制文件中读写一个结构。我们在5

    9节中说明这两个函数。
    直接I/O这个术语来自ANSI C标准,有时也被称为:二进制I/O、一次一个对象I/O、面向记

    录的I/O、或面向结构的I/O。
    输入函数
    有三个函数使我们可以一次读一个字符。
    #include
    int getc(FILE *fp);
    int fgetc(FILE *fp);
    int getchar(void);
    三个函数的返回:若成功为下一个字符已处文件尾或出错为EOF
    函数getchar等同于getc(stdin)。前二个函数的区别是getc可被实现为宏,而fgetc则不能

    实现为宏。这意味着:
    1 getc的参数不应当是具有副作用的表达式。
    2因为fgetc一定是个函数,所以我们可以得到其地址。这就允许我们将fgetc的地址作为

    一个参数传送给另一个函数。
    3调用fgetc所需时间很可能长于调用getc,因为调用函数通常所需的时间长于调用宏。确

    实,检验一下头文件的大多数实现,从中可见getc是一个宏,其编码具有较高的

    工作效率。
    这三个函数以不带符号字符(unsigned char)类型转换为int的方式返回下一个字符。说明为

    不带符号的理由是,如果最高位为1也不会使返回值为负。要求整型返回值的理由是,这样

    就可以返回所有可能的字符值再加上一个已发生错误或已到达文件尾端的指示值。在
    h>中的常数EOF被要求是一个负值,其值经常是-1。这就意味着我们不能将这三个函数的

    返回值存放在一个字符变量中,我们在以后还要将这些函数的返回值与常数EOF相比较。
    注意,不管是出错还是到达文件尾端,这三个函数都返回同样的值。为了区分这两种不同的

    情况,我们必须调用ferror或feof。
    #include
    int ferror(FILE *fp);
    int feof(FILE *fp);
    二个函数返回:若条件为真为非0(真),否则为0(假)
    void clearerr(FILE *fp);
    在大多数实现的FILE对象中,为每个流保持了两个标志:
    ·出错标志,
    ·文件结束标志。
    调用clearerr则清除这二个标志。
    从一个流读之后,可以调用ungetc将字符再送回流中。
    #include
    int ungetc(int c,FILE *fp);
    返回:若成功为C,出错为EOF
    送回到流中的字符以后又可从流中读出,但读出字符的顺序与送回的顺序相反。应当了解,

    虽然ANSI C允许支持任何数量的字符回送的实现,但是它要求任何一种实现都要支持一个字

    符的回送功能。
    回送的字符,不必一定是上一次读到的字符。不能回送EOF。但是当已经到达文件尾端时,

    我们仍可以回送一字符。下次读将返回该字符,再次读则返回EOF。所以能这样做的原因是

    一次成功的ungetc调用会清除该流的文件结束指示。
    当正在读一个输入流,并进行某种形式的分字或分配号操作时,经常使用回送字符操作。有

    时我们需要先看一看下一个字符,以决定如何处理当前字符。然后就需要方便地将刚查看的

    字符送回,以便下一次调用getc时返回该字符。如果标准I/O库不提供回送能力,就需将该

    字符存放到一个我们自己的变量中,并设置一个标志以便差别在下一次需要一个字符时是调

    用getc,还是从我们自己的变量中取用。
    输出函数
    对应于上面所述的每个输入函数都有一个输出函数。
    #include
    int putc(int c,FILE *fp);
    int fputc(int c,FILE *fp);
    int putchar(int c);
    三个函数返回:若成功为C,出错为EOF
    与输入函数一样,putchar(c)等同于putc(c,stdout),putc可被实现为宏,而fputc则不能实

    现为宏。
    57〓每次一行I/O
    下面二个函数提供每次输入一行的功能。
    #include
    char *fgets(char *buf,int n,FILE *fp);
    char *gets(char *buf);
    二个函数返回:若成功为buf,已处文件尾或出错为NULL
    这两个函数都指定了缓存地址,读入的行将送入其中。gets从标准输入读,而fgets则从指

    定的流读。
    对于fgets,我们必须指定缓存的长度,n。此函数一直读到并包括下一个新行符为止,但是

    不超过n-1字符,读入的字符都送入缓存。该缓存以null字符结尾。如若该行,包括最后一

    个新行符的字符数超过n-1,则只返回一个不完整的行,而且缓存总是以null字符结尾。对fg

    ets的下一次调用则会继续读该行。
    gets是一个不推荐使用的函数。问题是调用者在使用gets时不能指定缓存的长度。这样就可

    能造成缓存越界(如若该行长于缓存长度),写到缓存之后的存储空间中,从而产生不可予料

    的后果。这种缺陷曾被利用来构成1988的Internet蠕虫。关于这种情况的说明清见ACM通信J

    une 1989(Vol32,No6)。gets与fgets的另一个区别是,gets并不将新行符存入缓存中。


    这两个函数对新行符进行处理方面的差别与Unix的进展有关。早在Version 7的手册中就说

    明:“为了向后兼容,gets删除新行符,而fgets则保持新行符。”
    虽然ANSI C要求提供gets,但请不要使用它。
    fputs和puts提供每次输出一行的功能。
    #include
    int fputs(const char *str,FILE *fp);
    int puts(const char *str);
    二个函数返回:若成功为非负值,出错为EOF
    函数fputs将一个以null符终止的字符串写到指定的流,终止符null不写出。注意,这并不

    一定是每次输出一行,因为它并不要求在null符之前一定是新行符。通常,在null符之前是

    一个新行符,但并不要求总是如此。
    puts将一个以null符终止的字符串写到标准输出,终止符不写出。但是,puts然后又将一个

    新行符写到标准输出。
    puts并不象它所对应的gets那样不安全。但是我们还是避免使用它,以免需要记住它在最后

    又加上了一个新行符。如果总是使用fgets和fputs,那么就会熟悉在每行终止处我们必须自

    己加一个新行符。
    58〓标准I/O的效率
    使用前面部分所述的函数,我们可以对标准I/O系统的效率有所了解。程序51类似于程序

    33,它使用getc和putc将标准输入复制到标准输出。这两个函数可以实现为宏。
    程序51〓用getc和putc将标准输入复制到标准输出
    我们可以用fgetc和fputc改写该程序,这两个一定是函数,而不是宏。(我们没有示出对源

    代码更改的细节。)
    最后,我们还编写了一个读、写行的版本,程序52。
    程序52〓用fgets和fputs将标准输入复制到标准输出
    注意,在程序51和程序52中,我们没有显式地关闭标准I/O流。我们知道exit函数将会

    刷新任何未写的数据,然后关闭所有打开的流。(我们将在85节讨论这一点。)将这三个程

    序的时间与图31中的时间进行比较是很有趣的。我们在图54中显示了对同一文件(15M

    bytes,30,000行)进行操作所得的数据。
    图54〓使用标准I/O例程得到的时间结果
    对于这三个标准I/O版本的每一个,其用户CPU时间都是大于图31中的最佳read版本,因为

    每次读一个字符版本中有一个要执行150万次的循环,而在每次读1行的版本中有一个要执行

    30,000次的循环。在read版本中,其循环只需执行180次(对于缓存长度为8192字节)。因为

    系统CPU时间都相同,所以用户CPU时间的差别造成了时钟时间的差别。
    系统CPU时间相同的原因是因为所以这些程序对系统核提出的读、写请求数相同。注意,使

    用标准I/O例程的一个优点是我们无需考虑缓冲及最佳I/O长度的选择。我们在使用fgets时

    需要考虑最大行长,但是最佳I/O长度的选择要方便得多。
    图54中的最后一列是每个main函数的文本空间字节数(由C编译产生的机器指令)。从中可

    见,使用getc的版本在文本空间中作了getc和putc的宏代换,所以它所需使用的指令数超过

    了调用fgetc和fputc函数所需指令数。观察getc版本和fgetc版本在用户CPU时间方面的差别

    ,可以看到在程序中作宏代换和调用两个函数在进行本测试的系统上并没有造成多大差别。


    使用每次一行I/O版本其速度大约是每次一个字符版本的两倍(用户CPU时间和时钟时间两者)

    。如果fgets和fputs函数是用getc和putc实现的(例如,见Kesnighan和Ritchie〔1988〕的7

    7节),那么,可以予期fgets版本的时间会与getc版本相接近。实际上,我们可以予料每

    次一行的版本会更慢一些,因为除了现已存在的60,000次函数调用外还需增加了百万次宏

    调用。而在本测试中所用的每次一行函数是用memccopy(3)实现的。通常,为了提高效率,m

    emccpy函数是用汇编语言而非C语言编写的。
    这些时间数字的最后一个有趣三点是:fgetc版本较图31中BUFSIZE=1的版本要快得多。

    两者都使用了约3百次的函数调用,而fgetc版本的速度在用户CPU时间方面,大约是后者的5

    倍,而在时钟时间方面则几乎是100倍。造成这种差别的原因是:使用read的版本执行了3百

    万次函数调用,这也就引起3百万次系统调用。而对于fgetc版本,它也执行3百万次函数调

    用,但是这只引起360次系统调用。系统调用与普通的函数调用相比是很花费时间的。
    需要声明的是这些时间结果只在某些系统只才是有效的。这种时间结果依赖于很多实现的特

    征,而这种特征对于不同的Unix系统却可能是不同的。尽管如此,有这样一组数据,并对它

    们的差别作出解释,这有助于我们更好地了解系统。
    在本节及39节中我们学到的基本事实是:标准I/O库与直接调用read和write函数相比并不

    慢很多。我们观察到的大致代价是使用getc和putc复制1Mbyte数据大约需30秒CPU时间。

    对于大多数比较复杂的应用程序,最主要的用户CPU时间是由应用本身的各种处理花费的,

    而不是由标准I/O例程消耗的。
    59〓两进制I/O
    56节中的函数是以一次一个字符或一次一行的方式进行操作的。如若做两进制I/O,那

    么我们一次愿忌读、写一整个结构。为了使用getc或putc做到这一点,我们必须循环通过整

    个结构,一次读、写一个字节。因为fputs在遇到null字节时就停止,而在结构中可能含有n

    ull字节,所以我们不能使用每次一行函数实现这种要求。相类似,如果输入数据中包含有n

    ull字节或新行符,则fgets也不能正确工作。因此,提供了下列两个函数以执行两进制I/O

    操作。
    #include
    size〖CD#2〗t fread(void *ptr,size〖CD#2〗t size,size〖CD

    #2〗t nobj,FILE *fp);
    size〖CD#2〗t fwrite(const void *ptr,size〖CD#2〗t size,siz

    e〖CD#2〗t nobj,FILE *fp);
    二个函数的返回:读或写的对象数
    这些函数有两个常见的用法。
    1读或写一个两进制数组。例如,将一个浮点数组的第2至第5个元素写至一个文件上,我

    们可以写成:
    float data〔10〕;
    if(fwrite(& data〔2〕,sizeof(float),4,fp)!=4)
    err〖CD#2〗sys(″fwrite error″);
    其中,指定size为每个数组元素的长度,nobj为欲写的元素数。
    2读或写一个结构,例如,我们可以写成:
    struct {
    short count;
    long total;
    char name[NAMESIZE];
    }item;
    if(fwrite(&item,sizeof(item),1,fp)!=1)
    err〖CD#2〗sys(″fwrite error″);
    其中,我们指定size为结构的长度,nobj为1(要写的对象数)。将这两个例子结合起来就可

    读或写一个结构数组。为了做到这一点,size应当是该结构的sizeof,nobj应是该数组中的

    元素数。
    fiead和fwrite返回读或写的对象数。对于读,如果出错或读到了文件尾端,则此数字可以

    少于nobj。在这种情况,应调用ferror或feof,以判断究竟是那一种情况。对于写,如果返

    回值少于所要求的nobj,则出错。
    使用两进制I/O的基本问题是,它只能用于读在同一系统上原先写的数据。在很多年之前,

    这并无问题(那时,所有Unix系统都在ppp-11上运行),而现在,很多异构系统通过网相互连

    接起来,而且,这种情况已经非常普遍。常常有这种情形,在一个系统上写的数据,在另一

    个系统上处理。在这种环境下,这两个函数可能就不能正常工作,其原因是:
    1在一个结构中,同一成员的位移量可能随编译程序和系统的不同而异。(由于不同的对准

    要求。)确实,某些编译程序有一选择项,它允许紧密包装结构(节省存储空间,而运行性能

    则可能有所下降)或准确对齐,以便在运行时易于存取结构中各成员。这意味着即使在单一

    系统上,一个结构的两进制存放结构也可能因编译程序的选择项而不同。
    2用来存储多字节整数和浮点值的两进制格式在不同的系统结构间也可能是不同的。
    在不同系统之间交换两进制数据的实际解决方法是使用较高层次的协议。关于网络协议作用

    的交换两进制数据的某些技术,请参阅Stevens〔1990〕的182节。
    在813节中,我们将再回到fread函数,那时将用它读一个两进制结构—Unix的进程记账记

    录。
    510〓定位一个流
    有两种方法定位一个标准I/O流。
    1 ftell和fseek。这两个函数自Version 7以来就存在了,但是它们都假定文件的位置可

    以存放在一个长整型中。
    2 fgetpos和fsetpos。这两个函数是新由ANSI C引入的。它们引进了一个新的抽象数据类

    型,fpos〖CD#2〗t,它记录文件的位置。在非Unix系统中,这种数据类型可以定义为

    记录一个文件的位置所需的长度。
    需要移植到非Unix系统上运行的应用程序应当使用fgetpos和fsetpos。
    #include
    long ftell(FILE *fp);
    返回:若成功为当前文件位置指示,出错为-1L
    int fseek(FILE *fp,long offset,int whence);
    返回:若成功为0,出错为非0
    void rewind(FILE *fp);
    对于一个两进制文件,其位置指示器是从文件起始位置开始度量,并以字节为计量单位的。

    ftell用于两进制文件时,其返回值就是这种字节位置。为了用fseek定位一个两进制文件,

    必须指定一个字节位移量,以及解释这种位移量的方式。whence的值与36节中lseek函数

    的相同:SEEK〖CD#2〗SET表示从文件的起始位置开始计算位移量;SEEK〖CD#

    2〗CVR表示从当前文件位置;

    SEEK〖CD#2〗END表示从文件的尾端。ANSI C并不要求一个实现对两进制文件支持SEE

    K〖CD#2〗END规格说明

    ,其原因是某些系统要求两进制文件的长度是某个幻数的整数倍,不是部分则充填为0。但

    是在Unix中,对于两进制文件SEEK〖CD#2〗END是得到支持的。
    对于文本文件,它们的文件当前位置可能不以简单的字节位移量来度量。再一次,这主要也

    是在非Unix系统中,它们可能以不同的格式存放文本文件。为了定位一个文本文件,whence

    一定要是SEEK〖CD#2〗SET,而且offset只能有两种值:0(表示反绕文件关其起始位置

    ),或

    是对该文件的ftell所返回的值。使用rewind函数也可将一个流设置到文件的起始位置。
    正如我们已提及的,下列两个函数是C标准新引进的。
    #include
    int fgetpos(FILE *fp,fpos〖CD#2〗t *pos);
    int fsetpos(FILE *fp,const fpos〖CD#2〗t *pos);
    二个函数返回:若成功为0,出错为非0
    fgetpos将文件位置指示器的当前值存入由pos指向的对象中。在以后调用fsetpos时,可以

    使用此值将流重新定位至该位置。
    511〓格式化I/O
    格式化输出
    执行格式化输出处理的是三个printf函数。
    #include
    int printf(const char *format,);
    int fprintf(FILE *fp,const char *format,);
    二个函数返回:若成功为输出字符数,若输出了错为负值
    int sprintf(char *buf,const char *format,);
    返回:存入数组的字符数
    printf将格式化数据写到标准输出,fprintf写至指定的流,sprintf将格式化的字符送入数

    组buf中。sprintf在该数组的尾端自动加一个null字节,但该字节不包括在返回值中。
    43BSD定义sprintf返回第一个参数(缓存指针,类型为char*),而不是一个整型。ANSI C

    要求sprintf返回一个整型。
    注意,sprintf可能会造成由buf指向的缓存的越界(溢出)。保证该缓存有足够长度是调用者

    的责任。
    对这三个函数可能使用的各种格式变换,请参阅Unix手册,或Kesmighan和Ritchie〔1988〕

    的附录B。
    下列三种printf族的变体类似于上面的三种,但是可变参数表()代换成了arg。
    #include
    #include
    int vprintf(const char *format,va〖CD#2〗list arg);
    int vfprintf(FILE *fp,const char *format,va〖CD#2〗list arg);


    两个函数返回:若成功为输出字符数,若输出了错为负值
    int vsprintf(char *buf,const char *format,va〖CD#2〗list arg);


    返回:存入数组的字符数在附录B的出错例程中,我们将使用usprintf函数。
    关于ANSI C标准中有关可变长度参数表的详细说明请参阅Kennighan和itchie〔1988〕的7

    3节。应当了解的是,由ANSI C提供的可变长度参数表例程(头文件和相关的例

    程)与由SVR3(以及更早版本)和43BSD提供的例程是不同的。
    格式化输入
    执行格式化输入处理的是三个scanf函数。
    #include
    int scanf(const char *format,);
    int fscanf(FILE *fp,const char *format,);
    int sscanf(const char *buf,const char *format,);
    三个函数返回:指定的输入项数,若输入出错,叵在任一变换前已至文件尾则为EOF
    如同printf族一样,关于这三个函数的各个格式选择项的详细情况,请参阅Unix手册。
    512〓实现细节
    正如我们已提及的,在Unix中,标准I/O库最终都要调用第三章中说明的I/O例程。每个I/O

    流都有一个与其相关联的文件插述符,可以对一个流调用fileno以获得其插述符。
    #include
    int fileno(FILE *fp);
    返回:与该流相关联的文件描述符
    如果要调用dup或fcntl等函数,那么就需要此函数。
    为了了解你所使用的系统中标准I/O库的实现,最好从头文件开始。从中可以看



    ;FILE对象是如何定义的;每个流的标志的定义;定义为宏的各个标准I/O例程(例如getc)

    。Keinighan和Ritche〔1988〕中的85节含有一个简单的实现,从中可以看到很多Unix实

    现的基本样式。Plallger〔1992〕的第十二章提供了标准I/O库一种实现的全部源代码。4

    3+BSD中标准I/O库的实现(由Chris Torek编写)也是公开可以使用的。
    实例
    程序53为三个标准流以及一个与一个普通文件相关联的流打印有关缓冲状态信息。注意,

    在打印缓冲状态信息之前,先对每个流执行I/O操作,因为第一个I/O操作通常就造成为该流

    分配缓存。结构成员〖CD#2〗Flag、〖CD#2〗bufsize以及常数〖CD#2

    〗IONBF和〖CD#2〗IOLBF是由作者所使用的系统定义的。
    如果我们运行程序53两次,一次使三个标准与终端相连接,另一次使它们重定向到普通文

    件,则所得结果是:
    $ aout〓stdin,stdout和stderr都连至终端
    enter any character〓键入新行符
    one line to standard error
    stream=stdin,line buffered,buffer size=128
    stream=stdout,line buffered,buffer size=128
    stream=stderr,unbuffered,buffer size=8
    stream=/etc/motd,fully buffered,buffer size=8192
    $ aoutstdout 2>stderr〓三个流都重定向,再次运行该程序
    $ cat stderr
    one line to standard error
    $ cat stdout
    enter any character
    stream=stdin,fully buffered,buffer size=8192
    stream=stdout,fully buffered,buffer size=8192
    stream=stderr,unbuffered,buffer size=8
    stream=/etc/motd,fully buffered,buffer size=8192
    程序53〓对各个标准I/O流打印缓冲状态信息
    从中可见,该系统的默认是:当标准输入、输出连至终端时,它们是行缓冲的。行缓存的长

    度是128bytes。注意,这并没有将输入、输出的行长限制为128bytes,这些是缓存的长度。

    将512bytes的行写到标准输出会四次调用write系统调用。当将这两个流重新定向到普通文

    件时,它们扰变或是全缓冲的,其缓存长度是该文件系统优先选用的I/O长度(从stat结构中

    得到的st〖CD#2〗blksize)。从中也可看到,标准出错如它所应该的那样是非缓冲的

    ,而普通文件按系统默认是全缓冲的。
    513〓临时文件
    标准I/O库提供了二个函数以帮助创建临时文件。
    #include
    char *tmpnam(char *ptr);
    返回:指向—唯一路径名的指针
    FILE *tmpfile(void);
    返回:若成功为文件指针,出错为NULL
    temnam产生一个与现在文件名不同的一个有效路径名字符串。每次调用它时,它都产生一个
    不同的路径名,最多调用次数是TMP〖CD#2〗MAX。TMP〖CD#2〗MAX定义在
    ioh>中。
    虽然TMP〖CD#2〗MAX是由ANSI C定义的。但该C标准只要求其值至少应为25。但是,X

    PG都要求其值

    至少为10,000。在此最小值允许一个实现使用4位数字作为临时文件名的同时(0000~99

    99),大多数Unix实现用的却是大、小写字符。
    若ptr是NULL,则所产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返

    回。下一次再调用tmpnam时,会重写该静态区。(这忌味着,如果我们调用此函数多次,而

    且想保存路径名,则我们应当保存该路径名的副本,而不是指针的副本。)如若ptr不是NULL

    ,则认为它指向长度至少是L〖CD#2〗tmpnam个字符的数组。(常数L〖CD#2〗t

    mpanam

    定义在头文件中。)所产生的路径名存放在该数组中,ptr也作为函数值返回。
    tmpfile创建一个临时二进制文件(类型wb+),在关闭该文件时,或程序结束时将自动删除这

    种文件。注意,UNIX对二进制文件都不作特殊区分。
    实例
    程序54说明了这两个函数的应用。若执行程序54,则得:
    $ aout
    /usr/tmp/aaaa 00470
    /usr/tmp/baaa 00470
    one line of output
    加到临时文件名中的5位数字后缀是进程ID。这就保证了对各个进程产生的路径名都会不同

    。
    tmpfile函数经常使用的标准Unix技术是先调用tempnam产生一个唯一的路径名,然后立即un

    link它。
    程序54〓tmpnam和tmpfile函数应用实例
    请回忆415节,对一个文件解除连接,并不删除其内容,关闭该文件时才删除其内容。
    tempnam是tmpnam的一个变体,它允许调用者为所产生的路径名指定目录和前缀。
    对于目录有四种不同的选择,并且使用第一个为真的作为目录:
    1如果定义了环境变量TMPDIR,则用其作为目录。(在79节中将说明环境变量。)
    2如果参数directory非NULL,则用其作为目录。
    3在中的字符串P〖CD#2〗tmpdir用作为目录。
    4本地目录,通常是/tmp,用作为目录。
    如果prefix非NULL,则它应该是最多包含5个字符的字符串,用其作为文件名的头几个字符

    。
    该函数调用malloc函数分配动态存储区,用其存放所构造的路径名。当我们不再使用此路径

    名时就可释放此存储区。(在78节中说明malloc和fiee函数。)
    tempnam不是POSIX1和ANSI C的所属部分,它是XPG3的所属部分。
    我们所说明的实现对应于SVR4和43+BSD。除了XPG3版本,不支持环境变量TMPDIR之外

    ,其它则与此相同。
    实例
    程序55显示了tempnam的应用
    程序55〓tempnam函数的应用
    注意,如果命令行参数(目录或前缀)中的任一个以空白开始,则我们将其作为null指针传送

    给该函数。下面显示使用该程序的各种方式。
    上面说明过的选择目录名的四个步骤按序执行,该�
    发布人:netbull 来自:LinuxAid