第五章〓标准I/O库
51〓引言
本章说明标准I/O库。因为不仅在Unix而且在很多操作系统上都实现此库,所以它
由ANSI C
标准说明。标准I/O库处理很多细节,例如缓存分配,以优化长度执行I/O等。这样
使用户就
不必担心如何选择使用正确的块长度(如39节中所述)。标准I/O库是在系统调用
函数基础
上构造的,它便于用户使用,但是如果不较深入了解库的操作,也会带来一些问题
。
标准I/O库是由Dennis Ritchie在1975年左右编写的。它是由Mike Lesk编写的可移
植I/O库
的主要修改版本。令人惊异的是,15年后制订的标准I/O库对它只作极小的修改。
52〓流和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实现作为
典型进行
介绍。
53〓标准输入、标准输出和标准出错
对一个进程予定义了三个流,它们自动地可为进程使用:标准输入、标准输出和标
准出错。
在32节中我们曾用文件描述符STDIN 迹茫模*常病紽ILENO,STDOUT 迹茫模*常?nbsp;
〗FILENO
和STDERR 迹茫模*常病紽ILENO分别表示它们。
这三个标准I/O流通过予定义文件指针stdin,stdout和stderr加以引用。这三个文
件指针同
样定义在头文件<stdioh>中。
54〓缓冲存储
标准I/O提供缓冲存储的目的是尽可能减少使用read和write调用的数量。(回忆图
31,其
中显示了在不同的缓存长度情况下,为执行I/O所需的CPU时间量。)它也对每个I/
O流自动地
进行缓冲存储管理,避免了应用程序需要考虑这一点所带来的麻烦。不幸的是,标
准I/O库
令人最感迷惑的也是它的缓冲存储。
标准I/O提供了三种类型的缓冲存储:
1全缓冲。在这种情况下,当填满标准I/O缓存后才进行实际I/O操作。对于驻在
磁盘上的
文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,通常调
用malloc
(第78节)标准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和43BSD的系统默
认使用下
列类型的缓冲:
·标准出错是不带缓存的。
·如若是涉及终端设备的其它流,则它们是行缓冲的;否则是全缓冲的。
对任何一个给定的流,如果我们并不喜欢这些系统默认,则可调用下列两个函数中
的一个更
改缓冲类型:
#include<stdioh>
void setbuf(FILE *fp,char *buf);
int setvbuf(FILE *fp,char *buf,int mode,size 迹茫模*常病絫
size)
;
返回:若成功为0,出错为非0
这些函数一定要在流已被打开后调用(这是十分明显的,因为每个函数都要求一个
有效的文
件指针作为它们的第一个参数),而且也应在对该流执行任何一个其它操作之前调
用。
使用setbuf函数,我们可以打开或关闭缓冲机制。为了带缓存进行I/O,参数buf必
须指向一
个长度为BUFSIZ的缓存(该常数定义在<stdioh>中)。通常在此之后该流就是全缓
冲的,但
是如果该流与一个终端设备相关,那么某些系统也可将其设置为行缓冲的。为了关
闭缓冲,
将buf设置为NULL。
使用setvbuf,我们可以精确地说明我们所需的缓冲类型。这是依靠mode参数实现
的:
迹茫模*常病絀OFBF〓全缓冲
迹茫模*常病絀OLBF〓行缓冲
迹茫模*常病絀ONBF〓不带缓冲
如果我们指定一个不带缓冲的流,则忽略buf和size参数。如果我们指定全缓冲或
行缓冲,
则buf和size可以选择地指定一个缓存及其长度。如果该流量带缓存的,而buf是N
ULL,则标
准I/O库将自动地为该分配适当长度的缓存。对于适当长度,我们指的是由struct
结构中的
成员st 迹茫模*常病絙lksige所指定的值(见42节)。如果系统不能为该流失定
此值(例如
若此流涉及一个设备或一个管道),则分配长度为BUFSIZE的缓存。
贝克莱系统首先使用st 迹茫模*常病絙lksize表示缓存长度。较早的系统V版本使
用标准I/
O常数BUFSIZE
(其典型值是1024)。即使43+BSD使用st 迹茫模*常病絙lksize决定最佳的I/O缓
存长度,
它仍将BUFSIZE为1024。
图51摘要列出了这两个函数的动作,以及它们的各个选择项。
图51〓setbuf和setvbuf函数摘要
要了解,如果在一个函数中分配一个自动变量类的标准I/O缓存,则从该函数返回
之前,我
们必须关闭该流。(我们将在78节对此作更多讨论。)另外,SVR4将缓的一部分用
于它自己
的管理操作,所以可以存放在缓存中的实际数据字节数少于size。一般而言,应由
系统选择
缓存的长度,并自动分配缓存。在这样处理时,标准I/O在我们关闭此流时将自动
释放此缓
存。
任何时候,我们都可强制刷新一个流。
#include<stdioh>
int fflush(FILE *fp);
返回:若成功为0,出错为EOF
此函数使该流所有末写的数据都被传递至系统核。作为一种特殊情形,如若fp是N
ULL,则此
函数刷新所有输出流。
传送一个空指针以强迫刷新所有输出流,这是新由ANSI C引入的。非ANSI C库(例
如较早的
系统V版本和43BSD)并不支持此种特征。
55〓打开一个流
下列三个函数打开一个标准I/O流。
#include<stdioh>
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指示打开一个指定的文件(其路径名由pathnam
e指示),
如若该流已经打开,则先关闭该流。此函数典型用于将一个指定的文件打开为一个
预定义的
流:标准输入、标准输出、或标准出错。
3 fdopen取一个现存的文件插述符(我们可能从open,dup,dup2,fcntl或pipe函数
得到此文
件插述符),并使一个标准的I/O流与该插述符相结合。此函数常用于由创建管道和
网络通信
通道函数获得的插述符。因为这些特殊类型的文件不能用标准I/O fopen函数打开
,我们必
须先调用设备-专用函数以获得一个文件描述符,然后用fdopen使一个标准I/O流与
该描述
符相结合。
fopen和freopen是ANSI C的所属部分。因为ANSI C并不涉及文件描述符,所以POS
IX1具有
fdopen。
type参数指定对该I/O流的读、写方式,ANSI C规定type参数可以有15种不同的值
,它们示
于图52中。
图52〓打开标准I/O流的type参数
使用字符b作为type的一部分,使得标准I/O系统可以区分文本文件和两进制文件。
因为Unix
系统核并不对这两种文件进行区分,所以在Unix系统环境下指定字符b作为type的
一部分实
际上并无作用。
对于fdopen,type参数的意义则稍有区别。因为该插述符已被打开,为写而打开并
不截短该
文件。例如,若该插述符原来是由open函数打开的,该文件那时已经存在,则其O
迹茫模?nbsp;
2〗TRUNC标
志将决定是否截短该文件。fdopen函数不能截短它为写而打开的任一文件。)另外
,标准I/O
添加方式也不能用于创建该文件(因为如若一个插述符引用一个文件,则该文件一
定已经存
在)。
当用添加类型打开一文件后,则每次写都将数据写到文件的当前尾端处。如若有多
个进程用
标准I/O添加方式打开了同一文件,那么来自每个进程的数据都将正确地写到文件
中。
43+BSD以前的贝克莱版本以及Kenighan和Ritchie〔1988〕177页上所示的简单版
本并不能
正确地处理添加方式。这些版本在打开流时,调用lsee,使文件位移量为文件结尾
处。在涉
及多个进程时,为了正确地支持添加方式,该文件必须用O 迹茫模*常病紸PPEND
标志打开
,我们已在3
3节中对此进行了讨论。在每次写前,做一次lseek操作同样也不能正确工作(如同
在311节
中讨论的一样)。
当以读和写类型打开一文件时(type中+号),具有下列限制:
·如果中间没有fflushfseek、fsetpos或rewind,则在输出的后面不能直接跟随
输入。
·如果中间没有fseek、fsetpos或rewind,或者一个输出操作没有到达文件尾端,
则在输入
操作之后不能直接跟随输出。
按照图52,我们在图53中摘要列出了打开一个流的六种不同的方式。
图53〓打开一个标准I/O流的六种不同的方式
注意,在指定w或a类型创建一个新文件时,我们无法说明该文件的存取数位。(在
第三章中
所述的open函数和creat函数则能做到这一点。)POSIX1要求以这种方式创建的文
件具有下
列存取数:
S 迹茫模*常病絀RUSR|S 迹茫模*常病絀WUSR|S 迹茫模*常病絀RGRP|S〖C
模*常?nbsp;
〗IWGRP|S 迹茫模*常病絀ROTH|S 迹茫模*常病絀WOTH
除非一个流引用一个终端设备,否则按系统默认,它被打开时是全缓冲的。若一个
流引用一
终端设备时,该流是行缓冲的。一旦打开了流,那么在对该流执行任何操作之前,
如果希望
,则可使用前节所述的setbuf和setvbuf改变缓冲的类型。
调用fclose关闭一个打开的流。
#include <stdioh>
int fclose(FILE *fp);
返回:若成功为0,出错为EOF
在该文件被关闭之前,刷新在缓存中的输出数据。在缓存中的输入数据则被丢弃。
如果标准
I/O库已经为该流自动分配了一个缓存,则释放此缓存。
当一个进程正常终止时(直接调用exit,或从main函数返回),则所有带来写的已在
缓存中数
据的标准I/O流都被刷新,所有打开的标准I/O流都被关闭。
56〓读、写一个流
一旦打开了一个流,则可在三种不同类型的非格式化I/O中进行选择,对其进行读
、写操作
。(在51)节中,我们说明了格式化I/O函数,例如printf和scanf。)
1每次一个字符的I/O。一次读或写一个字符,如果流星带缓冲的,则标准I/O函
数处理所
有缓冲。
2每次一行的I/O。使用fgets和fputs一次读或写一行。每行都以一个新行符终止
。当调用
fgets时,我们应说明我们能处理的最大行长。我们将在57节中说明这两个函数
。
3直接I/O。fread和fwrite函数支持这种类型的I/O。每次I/O操作读或写某种数
量的对象
,而每个对象具有指定的长度。这两个函数常用于从二进制文件中读写一个结构。
我们在5
9节中说明这两个函数。
直接I/O这个术语来自ANSI C标准,有时也被称为:二进制I/O、一次一个对象I/O
、面向记
录的I/O、或面向结构的I/O。
输入函数
有三个函数使我们可以一次读一个字符。
#include<stdioh>
int getc(FILE *fp);
int fgetc(FILE *fp);
int getchar(void);
三个函数的返回:若成功为下一个字符已处文件尾或出错为EOF
函数getchar等同于getc(stdin)。前二个函数的区别是getc可被实现为宏,而fge
tc则不能
实现为宏。这意味着:
1 getc的参数不应当是具有副作用的表达式。
2因为fgetc一定是个函数,所以我们可以得到其地址。这就允许我们将fgetc的
地址作为一个参数传送给另一个函数。
3调用fgetc所需时间很可能长于调用getc,因为调用函数通常所需的时间长于调
用宏。确
实,检验一下<stdioh>头文件的大多数实现,从中可见getc是一个宏,其编码具
有较高的
工作效率。
这三个函数以不带符号字符(unsigned char)类型转换为int的方式返回下一个字符
。说明为
不带符号的理由是,如果最高位为1也不会使返回值为负R笳头祷刂档睦碛?nbsp;
是,这样
就可以返回所有可能的字符值再加上一个已发生错误或已到达文件尾端的指示值。
在<stdio
h>中的常数EOF被要求是一个负值,其值经常是-1。这就意味着我们不能将这三
个函数的
返回值存放在一个字符变量中,我们在以后还要将这些函数的返回值与常数EOF相
比较。
注意,不管是出错还是到达文件尾端,这三个函数都返回同样的值。为了区分这两
种不同的
情况,我们必须调用ferror或feof。
#include<stdioh>
int ferror(FILE *fp);
int feof(FILE *fp);
二个函数返回:若条件为真为非0(真),否则为0(假)
void clearerr(FILE *fp);
在大多数实现的FILE对象中,为每个流保持了两个标志:
·出错标志,
·文件结束标志。
调用clearerr则清除这二个标志。
从一个流读之后,可以调用ungetc将字符再送回流中。
#include<stdioh>
int ungetc(int c,FILE *fp);
返回:若成功为C,出错为EOF
送回到流中的字符以后又可从流中读出,但读出字符的顺序与送回的顺序相反。应
当了解,
虽然ANSI C允许支持任何数量的字符回送的实现,但是它要求任何一种实现都要支
持一个字
符的回送功能。
回送的字符,不必一定是上一次读到的字符。不能回送EOF。但是当已经到达文件
尾端时,
我们仍可以回送一字符。下次读将返回该字符,再次读则返回EOF。所以能这样做
的原因是
一次成功的ungetc调用会清除该流的文件结束指示。
当正在读一个输入流,并进行某种形式的分字或分配号操作时,经常使用回送字符
操作。有
时我们需要先看一看下一个字符,以决定如何处理当前字符。然后就需要方便地将
刚查看的
字符送回,以便下一次调用getc时返回该字符。如果标准I/O库不提供回送能力,
就需将该
字符存放到一个我们自己的变量中,并设置一个标志以便差别在下一次需要一个字
符时是调
用getc,还是从我们自己的变量中取用。
输出函数
对应于上面所述的每个输入函数都有一个输出函数。
#include<stdioh>
int putc(int c,FILE *fp);
int fputc(int c,FILE *fp);
int putchar(int c);
三个函数返回:若成功为C,出错为EOF
与输入函数一样,putchar(c)等同于putc(c,stdout),putc可被实现为宏,而fput
c则不能实
现为宏。
57〓每次一行I/O
下面二个函数提供每次输入一行的功能。
#include<stdioh>
char *fgets(char *buf,int n,FILE *fp);
char *gets(char *buf);
二个函数返回:若成功为buf,已处文件尾或出错为NULL
这两个函数都指定了缓存地址,读入的行将送入其中。gets从标准输入读,而fge
ts则从指
定的流读。
对于fgets,我们必须指定缓存的长度,n。此函数一直读到并包括下一个新行符为
止,但是
不超过n-1字符,读入的字符都送入缓存。该缓存以null字符结尾。如若该行,包
括最后一
个新行符的字符数超过n-1,则只返回一个不完整的行,而且缓存总是以null字符结
尾。对fg
ets的下一次调用则会继续读该行。
gets是一个不推荐使用的函数。问题是调用者在使用gets时不能指定缓存的长度。
这样就可
能造成缓存越界(如若该行长于缓存长度),写到缓存之后的存储空间中,从而产生
不可予料
的后果。这种缺陷曾被利用来构成1988的Internet蠕虫。关于这种情况的说明清见
ACM通信J
une 1989(Vol32,No6)。gets与fgets的另一个区别是,gets并不将新行符存入
缓存中。
这两个函数对新行符进行处理方面的差别与Unix的进展有关。早在Version 7的手
册中就说
明:"为了向后兼容,gets删除新行符,而fgets则保持新行符。"
虽然ANSI C要求提供gets,但请不要使用它。
fputs和puts提供每次输出一行的功能。
#include<stdioh>
int fputs(const char *str,FILE *fp);
int puts(const char *str);
二个函数返回:若成功为非负值,出错为EOF
函数fputs将一个以null符终止的字符串写到指定的流,终止符null不写出。注意
,这并不
一定是每次输出一行,因为它并不要求在null符之前一定是新行符。通常,在nul
l符之前是
一个新行符,但并不要求总是如此。
puts将一个以null符终止的字符串写到标准输出,终止符不写出。但是,puts然后
又将一个
新行符写到标准输出。
puts并不象它所对应的gets那样不安全。但是我们还是避免使用它,以免需要记住
它在最后
又加上了一个新行符。如果总是使用fgets和fputs,那么就会熟悉在每行终止处我
们必须自
己加一个新行符。
58〓标准I/O的效率
使用前面部分所述的函数,我们可以对标准I/O系统的效率有所了解。程序51类
似于程序
33,它使用getc和putc将标准输入复制到标准输出。这两个函数可以实现为宏。
程序51〓用getc和putc将标准输入复制到标准输出
我们可以用fgetc和fputc改写该程序,这两个一定是函数,而不是宏。(我们没有
示出对源
代码更改的细节。)
最后,我们还编写了一个读、写行的版本,程序52。
程序52〓用fgets和fputs将标准输入复制到标准输出
注意,在程序51和程序52中,我们没有显式地关闭标准I/O流。我们知道exit
函数将会
刷新任何未写的数据,然后关闭所有打开的流。(我们将在85节讨论这一点。)将
这三个程
序的时间与图31中的时间进行比较是很有趣的。我们在图54中显示了对同一文
件(15M
bytes,30,000行)进行操作所得的数据。
图54〓使用标准I/O例程得到的时间结果
对于这三个标准I/O版本的每一个,其用户CPU时间都是大于图31中的最佳read版
本,因为
每次读一个字符版本中有一个要执行150万次的循环,而在每次读1行的版本中有一
个要执行
30,000次的循环。在read版本中,其循环只需执行180次(对于缓存长度为8192字
节)。因为
系统CPU时间都相同,所以用户CPU时间的差别造成了时钟时间的差别。
系统CPU时间相同的原因是因为所以这些程序对系统核提出的读、写请求数相同。
注意,使
用标准I/O例程的一个优点是我们无需考虑缓冲及最佳I/O长度的选择。我们在使用
fgets时
需要考虑最大行长,但是最佳I/O长度的选择要方便得多。
图54中的最后一列是每个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版本较图31中BUFSIZE=1的版本要
快得多。
两者都使用了约3百次的函数调用,而fgetc版本的速度在用户CPU时间方面,大约
是后者的5
倍,而在时钟时间方面则几乎是100倍。造成这种差别的原因是:使用read的版本
执行了3百
万次函数调用,这也就引起3百万次系统调用。而对于fgetc版本,它也执行3百万
次函数调
用,但是这只引起360次系统调用。系统调用与普通的函数调用相比是很花费时间
的。
需要声明的是这些时间结果只在某些系统只才是有效的。这种时间结果依赖于很多
实现的特
征,而这种特征对于不同的Unix系统却可能是不同的。尽管如此,有这样一组数据
,并对它
们的差别作出解释,这有助于我们更好地了解系统。
在本节及39节中我们学到的基本事实是:标准I/O库与直接调用read和write函数
相比并不
慢很多。我们观察到的大致代价是使用getc和putc复制1Mbyte数据大约需30秒C
PU时间。
对于大多数比较复杂的应用程序,最主要的用户CPU时间是由应用本身的各种处理
花费的,
而不是由标准I/O例程消耗的。
59〓两进制I/O
56节中的函数是以一次一个字符或一次一行的方式进行操作的。如若做两进制I
/O,那
么我们一次愿忌读、写一整个结构。为了使用getc或putc做到这一点,我们必须循
环通过整
个结构,一次读、写一个字节。因为fputs在遇到null字节时就停止,而在结构中
可能含有n
ull字节,所以我们不能使用每次一行函数实现这种要求。相类似,如果输入数据
中包含有n
ull字节或新行符,则fgets也不能正确工作。因此,提供了下列两个函数以执行两
进制I/O
操作。
#include<stdioh>
size 迹茫模*常病絫 fread(void *ptr,size 迹茫模*常病絫 size,s
ize CD
*常病絫 nobj,FILE *fp);
size 迹茫模*常病絫 fwrite(const void *ptr,size 迹茫模*常病絫 si
ze,siz
e 迹茫模*常病絫 nobj,FILE *fp);
二个函数的返回:读或写的对象数
这些函数有两个常见的用法。
1读或写一个两进制数组。例如,将一个浮点数组的第2至第5个元素写至一个文
件上,我
们可以写成:
float data〔10〕;
if(fwrite(& data〔2〕,sizeof(float),4,fp)!=4)
err 迹茫模*常病絪ys(″fwrite error″);
其中,指定size为每个数组元素的长度,nobj为欲写的元素数。
2读或写一个结构,例如,我们可以写成:
struct {
short count;
long total;
char name[NAMESIZE];
}item;
if(fwrite(&item,sizeof(item),1,fp)!=1)
err 迹茫模*常病絪ys(″fwrite error″);
其中,我们指定size为结构的长度,nobj为1(要写的对象数)。将这两个例子结合
起来就可
读或写一个结构数组。为了做到这一点,size应当是该结构的sizeof,nobj应是该
数组中的
元素数。
fiead和fwrite返回读或写的对象数。对于读,如果出错或读到了文件尾端,则此
数字可以
少于nobj。在这种情况,应调用ferror或feof,以判断究竟是那一种情况。对于写
,如果返
回值少于所要求的nobj,则出错。
使用两进制I/O的基本问题是,它只能用于读在同一系统上原先写的数据。在很多
年之前,
这并无问题(那时,所有Unix系统都在ppp-11上运行),而现在,很多异构系统通过
网相互连
接起来,而且,这种情况已经非常普遍。常常有这种情形,在一个系统上写的数据
,在另一
个系统上处理。在这种环境下,这两个函数可能就不能正常工作,其原因是:
1在一个结构中,同一成员的位移量可能随编译程序和系统的不同而异。(由于不
同的对准
要求。)确实,某些编译程序有一选择项,它允许紧密包装结构(节省存储空间,而
运行性能
则可能有所下降)或准确对齐,以便在运行时易于存取结构中各成员。这意味着即
使在单一
系统上,一个结构的两进制存放结构也可能因编译程序的选择项而不同。
2用来存储多字节整数和浮点值的两进制格式在不同的系统结构间也可能是不同
的。
在不同系统之间交换两进制数据的实际解决方法是使用较高层次的协议。关于网络
协议作用
的交换两进制数据的某些技术,请参阅Stevens〔1990〕的182节。
在813节中,我们将再回到fread函数,那时将用它读一个两进制结构-Unix的进
程记账记
录。
510〓定位一个流
有两种方法定位一个标准I/O流。
1 ftell和fseek。这两个函数自Version 7以来就存在了,但是它们都假定文件
的位置可
以存放在一个长整型中。
2 fgetpos和fsetpos。这两个函数是新由ANSI C引入的。它们引进了一个新的抽
象数据类
型,fpos 迹茫模*常病絫,它记录文件的位置。在非Unix系统中,这种数据类型可
以定义为
记录一个文件的位置所需的长度。
需要移植到非Unix系统上运行的应用程序应当使用fgetpos和fsetpos。
#include<stdioh>
long ftell(FILE *fp);
返回:若成功为当前文件位置指示,出错为-1L
int fseek(FILE *fp,long offset,int whence);
返回:若成功为0,出错为非0
void rewind(FILE *fp);
对于一个两进制文件,其位置指示器是从文件起始位置开始度量,并以字节为计量
单位的。
ftell用于两进制文件时,其返回值就是这种字节位置。为了用fseek定位一个两进
制文件,
必须指定一个字节位移量,以及解释这种位移量的方式。whence的值与36节中l
seek函数
的相同:SEEK 迹茫模*常病絊ET表示从文件的起始位置开始计算位移量;SEEK〖
茫模*?nbsp;
2〗CVR表示从当前文件位置;
SEEK 迹茫模*常病紼ND表示从文件的尾端。ANSI C并不要求一个实现对两进制文
件支持SEE
K 迹茫模*常病紼ND规格说明
,其原因是某些系统要求两进制文件的长度是某个幻数的整数倍,不是部分则充填
为0。但
是在Unix中,对于两进制文件SEEK 迹茫模*常病紼ND是得到支持的。
对于文本文件,它们的文件当前位置可能不以简单的字节位移量来度量。再一次,
这主要也
是在非Unix系统中,它们可能以不同的格式存放文本文件。为了定位一个文本文件
,whence
一定要是SEEK 迹茫模*常病絊ET,而且offset只能有两种值:0(表示反绕文件关其
起始位置
),或
是对该文件的ftell所返回的值。使用rewind函数也可将一个流设置到文件的起始
位置。
正如我们已提及的,下列两个函数是C标准新引进的。
#include<stdioh>
int fgetpos(FILE *fp,fpos 迹茫模*常病絫 *pos);
int fsetpos(FILE *fp,const fpos 迹茫模*常病絫 *pos);
二个函数返回:若成功为0,出错为非0
fgetpos将文件位置指示器的当前值存入由pos指向的对象中。在以后调用fsetpos
时,可以
使用此值将流重新定位至该位置。
511〓格式化I/O
格式化输出
执行格式化输出处理的是三个printf函数。
#include<stdioh>
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字节,但该字节不包括在返回值
中。
43BSD定义sprintf返回第一个参数(缓存指针,类型为char*),而不是一个整型
。ANSI C
要求sprintf返回一个整型。
注意,sprintf可能会造成由buf指向的缓存的越界(溢出)。保证该缓存有足够长度
是调用者
的责任。
对这三个函数可能使用的各种格式变换,请参阅Unix手册,或Kesmighan和Ritchi
e〔1988〕
的附录B。
下列三种printf族的变体类似于上面的三种,但是可变参数表()代换成了a
rg。
#include<stdargh>
#include<stdioh>
int vprintf(const char *format,va 迹茫模*常病絣ist arg);
int vfprintf(FILE *fp,const char *format,va 迹茫模*常病絣ist
arg);
两个函数返回:若成功为输出字符数,若输出了错为负值
int vsprintf(char *buf,const char *format,va 迹茫模*常病絣ist
arg);
返回:存入数组的字符数在附录B的出错例程中,我们将使用usprintf函数。
关于ANSI C标准中有关可变长度参数表的详细说明请参阅Kennighan和itchie〔19
88〕的7
3节。应当了解的是,由ANSI C提供的可变长度参数表例程(<stdargh>头文件和
相关的例
程)与由SVR3(以及更早版本)和43BSD提供的<varargsh>例程是不同的。
格式化输入
执行格式化输入处理的是三个scanf函数。
#include<stdioh>
int scanf(const char *format,);
int fscanf(FILE *fp,const char *format,);
int sscanf(const char *buf,const char *format,);
三个函数返回:指定的输入项数,若输入出错,叵在任一变换前已至文件尾则为E
OF
如同printf族一样,关于这三个函数的各个格式选择项的详细情况,请参阅Unix手
册。
512〓实现细节
正如我们已提及的,在Unix中,标准I/O库最终都要调用第三章中说明的I/O例程。
每个I/O
流都有一个与其相关联的文件插述符,可以对一个流调用fileno以获得其插述符。
#include<stdioh>
int fileno(FILE *fp);
返回:与该流相关联的文件描述符
如果要调用dup或fcntl等函数,那么就需要此函数。
为了了解你所使用的系统中标准I/O库的实现,最好从头文件<stdioh>开始。从
中可以看
到
;FILE对象是如何定义的;每个流的标志的定义;定义为宏的各个标准I/O例程(例
如getc)
。Keinighan和Ritche〔1988〕中的85节含有一个简单的实现,从中可以看到很
多Unix实
现的基本样式。Plallger〔1992〕的第十二章提供了标准I/O库一种实现的全部源
代码。4
3+BSD中标准I/O库的实现(由Chris Torek编写)也是公开可以使用的。
实例
程序53为三个标准流以及一个与一个普通文件相关联的流打印有关缓冲状态信息
。注意,
在打印缓冲状态信息之前,先对每个流执行I/O操作,因为第一个I/O操作通常就造
成为该流
分配缓存。结构成员 迹茫模*常病紽lag、 迹茫模*常病絙ufsize以及常数〖C
模*常?nbsp;
〗IONBF和 迹茫模*常病絀OLBF是由作者所使用的系统定义的。
如果我们运行程序53两次,一次使三个标准与终端相连接,另一次使它们重定向
到普通文
件,则所得结果是:
$ aout〓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
$ aout</etc/termcap>stdout 2>stderr〓三个流都重定向,再次运行该程
序
$ cat stderr
one line to standard error
$ cat stdout
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
程序53〓对各个标准I/O流打印缓冲状态信息
从中可见,该系统的默认是:当标准输入、输出连至终端时,它们是行缓冲的。行
缓存的长
度是128bytes。注意,这并没有将输入、输出的行长限制为128bytes,这些是缓存
的长度。
将512bytes的行写到标准输出会四次调用write系统调用。当将这两个流重新定向
到普通文
件时,它们扰变或是全缓冲的,其缓存长度是该文件系统优先选用的I/O长度(从s
tat结构中
得到的st 迹茫模*常病絙lksize)。从中也可看到,标准出错如它所应该的那样是
非缓冲的
,而普通文件按系统默认是全缓冲的。
513〓临时文件
标准I/O库提供了二个函数以帮助创建临时文件。
#include<stdioh>
char *tmpnam(char *ptr);
返回:指向-唯一路径名的指针
FILE *tmpfile(void);
返回:若成功为文件指针,出错为NULL
temnam产生一个与现在文件名不同的一个有效路径名字符串。每次调用它时,它都
产生一个
不同的路径名,最多调用次数是TMP 迹茫模*常病組AX。TMP 迹茫模*常病組AX定
义在<std
ioh>中。
虽然TMP 迹茫模*常病組AX是由ANSI C定义的。但该C标准只要求其值至少应为25
。但是,X
PG都要求其值
至少为10,000。在此最小值允许一个实现使用4位数字作为临时文件名的同时(00
00~99
99),大多数Unix实现用的却是大、小写字符。
若ptr是NULL,则所产生的路径名存放在一个静态区中,指向该静态区的指针作为
函数值返
回。下一次再调用tmpnam时,会重写该静态区。(这忌味着,如果我们调用此函数
多次,而
且想保存路径名,则我们应当保存该路径名的副本,而不是指针的副本。)如若pt
r不是NULL
,则认为它指向长度至少是L 迹茫模*常病絫mpnam个字符的数组。(常数L CD
*常病絫
mpanam
定义在头文件<stdioh>中。)所产生的路径名存放在该数组中,ptr也作为函数值
返回。
tmpfile创建一个临时二进制文件(类型wb+),在关闭该文件时,或程序结束时将自
动删除这
种文件。注意,UNIX对二进制文件都不作特殊区分。
实例
程序54说明了这两个函数的应用。若执行程序54,则得:
$ aout
/usr/tmp/aaaa 00470
/usr/tmp/baaa 00470
one line of output
加到临时文件名中的5位数字后缀是进程ID。这就保证了对各个进程产生的路径名
都会不同
。
tmpfile函数经常使用的标准Unix技术是先调用tempnam产生一个唯一的路径名,然
后立即un
link它。
程序54〓tmpnam和tmpfile函数应用实例
请回忆415节,对一个文件解除连接,并不删除其内容,关闭该文件时才删除其
内容。
tempnam是tmpnam的一个变体,它允许调用者为所产生的路径名指定目录和前缀。
对于目录有四种不同的选择,并且使用第一个为真的作为目录:
1如果定义了环境变量TMPDIR,则用其作为目录。(在79节中将说明环境变量。
)
2如果参数directory非NULL,则用其作为目录。
3在<stdioh>中的字符串P 迹茫模*常病絫mpdir用作为目录。
4本地目录,通常是/tmp,用作为目录。
如果prefix非NULL,则它应该是最多包含5个字符的字符串,用其作为文件名的头
几个字符
。
该函数调用malloc函数分配动态存储区,用其存放所构造的路径名。当我们不再使
用此路径
名时就可释放此存储区。(在78节中说明malloc和fiee函数。)
tempnam不是POSIX1和ANSI C的所属部分,它是XPG3的所属部分。
我们所说明的实现对应于SVR4和43+BSD。除了XPG3版本,不支持环境变量T
MPDIR之外
,其它则与此相同。
实例
程序55显示了tempnam的应用
程序55〓tempnam函数的应用
注意,如果命令行参数(目录或前缀)中的任一个以空白开始,则我们将其作为nul
l指针传送
给该函数。下面显示使用该程序的各种方式。
上面说明过的选择目录名的四个步骤按序执行,该函数也检查相应的目录名是否有
意义。如
果该目录并不存在(例如/no/such/dir),蔌者对该目录并无写存取数(例如/etc/u
ucp),则跳
过这些,试探对目录名的下一次选择。从本例中可以看到在路径名中如何使用进程
$ aout /home/stevens TEMP〓指定目录和前缀
/home/stevens/TEMPAAAa00571
$ aout″″PFX〓使用默认目录:p 迹茫模*常病絫mpdir
/usr/tmp/PFXAAAa00572
$ TMPDIR=/tmp aout/usr/tmp″″〓使用环境变量;无前缀
/tmp/AAAa00573〓环境变量复量目录
$ TMPDIR=/no/such/dir aout/tmp QQQQ
/tmp/QQQQAAAa00574〓忽略无效环境目录
$ TMPDIR=/no/such/file aout /etc/uucp MMMMM
/usr/tmp/MMMMMAAAa00575〓无效环境和无效目录两者皆忽略ID,也可看出在本实
现中,P 迹茫模*常病?nbsp;
mpdir是/usr/tmp。设置环境变量的技术(在程序前的TMPDIR=)适用于Bour
ne shell和
Kornshell。
514〓标准I/O的代替软件
标准I/O库并不是非常完善的。Korn和VO〔1991〕列出它的很多不是之处-某些属于
其基本
设计,但是大多数则与各种不同的实现有关。
在标准I/O库中,一个效率不高的不足处是需要复制的数据量。当使用每次一行函
数fgets和
fputs时,通常需要复制二次数据:一次是在系统核和标准I/O缓存之间(当调用re
ad和write
时),第二次是在标准I/O缓存和用户程序中的行缓存之间。快速I/O库〔AT&T 199
0a中的fio
(3)〕避免了这一点,其方法是使读1行的函数返回指向该行的指针,而不是将该行
复制到另
一个缓存中。H〔1988〕报告了由于作了这种更改,grep(1)公用程序的速度增加了
2倍。
Korn和Vo〔1991〕说明了标准I/O库的另一种代替版:sfio。这一软件包在速度上
与fio相近
,通常快于标准I/O库。sfio也提供了一些新的特征:推广了I/O流,使其不仅可以
代表文件
,也可代表存储区;可以编写处理模块,并以找方式将其压入I/O流,这样就可以
改变一个
流的操作;较好的异常处理等。
Krieger,Stumm和Unrau〔1992〕说明了另一个代换软件包,它使用了映照文件-mm
ap函数,
我们将在129说明此函数。该新软件包称为ASI(Alloc Stream Interface)。此程
序界面类
似于Unix存储分配函数(malloc,realloc和fiee,这些在78节中说明)。与sfio软
件包相同
,ASI使用指针力图减少数据复制量。
515〓摘要
大多数Unix应用程序都使用标准I/O库。我们在本章中说明了该库提供的所有函数
,某些实
现细节和效率方面的考虑。应该看到标准I/O库使用了缓存机制,而这种机制是产
生很多问
题,引起很多混淆的一个领域。
返回:若成功为0,出错为-1