UNIX环境高级编程第五章 读书笔记
一、流和FILE对象
对于标准I/O库,他们的操作是围绕流(stream)进行的。当用标准I/O库打开或创建一个文件时,我们已使一个流与一个文件相关联。
标准I/O文件流可用于单字节或多字节字符集。流的定向决定了所读、写的字符是单字节还是多字节。当一个流最初被创建时,它并没有定向。如若在未定向的流上使用一个多字节的I/O函数,则将该流的定向设置为宽定向的。若在未定向的流上使用一个单字节I/O函数,则将该流设为字节定向的。
只有两个函数可以更改流的定向。
1.freopen函数可以清除一个流的定向
2.fwide函数可用于设置流的定向#include < stdio.h >
#include < wchar.h >
int fwide(FILE *fp,int mode)
若流是宽定向的,返回正值,若流是单定向的,返回负值,若流失是定位的,返回0根据mode参数的不同值,fwide函数执行不同的工作。
- 如若mode参数值为负,fwide将试图使指定的流是字节定向的。
- 如若mode参数值为正,fwide将试图使指定的流是宽定向的。
- 如若mode参数值为0,fwide将不试图设置流的定向,但返回标识该流定向的值。
注意:fwide并不改变已定向流的定向,而且fwide并没有出错返回。因此检测errno的值就是用来检查fwide是否出错,所以在调用fwide之前要先清除errno的值,然后通过判断fwide返回后,errno的值来判断是否出错。当打开一个流时,标准I/O函数fopen返回一个指向FILE对象的指针。该对象通常是一个结构,它包含了标准I/O库为管理该流需要的所有信息,包括用于实际I/O的文件描述符、指向用于该流缓冲区的指针、缓冲区的长度、当前在缓冲区的字节数以及出错标志等。
应用程序没有必要检验FILE对象。为了引用一个流,需将FILE指针作为参数传递给每个标准I/O函数。
二、标准输入、标准输出和标准错误
对一个进程预定义了3个流,并且这3个流可以自动地被进程使用,它们是:标准输入(预定义文件指针stdin)、标准输出(预定义文件指针stdout)和标准错误(预定义文件指针stderr)。这三个文件指针定义在头文件< stdio.h >中。
三、缓冲
标准I/O库提供缓冲的目的是尽可能减少使用read和write调用的次数。
标准I/O库会使每个I/O流自动进入缓冲管理,从而避免了应用程序需要考虑这一点所带来的麻烦。标准I/O提供了以下3种类型的缓存。
1.全缓冲。在这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲的。在一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓冲区。
2.行缓冲。在这种情况下,当输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符,但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时,通常使用行缓冲。
3.不带缓冲。标准I/O库不对字符进行缓冲存储。对于行缓冲有两个限制。
1.因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使没有写换行符,也进行I/O操作。
2.任何时候只要通过标准I/O库要求从一个不带缓冲的流,或者一个行缓冲的流(它从内核请求需要数据)得到输入数据,那么就会冲洗所有行缓冲输出流。术语冲洗(flush)说明标准I/O缓冲区的写操作。缓冲区可由标准I/O例程自动地冲洗,或者可以调用函数fflush冲洗一个流。
在UNIX环境中,flush有两种意思。
1.在标准I/O库方面,flush意味着将缓冲区中的内容写到磁盘上。
2.在终端驱动程序方面,flush表示丢弃已存储在缓冲区中的数据。标准错误流stderr通常是不带缓冲的,这就使得错误信息可以尽快输出出来。
ISO C要求下列缓冲特征。
- 当且仅当标准输入和标准输出并不指向交互式设备时,它们才是全缓冲的。
- 标准错误决不会是全缓冲的。许多系统默认使用下列类型的缓冲:
- 标准错误是不带缓冲的。
- 若是指向终端设备的流,则是行缓冲的,否则是全缓冲的。可以使用以下两个函数更改缓冲类型。
#include < stdio.h >
void setbuf(FILE *restrict fp,char *restrict buf)
int setvbuf(FILE *restrict fp,char *restrict buf,int mode,size_t size)
若成功,返回0,若出错,返回非0这些函数一定要在流已被打开后调用,而且也应在对该流执行任何一个其他操作之前调用。
可以使用setbuf函数打开或关闭缓冲机制。为了带缓冲进行I/O,参数buf必须指向一个长度为BUFSIZ的缓冲区。通常在此之后该流就是全缓冲的,但是如果该流与一个终端设备相关,那么某些系统也可将其设置为行缓冲区。为了关闭缓冲,将buf设置为NULL。使用setvbuf,我们可以精确地说明所需的缓冲类型。这是用mode参数实现的:
_IOFBF 全缓冲
_IOLBF 行缓冲
_IONBF 不带缓冲如果指定一个不带缓冲的流,则忽略buf和size参数。如果指定全缓冲或行缓冲,则buf和size可选择地指定一个缓冲区及其长度。如果该流是带缓冲的,而buf是NULL,则标准I/O库将自动地为该流分配适当长度的缓冲区,适当长度指的是由常量BUFSIZ所指的值。
setbuf和setvbuf函数
![]()
如果在一个函数内分配一个自动变量类的标准I/O缓冲区,则从该函数返回之前,必须关闭该流。另外,其些实现将缓冲区的一部分用于存放它自己的管理操作信息,所以可以存放在缓冲区中的实际数据字节数少于size。一般而言,应由系统选择缓冲区的长度,并自动分配缓冲区。在这种情况下关闭此流时,标准I/O库将自动释放缓冲区。强制冲洗一个流
#include < stdio.h >
int fflush(FILE *fp)
若成功,返回0,若出错,返回EOF此函数使该流所有未写的数据都被传送至内核。作为一种特殊情形。若fp是NULL,则此函数将导致所有输出流被冲洗。
四、打开流
下列3个函数打开一个标准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 fd,const char *type)
若成功,返回文件指针,若出错,返回NULL这3个函数的区别如下:
(1)fopen函数打开路径名为pathname的一个指定的文件。
(2)freopen函数在一个指定的流上打开一个指定的文件,如若该流已经打开,则先关闭该流。若该流已经定向,则使用freopen清除该定向。此函数一般用于将一个指定的文件打开一个预定义的流:标准输入、标准输出和标准错误。
(3)fdopen函数取一个已有的文件描述符,并使一个标准I/O与该描述符想结合。此函数常用于由创建管道和网络通信函数返回的描述符。需要先调用设备专用函数以获得一个描述符,然后启用fdopen使一个标准I/O流与该描述符相结合。type参数的取值
type 说明 open(2)标志 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的一部分,用于区分文本文件和二进制文件。
对于fdopen,type参数的意义稍有区别,因为该描述符已被打开,所以fdopen为写而打开并不截断该文件。另外,标准I/O追加写方式也不能用于创建该文件。
当用追加写类型打开一个文件后,每次写都将数据写到文件的当前尾端处。如果多个进程用标准I/O追加写同一个文件,那么来自每个进程的数据都将正确的写到文件中。
当以读和写打开一个文件时,具有下列限制。
- 如果中间没有fflush、fseek、fsetpos或rewind,则在输出的后面不能直接跟随输入。
- 如果中间没有fseek、fsetpos或rewind,或者一个输入操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。打开一个标准I/O流的6种不同的方式
![]()
注意:在指定w和a类型创建一个新文件时,我们无法说明该文件的访问权限位。POSIX.1要求使用以下权限位来创建文件。
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH调用fclose关闭一个打开的流
#include < stdio.h >
int fclose(FILE *fp)
若成功,返回0,若失败,返回EOF在该文件被关闭之前,冲洗缓冲中的输出数据。缓冲区中的任何输入数据被丢弃。如果I/O库已经为该流自动分配了一个缓冲区,则释放此缓冲区。
当一个进程正常终止时,则所有带未写缓冲数据的标准I/O流都被冲洗,所有打开的标准I/O流都被关闭。
五、读和写流
一旦打开了流,则可在3种不同类型的非格式I/O中进行选择,对其进行读、写操作。
1).每次打开一个字符的I/O。一次读或写一个字符,如果流是带缓冲的,则标准I/O函数处理所有缓冲。
2).每次一行的I/O。如果想要一次读或写一行,则使用fgets和fputs。每行都以一个换行符终止。当调用fgets时,应说明能处理的最大行长。
3).直接I/O。fread和fwrite函数支持这种类型的I/O。每次I/O操作读或写某种数量的对象,而每个对象具有指定的长度。这两个函数常用于从二进制文件中每次读或写一个结构。1.输入函数
以下3个函数可用于一次读一个字符。#include < stdio.h >
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要长,因为调用函数所需的时间通常长于调用宏。这3个函数在返回下一个字符时,将unsigned char类型转换为int类型。要求整型返回值的理由是,这样就可以返回所有可能的字符值再加上一个已出错或已到达文件尾端的指示值。在< stdio.h >中的常量EOF被要求是一个负值,其值经常是-1。这就意味着不能将这3个函数的返回值存放在一个字符变量中,以后还要将这些函数的返回值与常量EOF比较。
用来判断是出错还是到达文件尾端的函数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压送回到流中的字符以后又可从流中读出,但读出字符的顺序与压送回的顺序相反。ISO C实现提供一次只回送一个字符,所以不能期望一次能回一次能回送多个字符。
回送的字符不一定是上一次读到的字符。不能回送EOF。但是当到达文件尾端,也可以会送一个文件(调用ungetc会清除文件结束标志)。
2.输出函数
#include < stdio.h >
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不能实现为宏。
六、每次一行I/O
下面两个函数提供每次输入一行的功能。
#include < stdio.h >
char *fgets(char *restrict buf,int n,FILE *restrict fp)
char *gets(char *buf)
若成功,返回buf,若已到达文件尾端或出错,返回NULL这两个函数都指定了缓冲区的地址,读入的行将送入其中。gets从标准输入读,而fgets则从指定的流读。
对于fgets,必须指定缓冲的长度n。此函数一直读到下一个换行符为止,但是不超过n-1个字符,读入的字符被送入缓冲区。该缓冲区以null字节结尾。如若该行包括最后一个换行符的字符数超过n-1,则fgets只返回一个不完整的行,但是,缓冲区总是以null字节结尾。对fgets的下一次调用会继续读该行。
gets是一个不推荐使用的函数。由于在使用gets时不能指定缓冲区长度,可能造成缓冲区溢出。
gets与fgets的另一区别是,gets并不将换行符存入缓冲区中。
由于在最新版本的ISO C标准将gets忽略了;所以最好使用fgets而不是gets。
fputs和puts提供每次输出一行的功能
#include < stdio.h >
int fputs(const char *restrict str,FILE *restrict fp)
int puts(const char *str)
若成功,返回非负值,若出错,返回EOF函数fputs将一个以null字节终止的字符串写到指定的流,尾端的终止符null不写出。(null不是换行符)。
puts将一个以null字节终止的字符串写到标准输出,终止符不写出。但是,puts随后又将一个换行符写到标准输出。
注意:最好避免使用puts和gets。
利用getc和puts将标准输入复制到标准输出
#include "apue.h"
int
main(void)
{
int c;
while((c=getc(stdin))!=EOF)
if(putc(c,stdout)==EOF)
err_sys("output error");
if(ferror(stdin))
err_sys("input error");
exit(0);
}
利用fgets和fputs将标准输入复制到标准输出
#include "apue.h"
int
main(void)
{
char buf[MAXLINE];
while(fgets(buf,MAXLINE,stdin)!=NULL)
{
if(fputs(buf,stdout)==EOF)
err_sys("output error");
}
if(ferror(stdin))
err_sys("input error");
exit(0);
}
七、标准I/O的效率
使用标准I/O例程的一个优点是无需考虑缓冲及最佳I/O长度的选择。在使用fgets时需要考虑最大行长。
通常,getc和putc实现为宏。使用每次一行I/O版本的速度大约是每次一个字符版本速度的两倍。
系统调用与普通的函数调用相比需要花费更多时间。标准I/O库与直接调用read和write函数相比并不慢很多。对于大多数比较复杂的应用程序,最主要的用户CPU时间是由应用本身的各种处理消耗的,而不是由标准I/O例程消耗。
八、二进制I/O
下面两个函数可以按结构读取数据(执行二进制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)读或写一个二进制数组。
2)读或写一个结构。注意:size应当是该结构的sizeof,nobj应是该数组中的元素个数。
fread和fwrite返回读或写的对象数。对于读,如果出错或到达文件尾端,则此数字可以少于nobj。在这种情况,应调用ferror或feof以判断是那一种情况。对于写,如果返回值少于所要求的nobj,则出错。
使用二进制I/O的基本问题是,它只能用于读在同一系统上已写的数据。
在一个系统上写数据,在另一个系统读取该数据的情况下,这两个函数可能就不能正常工作,其原因是:
1)在一个结构中,同一成员的偏移量可能随编译程序和系统的不同而不同。
2)用来存储多字节整数和浮点值的二进制格式在不同的系统结构间也可能不同。
九、定位流
有3种方法定位标准I/O流。
1)ftell和fseek函数。
2)ftello和fseeko函数。
3)fgetpos和fsetpos函数。需要移植到非UNIX系统上运行的应用程序应当使用fgetpos和fsetpos。
#include < stdio.h >
long ftell(FILE *fp)
若成功,返回当前文件位置指示,若出错,返回-1L
int fseek(FILE *fp,long offset,int whence)
若成功,返回0,若出错,返回-1
void rewind(FILE *fp)对于一个二进制文件,其文件位置指示器是从文件起始位置开始度量,并以字节为度量的。ftell用于二进制文件,其返回值就是这种字节位置。为了用fseek定位一个二进制文件,必须指定一个字节offset,以及解释这种偏移量的方式。whence的值有:SEEK_SET表示从文件的起始位置开始,SEEK_CUR表示从当前文件位置开始,SEEK_END表示从文件尾端开始。
对于一个文本文件,定位一个文本文件,whence一定要是SEEK_SET,而且offset只能有两种值:0(后退到文件的起始位置),或是对该文件的ftell所返回的值。使用rewind函数也可将一个流设置到文件的起始位置。
除了偏移量的类型是off_t而非long以外,ftello函数与ftell函数相同,fseeko函数和fseek函数相同。
#include < stdio.h >
off_t ftello(FILE *fp)
若成功,返回当前文件位置,若出错,返回(off_t)-1
int fseeko(FILE *fp,off_t offset,int whence)
若成功,返回0,若出错,返回-1#include < stdio.h >
int fgetpos(FILE *restrict fp,fpos_t *restrict pos)
int fsetpos(FILE *restrict fp,const fpos_t *pos)
若成功,返回0,若出错,返回非0fgetpos将文件位置指示器的当前值存入由pos指向的对象中。在以后调用fsetpos时,可以使用此值将流重新定位至该位置。
十、格式化I/O
1.格式化输出
#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, … );
若成功,返回存入数组的字符数,若编码失败,返回负值
int snprintf(char *restrict buf,size_t n,const char *restrict format, … );
若缓冲区足够大,返回将要存入数组的字符数,若编码出错,返回负值printf函数将格式化数据写到标准输出,fprintf写至指定的流,dprintf写至指定的文件描述符,sprintf将格式化的字符送入数组buf中,sprintf在该数组的尾端自动加一个null字节,但该字符不包括在返回值中。
注意:sprintf函数可能会造成由buf指向的缓冲区的溢出。调用者有责任确保该缓冲区足够大。为了解决缓冲区溢出问题,引入了snprintf函数。在该函数中,缓冲区长度是一个显式参数,超过缓冲区尾端写的所有字符都被丢弃。如果缓冲区足够大,snprintf函数就会返回写入缓冲区的字符数。与snprintf相同,该返回值不包括结尾的null字节。若snprintf函数返回小于缓冲区长度n的正值,那么没有截断输出。若发生了一个编码的错误,snprintf返回负值。
使用dprintf不需要调用fdopen将文件描述符转换为文件指针。
格式说明控制其余参数如何编写,以后又以什么格式显示。每个参数按照转换说明编写,转换说明以一个百分号%开始,除转换说明8外,格式字符串中的其他字符将按原样,不经任何修改被复制输出*。
一个转换说明有4个可选择的部分,下面将它们都示于方括号。
%[ flags ][ fldwidth ][ precision ][ lenmodifier ] convtypeflags的符号解释说明
标志 说明 , (撇号)将整数按千位分组字符 - 在字段内左对齐输出 + 总是显示带符号转换的正负号 (空格) 如果第一个字符不是正负号,则在其前面加上一个空格 # 指定另一种转换形式 0 添加前导0进行填充 fldwidth部分说明最小字段宽度。转换后参数字符数若小于宽度,则多余字符位置用空格填充,字段宽度是一个非负十进制数,或是一个星号(*)。
precision说明整形转换后最少输出数字位数、浮点数转换后小数点后的最少位数、字符串转换后最大字节数。精度是一个点(.),其后跟随一个可选的非负十进制数或一个星号(*)。
宽度和精度字段两者皆可为*。此时,一个整形参数指定宽度或精度的值。该整形参数正好位于被转换的参数之前。lenmodifier说明参数长度。
长度修饰符 说明 hh 将相应的参数按signed或unsigned char类型输出 h 将相应的参数按signed或unsigned short类型输出 l 将相应的参数按signed或unsigned long类型输出 ll 将相应的参数按signed或unsigned long long类型输出 j intmax_t或uintmax_t z size_t t ptrdiff_t L long double convtype不是可选。他控制如何解释参数。
转换类型 说明 d、i 有符号十进制 o 无符号八进制 u 无符号十进制 x、X 无符号十六进制 f、F 双精度浮点数 e、E 指数格式双精度浮点数 g、G 根据转换后的值解释为f、F、e、E a、A 十六进制指数格式双精度浮点数 c 字符(若带长度修饰符l,为宽字符) s 字符串(若带长度修饰符l,为宽字符) p 指向void的指针 n 到目前位置,此printf调用输出字符的数目将被写入到指针所指向的带符号整数中 % 一个%字符 C 宽字符(等效于lc) S 宽字符串(等效于ls) 转换是按照他们在format参数之后的顺序应用于参数的。
可以显示地调用%n$序列来表示第n个参数的形式来命名参数。如果参数既没有两种提供字段宽度和也没有提供精度,通配符星号的语法就更改为*m$,m指明提供值的参数的位置。
注意:上面两种转换方式并不能混用。
下列printf函数的变体类类似上面的5种,但是可变参数表(…)替换成arg。
#include < stdarg.h >
#include < stdio.h >
int vprintf(const char *restrict format, va_list arg);
int vfprintf(FILE *restrict fp,const char *restrict format, va_list arg );
int vdprintf(int fd,const char *restrict format, va_list arg );
若成功,返回输出字符数,若输出出错,返回负值
int vsprintf(char *restrict buf,const char *restrict format, va_list arg );
若成功,返回存入数组的字符数,若编码失败,返回负值
int vsnprintf(char *restrict buf,size_t n,const char *restrict format, va_list arg );
若缓冲区足够大,返回将要存入数组的字符数,若编码出错,返回负值2.格式化输入
执行格式化输入处理的是3个scanf函数。#include < stdio.h >
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, … )
赋值的输入项数,若输入出错或在任一转换前已到达文件尾端,返回EOFscanf族用于分析输入字符串,并将字符序列转换成指定类型的变量。在格式之后的各参数包含了变量的地址,用转换结果对这些变量赋值。
格式说明控制有3个可选择的部分,下面将它们都示于方括号中:
%[ * ][ fldwidth ][ m ][ lenmodifier ] convtype可选择的星号(*)用于抑制转换。按照转换说明的其余部分的其余部分对输入进行转换,但转换结果并不存放在参数中。
fldwidth说明最大宽度。lenmodifier说明要用转换结果赋值的参数大小。由printf函数族支持的长度修饰符同样得到scanf族的支持。
convtype字段类似于printf族的类型转换类型字段,但两者还是有些区别:输入中带符号的类型可赋予无符号类型。
在字段宽度和长度修饰符之间的可选项m是赋值分配符。它可以用于%c、%s以及%[ 转换符,迫使内存缓冲区分配空间以接纳转换字符串。这种情况,相关的参数必须是指针地址,分配的缓冲区地址必须复制给该指针。如果调用成功,该缓冲区不再使用时,由调用者负责通过调用free函数来释放该缓冲区。scanf函数族同样支持另外一种转换说明,允许显式地命名参数:序列%n$代表了第n个参数。与printf函数族相同,统一编号在格式串中可引用多次。
convtype字段的转换类型
转换类型 说明 d 有符号十进制,基数为10 i 有符号十进制,基数由输入格式决定 O 无符号八进制(输入可选地有符号) u 无符号十进制,基数为10(输入可选地有符号) x,X 无符号十六进制(输入可选地有符号) a、A、e、E、f、F、g、G 浮点数 c 字符(若带长度修饰符l,为宽字符) s 字符串(若带长度修饰符l,为宽字符) [ 匹配列出的字符序列,以]终止 [^ 匹配除列出字符以外的所有字符,以]终止 p 指向void的指针 n 将到目前为止该函数调用读取的字符数写入到指针所指向的无符号整数中 % 一个%符号 C 宽字符(等效lc) S 宽字符(等效ls) scanf族的可变长度参数表如下:
#include < stdio.h >
#include < stdarg.h >
int vscanf(const char *restrict format, va_list arg)
int vfscanf(FILE *restrict fp,const char *restrict format, va_list arg )
int vsscanf(const char *restrict buf,const char *restrict format, va_list arg )
返回指定的输入项目数,若输入出错或在任一转换前文件结束,返回EOF
十一、实现细节
每个标准I/O流都有一个与其相关联的文件描述符,可以对一个流调用dileno函数获得其描述符。
注意:fileno不是ISO C标准部分,而是POSIX.1支持的扩展。#include < stdio.h >
int fileno(FILE *fp)
返回与该流相关联的文件描述符如果要调用dup或fcntl等函数,则需要此函数。
为3个标准流以及一个与普通文件相关联的流打印有关缓冲的状态信息
#include "apue.h"
void pr_stdio(const char *, FILE *);
int is_unbuffered(FILE *);
int is_linebuffered(FILE *);
int buffer_size(FILE *);
int
main(void)
{
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/passwd", "r")) == NULL)
err_sys("fopen error");
if (getc(fp) == EOF)
err_sys("getc error");
pr_stdio("/etc/passwd", fp);
exit(0);
}
void
pr_stdio(const char *name, FILE *fp)
{
printf("stream = %s, ", name);
if (is_unbuffered(fp))
printf("unbuffered");
else if (is_linebuffered(fp))
printf("line buffered");
else /* if neither of above */
printf("fully buffered");
printf(", buffer size = %d\n", buffer_size(fp));
}
/*
* The following is nonportable.
*/
#if defined(_IO_UNBUFFERED)
int
is_unbuffered(FILE *fp)
{
return(fp->_flags & _IO_UNBUFFERED);
}
int
is_linebuffered(FILE *fp)
{
return(fp->_flags & _IO_LINE_BUF);
}
int
buffer_size(FILE *fp)
{
return(fp->_IO_buf_end - fp->_IO_buf_base);
}
#elif defined(__SNBF)
int
is_unbuffered(FILE *fp)
{
return(fp->_flags & __SNBF);
}
int
is_linebuffered(FILE *fp)
{
return(fp->_flags & __SLBF);
}
int
buffer_size(FILE *fp)
{
return(fp->_bf._size);
}
#elif defined(_IONBF)
#ifdef _LP64
#define _flag __pad[4]
#define _ptr __pad[1]
#define _base __pad[2]
#endif
int
is_unbuffered(FILE *fp)
{
return(fp->_flag & _IONBF);
}
int
is_linebuffered(FILE *fp)
{
return(fp->_flag & _IOLBF);
}
int
buffer_size(FILE *fp)
{
#ifdef _LP64
return(fp->_base - fp->_ptr);
#else
return(BUFSIZ); /* just a guess */
#endif
}
#else
#error unknown stdio implementation!
#endif
部分解析:
1. <> &1代表把错误输出导入到标准输出流中。
2. 2>错误信息输出路径。
3. 1>标准信息输出路径(默认)
十二、临时文件
ISO C标准I/O库提供了两个函数以帮助创建临时文件。
#include < stdio.h >
char *tmpnam(char *ptr)
指向唯一路径名的指针
FILE *tmpfile(void)
若成功,返回文件指针,若出错,返回NULLtmpnam函数产生一个与现有文件名不同的一个有效路径名的字符串,每次调用它时,都产生一个不同的路径名,最多调用次数是TMP_MAX。TMP_MAX定义在< stdio.h >
若ptr是NULL,则产生的路径名存放在一个静态区中,指向该静态区的指针作为函数值返回。后续调用tmpnam时,会重写该静态区(如果需要保存路径名,则需要保存路径名的字符串的副本,而不是保存指针的副本)。若ptr不是NULL,则认为它应该是指向长度至少是L_tmpnam(定义在< stdio.h >)个字符的数组,所产生的路径名存放在该数组中,ptr也作为函数值返回。
tmpfile创建了一个临时二进制文件(类型wb+),在关闭该文件或程序结束时将自动删除这文件。注意:UNIX对二进制文件不进行区分。
tmpnam和tmpfile的使用
#include "apue.h"
int
main(void)
{
char name[L_tmpnam],line[MAXLINE];
FILE *fp;
printf("%s\n",tmpnam(NULL)); /*first temp name *//*输出静态区的中的临时文件路径*/
tmpnam(name); /*second temp name*//*输出name数组中的临时文件路径*/
printf("%s\n",name);
if((fp = tmpfile())==NULL) /*create temp file*/
err_sys("tmpfile error");
fputs("one line of output\n",fp);/*write to temp file*/
rewind(fp); /*then read it back*/
if(fgets(line,sizeof(line),fp)==NULL)
err_sys("fgets error");
fputs(line,stdout); /*print the line we wrote*/
exit(0);
}
mkdtemp和mkstemp函数也可用于处理临时文件。
#include < stdlib.h >
char *mkdtemp(char *template)
若成功,返回指向目录名的指针,若出错,返回NULL
int mkstemp(char *template)
若成功,返回文件描述符,若出错,返回-1mkdtemp函数创建了一个目录,该目录有一个唯一的名字,mkstemp函数创建了一个文件,该文件有一个唯一的名字。名字是通过template字符串进行选择的,这个字符串是后6位设置为XXXXXX的路径名。函数将这些占位符替换成不同的字符来构建一个唯一的路径名,如果成功的话,这两个函数将修改template字符串反映临时文件的名字。
用mkdtemp函数创建的目录使用下列访问权限:S_ISUSR|S_IWUSR|S_IXUSR。注意:利用屏蔽字可以进一步限制这些权限。如果目录创建成功,返回新目录的名字。
mkstemp函数以唯一的名字创建一个普通文件并且打开该文件,返回的文件描述符使用读写方式打开。mkstemp创建的文件使用访问权限位S_ISUSR|S_IWUSR。
注意:mkstemp所创建的临时文件并不会自动删除,必须调用unlink进行删除。
注意:tmpnam和tempnam在返回一个路径名和用该路径名创建文件这两个操作之间存在时间间隔,所以使得不同的进程在此期间可以使用同一个路径名创建一个文件。mkstemp和tmpfile并不存这种问题。
mkstemp函数的使用实例
#include "apue.h"
#include <errno.h>
#include <assert.h>
void make_temp(char *template);
int
main()
{
char good_template[] = "dirXXXXXX";/*right way*/
char *bad_template = "dirXXXXXX";/*wrong way*/
printf("trying to create first temp file...\n");
make_temp(good_template);
printf("trying to create second temp file...\n");
make_temp(bad_template);
exit(0);
}
void make_temp(char *template)
{
int fd;
struct stat sbuf;
assert(template!=NULL);
printf("%s",template);
fflush(stdout);
if((fd=mkstemp(template))<0)
err_sys("can't create temp file");
printf("temp name = %s\n",template);
close(fd);
if(stat(template,&sbuf)<0)
{
if(errno == ENOENT)
printf("file dosen't exist.\n");
else
err_sys("stat failed");
}
else
{
printf("file exists\n");
unlink(template);
}
}
代码解析:
第一个模版:由于使用了数组,使得名字在栈上分配,所以没有出错,正常返回名字。
第二个模版:由于使用了指针,只有指针本身在栈上驻留,而编译器把字符串存放在可执行文件的只读段,当mktemp视图修改字符串时,返回段错误。
十三、内存流
有三个函数可用于创建内存流。
1. fmemopen函数:#include < stdio.h >
FILE *fmemopen( void *restrict buf,size_t size,const char *restrict type )
若成功,返回流指针,若错误,返回NULLfmeopen函数允许调用者提供缓冲区用于内存流,buf参数指向缓冲区的开始位置。size参数指定了缓冲区大小的字节数,如果buf参数为空,fmemopen函数分配size字节的缓冲区。这种情况下,当流关闭时缓存区会被关闭。
type参数控制如何使用流。type可能取值如下:
type 说明 r或rb 为读而打开 w或wb 为写而打开 a或ab 追加:为第一个null字节处写而打开 r+或r+b或rb+ 为读和写而打开 w+或w+b或wb+ 把文件截断至0长,为读和写而打开 a+或a+b或ab+ 追加:为在第一个null字节处读和写而打开 注意:这些取值对应于基于文件的标准I/O流的type参数的取值,但其中有些微小差别:
第一、无论何时以追加写方式打开内存流时,当前文件位置设为缓冲区中的第一个null字节。如果缓存区中并不存在null字节,则当前位置就设为缓冲区结尾的后一个字节。当流并不是以追加写方式打开时,当前位置设为缓冲区的开始位置。因为追加写模式通过第一个null字节确定数据的尾端,内存流并不适合存储二进制数据。
第二、如果buf参数是一个null指针,打开流进行读或者写都没有任何意义。因为在这种情况下缓冲区是通过fmemopen进行分配的,没有办法找到缓冲区的地址。只写方式打开流意味着无法读取已写入的数据,同样,以读方式打开流意味着只能读取那些我们无法写入的缓冲区中的数据。
第三、任何时候需要增加流缓冲区中数据量以及调用fclose、fflush、fseek、fseeko以及fsetpos时都会在当前位置写入一个null字节。
使用已知函数来观察内存流对缓冲区的操作的程序
#include "apue.h"
#include <assert.h>
#define BSZ 48
int
main()
{
FILE *fp;
char buf[BSZ];
/*
* 利用memset先填充数据a
* 文件指针fp为fmemopen所创建的内存流(以buf作为缓冲区,BSZ作为缓冲区长度,w+代表截取0字节的读写操作)
* 利用fprintf将hello,world写入内存流fp
* fp指向缓冲区buf,利用fflush进行刷新,将数据冲洗入buf
* fp处于hello,world的末尾位置11
*/
memset(buf,'a',BSZ-2);//填充
buf[BSZ-2]='\0';
buf[BSZ-1]='X';
if((fp = fmemopen(buf,BSZ,"w+"))== NULL)
err_sys("fmemopen failed");
printf("initial buffer contents: %s\n",buf);
fprintf(fp,"hello,world");
printf("before flush: %s\n",buf);
fflush(fp);
printf("after fflush: %s\n",buf);
printf("len of string in buf = %ld\n",(long)strlen(buf));
/*
* 利用memset先填充数据b
* 文件指针fp处于位置11
* 利用fprintf将hello,world写入内存流fp
* fseek进行数据冲洗,然后将指针重定位到文件开头
*/
memset(buf,'b',BSZ-2);
buf[BSZ-2]='\0';
buf[BSZ-1]='X';
printf("%d",ftell(fp));
fprintf(fp,"hello,world");
//fflush(fp);
//printf("%s\n",buf);
fseek(fp,0,SEEK_SET);
printf("after fseek: %s\n",buf);
printf("len of string in buf = %ld\n",(long)strlen(buf));
/*
* 利用memset先填充数据c
* 文件指针fp处于位置0
* 利用fprintf将hello,world写入内存流fp
* fclose将数据冲洗出fp流,保存在缓冲区中
*/
memset(buf,'c',BSZ-2);
buf[BSZ-2]='\0';
buf[BSZ-1]='X';
fprintf(fp,"hello,world");
//printf("%s\n",buf);
printf("%d",ftell(fp));
fclose(fp);
printf("after fclose: %s\n",buf);
printf("len of string in buf = %ld\n",(long)strlen(buf));
return(0);
}
结果解析:
追加NULL的策略机制:
1.需要增加流缓冲区中的数据量。
2.调用close、fseek、fflush、fseeko以及fsetpos时
以上两者必须同时成立,才能在尾部增加null字符。
第一个模版直接调用fflush,先用截断方式打开,然后fflush进行写入,使得其新字符串长度12>被截断后字符串长度0,所以需要追加NULL。
第二个模版调用了fseek,由于第一个模版结束后fp指向位置为第12位,用fprintf继续进行写入,使得新字符串长度为24>原字符串长度12,所以需要追加NULL。
第三个模版调用fclose进行缓冲区冲洗,由于第二个模版结束后fp被置为0,指向第一个字符,利用fclose进行冲洗,新字符串长度为12,但是原字符串长度为46,所以不能追加NULL字符。
用于创建内存流的其他两个函数分别时open_memstream和open_wmemstream。
#include < stdio.h >
FILE *open_memstream(char **bufp,size_t *sizep)
#include < wchar.h >
FILE *open_wmemstream(wchar_t **bufp,size_t *sizep)
若成功,返回流指针,若出错,返回NULLopen_memstream函数创建的流是面向字节的,open_wmemstream函数创建的流是面向宽字节的。
这两个函数与fmeopen函数不同如下:
- 创建的流只能写打开。
- 不能指定自己的缓冲区,但可以分别通过bufp和sizep参数访问缓冲区地址和大小。··
- 关闭流后需要自行释放缓冲区。
- 对流添加字节会增加缓冲区大小。这两个函数在缓冲区地址和大小的使用上必须遵循一些原则:
第一:缓冲区地址和长度只有在调用fclose或fflush后才有效。
第二:这些值只有在下一次流写入或调用fclose前才有效。因为缓冲区可以增长,可能需要重新分配。如果出现这种情况,我们会发现缓冲区的内存地址值在下一次调用fclose或fflush时会改变。因为避免了缓冲区溢出,内存流非常适合用于创建字符串。因为内存流只访问主存,不访问磁盘上的文件,所以对于把标准I/O流作为参数用于临时文件的函数来说,会有很大的性能提升。
用setvbuf实现setbuf
#include "apue.h"
void set_buf(FILE *fp,char *buf)
{
//size only uses BUFSIZE;
if(buf==NULL||fp==stderr)//buffer is NULL or fp is stderr,set mode is _IONBF;
{
if(setvbuf(fp,buf,_IONBF,BUFSIZ)!=0)
{
err_sys("setvbuf error!");
return;
}
printf("open the stream by no buffer\n");
}
else
{
if(fp==stdin || fp==stdout)//when fp is stdin or stdout,set mode is _IOLBF;
{
if(setvbuf(fp,buf,_IOLBF,BUFSIZ)!=0)
{
err_sys("setvbuf error!");
return;
}
printf("open the stream by line buffer\n");
}
else
{
if(setvbuf(fp,buf,_IOFBF,BUFSIZ)!=0)//Except for above two situations,set mode is _IOFBF;
{
err_sys("setvbuf error!");
return;
}
printf("open the stream by full buffer\n");
}
}
}
//test code
int
main(void)
{
char buf[BUFSIZ];
FILE *fp;
if((fp=fopen("1","w+")))
{
set_buf(stderr,buf);//output no buffer
set_buf(stdin,NULL);//output no buffer
set_buf(stdout,buf);//output line buffer
set_buf(fp,NULL);//output no buffer
set_buf(fp,buf);//output full buffer
set_buf(stdin,buf);//output line buffer;
}
fclose(fp);
return 0;
}