8.5 样例 - fopen 和 getc 的实现
通过给出标准库例程 fopen 和 getc 的一个实现,我们来说明如何将前面这些内容组合起来。
回忆一下,在标准库中,文件用文件指针而不是文件描述符来描述。文件指针是包含一些文件信息的结构体指针:缓存指针,有缓存的话文件就能以大块的方式读入;缓存区剩下的字符个数;指向缓存区下一个字符的指针;文件描述符;表示读写模式、错误状态等信息的标志位。
用于描述文件的数据结构包含在 <stdio.h> 中,任何需要使用标准输入/输出库例程的源文件,都必须通过 #include 包含它。标准库中的很多函数也会包含它。下面的代码片段来自一个典型的 <stdio.h> 头文件,其中只打算被库函数使用的名称以下划线开头,这样它们与用户程序中名称冲突的可能性就更小。所有标准库的例程都采用这个约定。
#define NULL 0
#define EOF (-1)
#define BUFSIZE 1024
#define OPENMAX 20 /* 一次能打开的最多文件数 */typedef struct _iobuf {int cnt; /* 剩余的字符数 */char *ptr; /* 下一个字符位置 */char *base; /* 缓存区的位置 */int flag; /* 文件访问模式 */int fd; /* 文件描述符 */
} FILE;
extern FILE _iob[OPEN_MAX];#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])enum _flags {_READ = 01, /* 打开文件用于读 */_WRITE = 02, /* 打开文件用于写 */_UNBUF = 04, /* 文件无缓存 */_EOF = 010, /* 文件发生了EOF */_ERR = 020 /* 文件发生了错误 */
};int _filbuf(FILE *);
inf _flushbuf(int, FILE *);#define feof(p) (((p)->flag & _EOF) != 0)
#define ferror(p) (((p)->flag & _ERR) != 0)
#define fileno(p) ((p)->fd)#define getc(p) (--(p)->cnt >= 0 \? (unsigned char) *(p)->ptr++ : _fillbuf(p))
#define putc(x,p) (--(p)->cnt >=0 \? *(p)->ptr++ = (x) : _flushbuf((x), p))#define getchar() getc(stdin)
#define putchar(x) putc((x), stdout)
getc 宏通常是对字符数量递减,向前移动指针,并返回字符。(回忆一下,长的 #define 通过反斜杠来续行。)然而,如果数量变为负,则 getc 调用 _fillbuf 来重新填充缓存,重新初始化结构体的内容,并返回一个字符。字符返回成 unsigned,以保证所有字符都是正数。
尽管并不准备讨论任何细节,我们还是把 putc 的定义包含进来,以说明它和 getc 的工作方式大致相同,在缓存满时调用 _flushbuf 函数。代码里还包含了获取错误状态和文件结束状态,以及文件描述符的宏。
现在可以写 fopen 函数了。open 函数主要关注的是:打开文件,定位到正确的位置,设置标志位以表示正确的状态。fopen 不分配任何缓存空间;缓存是文件首次打开时由 _fillbuf 来做的。
#include <fcntl.h>
#include "syscalls.h"
#define PERMS 0666 /* 文件属主、组和其他用户都可读写 *//* fopen: 打开文件,返回文件指针 */
FILE *fopen(char *name, char *mode)
{int fd;FILE *fp;if (*mode != 'r' && *mode != 'w' && *mode != 'a')return NULL;for (fp = _iob; fp < _iob + OPEN_MAX; fp++)if ((fp->flag & (_READ | _WRITE)) == 0)break; /* 找到了空闲槽位 */if (fp >= _iob + OPEN_MAX) /* 没有空闲槽位 */return NULL;if (*mode == 'w')fd = creat(name, PERMS);else (*mode == 'a') {if ((fd = open(name, O_WRONLY, 0)) == -1)fd = creat(name, PERMS);lseek(fd, 0L, 2);} elsefd = open(name, O_RDONLY, 0);if (fd == -1) /* 无法访问 */return NULL;fp->fd = fd;fp->cnt = 0;fp->base = NULL;fp->flag = (*mode == 'r') ? _READ : _WRITE;return fp;
}
这个版本的 fopen 没有处理标准中所有可能的模式,不过要加上也不用太多代码。特别指出,这个版本的 fopen 不认识表示二进制模式的 “b”,因为在 UNIX系统上没有意义,也不认识表示同时允许读和写的 “+” 号。
对某个文件的首次 getc 调用会发现数量为 0,这就使 _fillbuf 被强制调用。如果 _fillbuf 发现文件没有为读而打开,它马上返回 EOF。否则,它尝试分配一个缓存(如果读需要缓存)。
一旦缓存建立起来,_fillbuf 会调用 read 来填充缓存,设置数量和指针,并返回缓存头部的字符。后续调用 _fillbuf 时会发现缓存已经分配好了。
#include "syscalls.h"/* _filbuf: 分配并填充输入缓存 */
int _fillbuf(FILE *fp)
{int bufsize;if ((fp->flag & (_READ|_EOF|ERR)) != _READ)return EOF;bufsize = (fp->flag & _UNBUF) ? 1 : BUFSIZ;if (fp->base == NULL) /* 还没有缓存 */if ((fp->base = (char *)malloc(bufsize)) == NULL)return EOF; /* 无法获取缓存 */fp->ptr = fp->base;fp->cnt = read(fp->fd, fp->ptr, bufsize);if (--fp->cnt < 0) {if (fp->cnt == -1)fp->flag |= _EOF;elsefp->flag |= _ERR;fp->cnt = 0;return EOF;}return (unsigned char) *fp->ptr++;
}
剩下还没说的是从哪里开始。必须定义 _iob 数组,并初始化 stdin,stdout 和 stderr 。
FILE _iob[OPEN_MAX] = { /* stdin, stdout, stderr: */{ 0, (char *)0, (char *)0, _READ, 0 },{ 0, (char *)0, (char *)0, _WRITE, 1 },{ 0, (char *)0, (char *)0, _WRITE |_UNBUF, 2 }
};
结构体中 flag 成员的初始化代码表明了 stdin 用来读,stdout 用来写,而 stderr 是无缓冲写。
练习8-2、重写 fopen 和 _fillbuf,使用位域而不是显式的比特位操作。比较代码量和执行速度的差异。
练习8-3、设计并实现 _flushbuf、fflush 和 fclose。
练习8-4、实现标准库函数 fseek
int fseek(FILE *fp, long offset, int origin)
它与 lseek 相同,区别仅在于 fp 是文件指针而不是文件描述符,且返回值是 int 而不是位置。保证你写的 fseek 与标准库中其他函数的缓存能一起协调配合。