8.5 实例(fopen
和 getc
函数的实现)
下面以标准库函数 fopen
和 getc
的一种实现方法为例来说明如何将这些系统调用结合起来使用
标准库中的文件不是通过文件描述符描述的,而是使用文件指针描述的
文件指针是一个指向包含文件各种信息的结构的指针,该结构包含下列内容:
- 一个指向缓冲区的指针,通过它可以一次读入文件的一大块内容
- 一个记录缓冲区中剩余的字符数的计数器
- 一个指向缓冲区中下一个字符的指针
- 文件描述符
- 描述读/写模式的标志
- 描述错误状态的标志
- 其它
描述文件的数据结构包含在头文件 <stdio.h>
中
任何需要使用标准输入/输出库中函数的程序都必须在源文件中包含这个头文件(通过 #include
指令包含头文件)
此文件也被库中的其它函数包含
在下面这段典型的 <stdio.h>
代码段中,只供标准库中其它函数所使用的名字以下划线开始,因此一般不会与用户程序中的名字冲突
所有的标准库函数都遵循该约定
#define NULL 0
#define EOF (-1)
#define BUFSIZ 1024
#define OPEN_MAX 20 /* max #files open at once */
typedef struct _iobuf {
int cnt; /* characters left */
char *ptr; /* next character position */
char *base; /* location of buffer */
int flag; /* mode of file access */
int fd; /* file descriptor */
} FILE;
extern FILE _iob[OPEN_MAX];
#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])
enum _flags {
_READ = 01, /* file open for reading */
_WRITE = 02, /* file open for writing */
_UNBUF = 04, /* file is unbuffered */
_EOF = 010, /* EOF has occurred on this file */
_ERR = 020 /* error occurred on this file */
};
int _fillbuf(FILE *);
int _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
一般先将计数器减 1
,将指针移到下一个位置,然后返回字符
一个长 #define
语句可用反斜杠分成几行
如果计数值变为负值,getc
就调用函数 _fillbuf
填充缓冲区,重新初始化结构的内容,并返回一个字符
返回的字符为 unsigned
类型,以确保所有的字符为正值
尽管在这里我们并不想讨论一些细节,但程序中还是给出了 putc
函数的定义,以表明它的操作与 getc
函数非常类似
当缓冲区满时,它将调用函数 _flushbuf
此外,我们还在其中包含了访问错误输出、文件结束状态和文件描述符的宏
fopen
函数的主要功能是打开文件,定位到合适的位置,设置标志位以指示相应的状态
它不分配任何缓冲区空间,缓冲区的分配是在第一次读文件时由函数 _fillbuf
完成的
#include <fcntl.h>
#include "syscalls.h"
#define PERMS 0666 /* RW for owner, group, others */
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; /* found free slot */
if (fp >= _iob + OPEN_MAX) /* no free slots */
return NULL;
if (*mode == 'w')
fd = creat(name, PERMS);
else if (*mode == 'a') {
if ((fd = open(name, O_WRONLY, 0)) == -1)
fd = creat(name, PERMS);
lseek(fd, 0L, 2);
} else
fd = open(name, O_RDONLY, 0);
if (fd == -1) /* couldn't access name */
return NULL;
fp->fd = fd;
fp->cnt = 0;
fp->base = NULL;
fp->flag = (*mode == 'r') ? _READ : _WRITE;
return fp;
}
该版本的 fopen
函数没有涉及标准 C 的所有访问模式,但是,加入这些模式并不需要增加多少代码
特别是,该版本的 fopen
不能识别表示二进制访问方式的 b
标志,这是因为,在 UNIX 系统中这种方式是没有意义的
同时,它也不能识别允许同时进行读和写的 +
标志
对于某一特定的文件,第一次调用 getc
函数时计数值为 0
,这样就必须调用一次函数 _fillbuf
如果 _fillbuf
发现文件不是以读方式打开的,它将立即返回 EOF
否则,它将试图分配一个缓冲区(如果读操作是以缓冲方式进行的话)
建立缓冲区后,_fillbuf
调用 read
填充此缓冲区,设置计数值和指针,并返回缓冲区中的第一个字符
随后进行的 _fillbuf
调用会发现缓冲区已经分配
#include "syscalls.h"
/* _fillbuf: allocate and fill input buffer */
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) /* no buffer yet */
if ((fp->base = (char *) malloc(bufsize)) == NULL)
return EOF; /* can't get buffer */
fp->ptr = fp->base;
fp->cnt = read(fp->fd, fp->ptr, bufsize);
if (--fp->cnt < 0) {
if (fp->cnt == -1)
fp->flag |= _EOF;
else
fp->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
执行缓冲方式的写操作