设为首页 加入收藏

TOP

C 标准I/O库粗略实现(一)
2018-10-21 18:11:01 】 浏览:250
Tags:标准 I/O 粗略 实现

本文同时发表在 https://github.com/zhangyachen/zhangyachen.github.io/issues/123

写一下fopen/getc/putc等C库的粗略实现,参考了K&R,但是有几点根据自己理解的小改动,下面再具体说一下^_^

写这篇文章主要是帮助自己理解下标准I/O库大体是怎么工作的。

fopen与open之间的关系

操作系统提供的接口即为系统调用。而C语言为了让用户更加方便的编程,自己封装了一些函数,组成了C库。而且不同的操作系统对同一个功能提供的系统调用可能不同,在不同的操作系统上C库对用户屏蔽了这些不同,所谓一次编译处处运行。这里open为系统调用,fopen为C库提供的调用。

image

C库对的读写操作封装了一个缓冲区。试想假如用户频繁的对文件读写少量字符,会频繁的进行系统调用(read函数),而系统调用比较耗时。C库自己封装了一个缓冲区,每次读取特定数据量到缓冲区,读取时优先从缓冲区读取,当缓冲区内容被读光后才进行系统调用将缓冲区再次填满。

image

FILE结构体

上面我们看到一个结构体,里面有5个参数,分别记录了:缓冲区剩余的字符数cnt、下一个字符的位置ptr、缓冲区的位置base、文件访问模式flag、文件描述符fd。
其中文件描述符就是系统调用open返回的文件描述符fd,是int类型。ptr与base上面图中已经展示了。cnt是缓冲区剩余字符数,当cnt为0时,会系统调用read来填满缓冲区。flag为文件访问模式,记录了文件打开方式、是否到达文件结尾等。

结构体的具体定义如下,对应调用fopen返回的文件指针FILE *fp = fopen(xxx,r)

typedef struct _iobuf{
    int cnt;                //缓冲区剩余字节数
    char *base;     //缓冲区地址
    char *ptr;      //缓冲区下一个字符地址
    int fd;         //文件描述符
    int flag;             //访问模式
} FILE;     //别名,与标准库一致

结构体中有flag字段,flag字段可以是以下几种的并集:

enum _flags {
    _READ = 1,      
    _WRITE = 2, 
    _UNBUF = 4,  //不进行缓冲
    _EOF = 8,       
    _ERR = 16
};

我们注意到其中有一个字段,标识不进行缓冲,说明此种情况下每一次读取和输出都调用系统函数。一个例子就是标准错误流stderr : 当stderr连接的是终端设备时,写入一个字符就立即在终端设备显示。
而stdin和stdout都是带缓冲的,明确的说是行缓冲。本文不考虑行缓冲,默认都是全缓冲,即缓冲区满了才刷新缓冲区。(详细可以参考《UNIX环境高级编程》标准I/O库章节)。

现在我们可以初始化stdin、stdout与stderr:

FILE _iob[OPEN_MAX] = {
    {0,NULL,NULL,_READ,0},
    {0,NULL,NULL,_WRITE,1},
    {0,NULL,NULL,_WRITE|_UNBUF,2}
};

_ferror/_feof/_fileno

//判断文件流中是否有错误发生
int _ferror(FILE *f){
    return f-> flag & _ERR;
}
//判断文件流是否到达文件尾
int _feof(FILE *f){
    return f-> flag & _EOF;
}
//返回文件句柄,即open函数的返回值
int _fileno(FILE *f){
    return f->fd;
}

_fopen

FILE *_fopen(char *file,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 == 0){ 
            break;
        }
    }
    if(fp >= _iob + OPEN_MAX){
        return NULL;
    }

    if(*mode == 'w'){
        fd = creat(file,PERMS);
    }else if(*mode == 'r'){
        fd = open(file,O_RDONLY,0);
    }else{      //a模式
        if((fd = open(file,O_WRONLY,0)) == -1){
            fd = creat(file,PERMS);
        }
        lseek(fd,0L,2);     //文件指针指向末尾
    }
    if(fd == -1){
        return NULL;
    }

    fp->fd = fd;
    fp->cnt = 0;        //fopen不分配缓存空间
    fp->base = NULL;
    fp->ptr = NULL;
    fp->flag = *mode == 'r' ? _READ : _WRITE;

    return fp;
}

fopen的处理过程:

  • 判断打开模式的合法性。
  • 在_iob中寻找一个空闲位置,找不到的话说明程序打开的文件数已经到达的最大值,不能再打开新的文件。
  • 如果是w模式,创建一个新文件。如果是r模式,以只读方式打开文件。如果是a模式,首先打开文件,如果打开失败则创建文件,否则通过系统调用lseek将文件指针置到末尾。
  • 对FILE结构体进行初始化,注意fopen不会分配缓冲区。

_getc
getc的作用是从文件中返回下一个字符,参数是文件指针,即FILE:

int _getc(FILE *f){
    return --f->cnt >= 0 ? *f->ptr++ : _fillbuf(f);
}

对照上面的图示:当缓冲区中还有剩余字符待读取时,读取该字符并返回,并将缓冲区指针向后移动一个char单位,否则就调用_fillbuf函数填满缓冲区,_fillbuf的返回值就是待读取的字符。

这里有一个问题:当读取到最后一个字符时,cnt为0,但是ptr已经越界了,如下图:
image

这种情况虽然是非法的,但是C语言中保证:数组末尾之后的第一个元素(即&arr[n],或者arr + n)的指针算术运算可以正确执行。下面的例子也是上述的一种应用场景:

int a[10] = {1,2,3,4,5,6,7,8,9,10};

for(int *x = a;x < a + 10; x++){
    printf("%d\n",*x);
}

当for循环到最后一步时,x也指向了a[10],虽然是非法的,但是C语言保证可以正确执行,只要不出现如下情况就ok:

int a[10] = {1,2,3,4,5,6,7,8,9,10};

int *x;
for(x = a;x < a + 10; x++){
    printf("%d\n",*x);
}

*x = 11;     //越界进行值访问

_fillbuf
我们看下_getc中的_fillbuf的实现,_fillbuf是当缓冲区没有可以读取的字符时,通过系统调用read读取一定字节的数据填满缓冲区,供之后使用:

int _fillbuf(FILE *f){

    int bufsize;

        if((f->flag & (_READ | _EOF | _ERR)) !=
首页 上一页 1 2 3 4 下一页 尾页 1/4/4
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇十六进制带小数转换成十进制 下一篇关于C语言中static保留字的使用

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目