[APUE]标准IO库(上)
一、流和FILE对象
系统IO都是针对文件描述符,当打开一个文件时,即返回一个文件描述符,然后用该文件描述符来进行下面的操作,而对于标准IO库,它们的操作则是围绕流(stream)进行的。
当打开一个流时,标准IO函数fopen返回一个指向FILE对象的指针。该对象通常是一个结构,它包含了IO库为管理该流所需要的所有信息:用于实际IO的文件描述符,指向流缓存的指针,缓存的长度,当前在缓存中的字符数,出错标志等等。
我们称指向FILE对象的指针(类型为FILE *)为文件指针。
二、缓存
标准IO提供缓存的目的是尽可能减少使用read和write调用的数量。标准IO提供了三种类型的缓存
(1) 全缓存。在这种情况下,当填满标准IO缓存后才进行实际IO操作。对于驻在磁盘上的文件通常是由标准IO实施全缓存的。在一个流上执行第一次IO操作时,相关标准IO函数通常调用malloc获得所需的缓存。
术语刷新(flush)说明标准IO缓存的写操作。缓存可由标准IO例程自动地刷新(例如当填满一个缓存时),或者可以调用函数flush刷新一个流。在UNIX环境中刷新有两种意思。在标准IO库方面,刷新意味着将缓存中的内容写入到磁盘上(该缓存可以只是局部填写 的)。在终端驱动程序方面,刷新表示丢弃已存在缓存中的数据。
(2) 行缓存。在这种情况下,当在输入和输出中遇到换行符时,标准IO库执行IO操作。这允许我们一次输入一个字符(用标准IO fputc函数),但只有在写了一行之后才进行实际IO操作。当流涉及一个终端时(例如标准输入和标准输出),典型的使用行缓存。
对于行缓存有两个限制,一是:因为标准IO库用来收集每一行的缓存长度是固定的,所以只要填满了缓存,那么即使还没有写一个换行符,也进行IO操作。第二是:任何时候只要通过标准输入输出库要求从(a)一个不带缓存的流(b)一个行缓存的流(它预先要求从内 核得到数据)得到输入数据,那么就会造成刷新所有行缓存输出流。在(b)中带了一个在扩号中的说明的理由是,所需的数据可能已在缓存中,它并不要求内核在需要该数据时才进行操作。很明显,从不带缓存的一个流中进行输入((a)项)要求当时从内核中得到数 据。
(3) 不带缓存。标准IO库不对字符进行缓存。标准出错流通常不带缓存,这可以使出错信息尽快的显示出来。
ASSI C要求以下缓存特征:
(1) 当且仅当标准输入和标准输出不涉及交互作用设备时,他们才是全缓存的。
(2) 标准出错绝不是全缓存的。
对于任何一个流如果我们不喜欢系统默认则可以通过以下两个函数来更改缓存类型:
#include <stdio.h> void setbuf(FILE *fp, char *buf); int setvbuf(FILE *fp, char *buf, int mode, size_t size);
这些函数必须在流打开后和对该流进行任何操作之前调用。
可以使用setbuf打开或关闭缓存机制。为了带缓存进行IO,参数buf必须指向一个长度为BUFSIZ的缓存(该常数定义在stdio.h中)。通常在此之后该流就是全缓存的,但是该流与终端设备相关,那么某些系统也可将其设置为行缓存。为了关闭缓存,将buf设置为NULL。
使用setvbuf可以精确的说明所需的缓存类型。由mode参数指定:
_IOFBF 全缓存
_IOLBF 行缓存,
_IONBF 不带缓存
如果指定了一个不带缓存的流,则忽略buf和size参数。如果指定全缓存或行缓存,则buf和size可以选择地指定一个缓存及长度。如果该流是带缓存的,而buf是NULL,则标准IO库将自动的为该流分配适当长度的缓存,适当长度是指struct stat结构体中的st_blksize所指定的值。如果系统不能为为该流决定此值(例如该流涉及一个设备或一个管道),则分配长度为BUFSIZ的缓存。
下表列出了这两个函数的动作,以及它们的各个选择项
如果在一个函数中分配了一个自动变量类的标准IO缓存,则从该函数返回之前必须关闭该流。SVR4将缓存的一部分用于ta自己的管理操作,所以可以存放在缓存中的实际数据字节数小于size。一般来说,应由系统选择缓存的长度,并自动分配缓存。在这样处理时,标准IO库在关闭此流时将自动关闭释放缓存。
可以在任何时候强制刷新一个流:
#include <stdio.h>
int fflush(FILE *fp);
此函数使该流所有的数据传递到内核,如果fp是NULL,则此函数刷新所有输出流。
三、打开流
#include <stdio.h> FILE *fopen(const char *pathname, const char *type); FILE *freopen(const char *pathname, const char *type, FILE fp); FILE *fdopen(int fileds, const char *type); 返回值: 成功文件指针,失败NULL
三个函数的区别:
(1)fopen打开路径为pathname的文件。
(2)freopen在一个特定流上(由fp指定)打开pathname文件。如果该流已打开则先关闭。此函数一般用于将一个文件打开为一个预定义的流:标准输入、标准输出和标准出错。
(3)fdopen取一个现存的文件描述符(可能从open\dup\dup2\fcntl\pipe函数得到此文件描述符)并使一个标准的IO流与该文件描述符结合。此函数常用于由创建管道和网络通信通道函数获得的插入符。因为这些特殊类型的文件不能用标准IO fopen打开
type参数指定对该IO流的读、写方式,ANSI C规定type参数可以有15种值:
使用字符b作为type的一部分使得标准IO系统可以区分文本文件或二进制文件。因为UNXI并不对这两种文件区分所以在UNIX环境下指定b作为type的一部分实际上并无作用。
对于fdopen,type参数的意义有些区别。因为文件描述符已打开,所以fdopen为写而打开并不截短该文件。另外,标准IO添加方式也不能用于创建文件,因为如果一个文件描述符引用一个文件则该文件一定已经存在。
当用添加类型打开一个文件后则每次写都将数据写到文件当前尾端处。如果有多个进程用标准IO的方式打开同一个文件,则来自每个进程的数据都将正确的写到文件中。
当以读和写打开一文件时(type中+号),具有如下限制:
如果中间没有fflush、fseek、fsetpos或rewind,则在输出的后面不能直接跟随输入。
如果中间没有fseek、fsetpos、或rewind或者一个输出操作没有到达文件尾端,则在输入操作之后不能直接跟随输出。
下表是打开一个流的六种不同的方式:
在指定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
在该文件关闭之前,刷新缓存中的输出数据。缓存中的输入数据被丢弃。如果标准IO库已经为该流自动分配了一个缓存则释放该缓存。
当一个进程正常终止时(直接调用exit()函数或者从main函数中返回),则所有带未写缓存数据的标准IO流都被刷新,所有打开的标准IO流都被关闭。
四、读和写流
一旦打开了流,则可以在以下三种不同类型的非格式化IO中进行选择,对其进行读写:
(1) 每次一个字符的IO。一次读或写一个字符,如果流是带缓存的,则标准IO函数处理所有缓存。
(2) 每次一行的IO。使用fgets和fputs一次读或写一行。每行都以一个新行符结束。当调用fgets时应说明能处理的最大行长。
(3) 直接IO。 fwrite和fread函数支持这种类型的IO。每次IO操作读或写某种数量的对象,每个对象具有指定的长度。这两个函数常用于从二进制文件中读或写一个结构。
直接IO(direct IO)这个术语来自ANSI C标准,有时也被称为:二进制IO、一次一个对象IO、面向记录的IO或面向结构的IO。
1.输入函数
以下三个函数用于一次读一个字符:
#include <stdio.h> int getc(FILE *fp); int fgetc(FILE *fp); int getchar(void); 返回值:成功则为下一个字符,如果已处于文件尾端或出错则为EOF
getchar等同于getc(stdin)。前两个函数的区别是getc可被实现为宏,而fgetc则不能。(这里的可被实现为宏即大多数UNIX系统中getc的实现是这样:在<stdio.h>中#define getc(FILE *fp) xxx(FILE *fp),即getc不是一个函数而是一个宏)这意味着:
(1) getc的参数不应当是具有副作用的表达式。
(2) 因为fgetc一定是个函数,所以可得到其地址。这就允许将fgetc的地址作为一个参数传送给另一个函数。
(3) 调用fgetc所需时间可能长于调用getc,因为调用函数通常所需的时间长于调用宏。
这三个函数以unsigned char类型转换为int的方式返回下一个字符。返回int的原因是函数可以返回一个负值(指示出错或已到达文件尾端),在<studio.h>中常数EOF常要求是一个负值,其值经常是-1。所以不能将这三个函数的返回值放到一个字符变量中。
不管是出错还是到达文件尾端,这三个函数都返回同样的值。为了区分这两种不同的情况,须调用ferror或feof。
#include <stdio.h> int ferror(FILE *fp); int feof(FILE *fp); 返回值:条件为真返回非0值,否则0 void clearerr(FILE *fp);
在大多数实现的FILE对象中,为每个流保持了两个标志
- 出错标志
- 文件结束标志
从一个流读之后,可以调用ungetc将字符再送回到流中。
#include <stdio.h> int ungetc(int c, FILE *fp);
送回到流中的字符又可以从流中读出,但读出字符的顺序与送回的顺序相反。回送的字符不一定是上一次读到的字符。EOF不能回送。但是当已到达文件尾端时仍可以回送一个字符。下次读将返回该字符,再次读则返回EOF。之所以可以这样做的原因是一次成功的ungetc调用会清除该流的文件结束指示。
当正在读一个输入流,并进行某种形式的分字或分记号操作时,会经常用到回送字符操作。有时需要先看一看下一个字符以决定如何处理当前字符。然后就需要方便的将刚查看的字符送回,以便下一次调用getc时返回该字符。
2. 输出函数
#include <stdio.h> int putc(int c, FILE *fp); int fputc(int c, FILE *fp); int putchar(int c); 返回值:成功返回c,出错EOF
putchar等同于putc(c, stdout)
五、 每次一行IO
下面两个函数提供每次输入一行的功能:
#include <stdio.h> char *fgets(char *buf, int n, FILE *fp); char *fgets(char *buf);
gets从标准输入读。对于fgets必须指定缓存buf的长度n。此函数会一直读到下一个新行符为止,但是不超过n-1个字符,读入的字符被送入到缓存。该缓存以null字符结尾。如果该行包括最后一个新行符的字符数超过n-1,则只返回一个不完整的行,而且缓存总是以null字符结尾。对fgets的下一次调用会继续该行。
gets函数不被推荐使用因为不能指定缓存的长度,gets并不将新行符存入缓存中。
#include <stdio.h> int fputs(const char *str, FILE *fp); int puts(const char *str);
返回值:成功返回非负值,出错EOF
函数fputs将一个以null符终止的字符串写入到指定的流,终止符null不写出。这并不一定是每次输出一行,因为它并不要求在null符之前一定是新行符,但是通常在null符之前是一个新行符。
puts将一个以null符终止的字符串写入到标准输出,终止符不写出。但是puts然后将一个新行符写到标准输出。