C语言K&R圣经笔记 8.5样例 - fopen和getc的实现

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);
    } else
        fd = 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;
        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 是无缓冲写。

练习8-2、重写 fopen 和 _fillbuf,使用位域而不是显式的比特位操作。比较代码量和执行速度的差异。

练习8-3、设计并实现 _flushbuf、fflush 和 fclose。

练习8-4、实现标准库函数 fseek

int fseek(FILE *fp, long offset, int origin)

它与 lseek 相同,区别仅在于 fp 是文件指针而不是文件描述符,且返回值是 int 而不是位置。保证你写的 fseek 与标准库中其他函数的缓存能一起协调配合。

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值