C 标准I/O库粗略完毕

正文同时发布在
https://github.com/zhangyachen/zhangyachen.github.io/issues/123

写一下fopen/getc/putc等C库的简要实现,参考了K&PAJERO,不过有几点根据自身知道的小改变,下边再具体说一下^_^

写那篇文章主倘若支援协调掌握下标准I/O库大体是怎么工作的。

fopen与open之间的涉嫌

操作系统提供的接口即为系统调用。而C语言为了让用户尤其有益的编制程序,自个儿包装了有个别函数,组成了C库。而且分化的操作系统对同1个效果提供的种类调用只怕两样,在不一样的操作系统上C库对用户屏蔽了这个不相同,所谓二次编写翻译随处运维。那里open为系统调用,fopen为C库提供的调用。

C语言 1

C库对的读写操作封装了贰个缓冲区。试想如若用户频仍的对文件读写少量字符,会1再的展开系统调用(read函数),而系统调用相比较耗费时间。C库本身包装了二个缓冲区,每回读取特定数据量到缓冲区,读取时事先从缓冲区读取,当缓冲区内容被读光后才开始展览系统调用将缓冲区再一次填满。

C语言 2

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
};

咱俩注意到中间有1个字段,标识不举办缓冲,表达此种意况下每3遍读取和出口都调用系统函数。一个事例正是正式错误流stderr
: 当stderr连接的是极限设备时,写入3个字符就马上在极限设备展现。
而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中找找3个空余地点,找不到的话表明程序打开的文件数已经抵达的最大值,不能够再打开新的文本。
  • 万1是w形式,创建2个新文件。要是是r格局,以只读方式打开文件。借使是a情势,首先打开文件,借使打开战败则创设文件,不然通过系统调用lseek将文件指针置到最终。
  • 对FILE结构体进行初阶化,注意fopen不会分配缓冲区。

_getc getc的功效是从文件中回到下三个字符,参数是文本指针,即FILE:

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

比较下边包车型客车图示:当缓冲区中还有剩余字符待读取时,读取该字符并回到,并将缓冲区指针向后移动3个char单位,不然就调用_fillbuf函数填满缓冲区,_fillbuf的重返值正是待读取的字符。

此间有贰个标题:当读取到终极八个字符时,cnt为0,但是ptr已经越界了,如下图:
C语言 3

C语言,那种状态固然是地下的,但是C语言中保险:数组末尾之后的率先个要素(即&arr[n],或者arr

  • n)的指针算术运算能够正确实施。下边包车型地铁例证也是上述的1种选拔场景:

    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)) != _READ){     //判断文件是否可读
        return EOF;
    }

    bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;

    if(f->base == NULL){            //没有分配过缓冲区
        if((f->base = (char *)malloc(bufsize)) == NULL){
            return EOF;
        }
    }

    f->ptr = f->base;
        int n = read(f->fd,f->ptr,BUFSIZ);      //系统调用read
        if(n == 0){         //到达文件结尾
        f->base = NULL;
        f->cnt = 0;
        f-> flag |= _EOF;
        return EOF;
        }else if(n == -1){      //出错
                f->cnt= 0;
        f->flag |= _ERR;
                return EOF;
    }else{
        f->cnt = --n;
        return *f->ptr++;   
    }
}

_fillbuf的处理过程:

  • 认清文件是或不是可读
  • 认清调用read函数时应该读取的字节数,当文件设置了无缓冲时,读取二个字节,不然读取BUFSIZ个字节,BUFSIZ在中定义,定义了在该操作系统条件下缓冲区的极品大小。
  • 判断是不是分配过缓冲区(fopen不会分配缓冲区,会再第3回调用getc时分配)。
  • 调用系统函数read。
  • 判定read重返值,分为到达文件结尾、出错和正规读取三种状态。
  • 常规意况下回到缓冲区第一个字符给getc函数,并将cnt减壹。

此间注意,到达文件结尾和失误都以回来EOF,差别是前者会将flag的_EOF地方一,后者会将flag的_E凯雷德帕杰罗地方一,上游能够透过feofferror函数实行判断(那三个函数在上面已经落到实处过了)。

The character read is returned as an int value.
If the End-of-File is reached or a reading error happens, the function returns EOF and the corresponding error or eof indicator is set. You can use either ferror or feof to determine whether an error happened or the End-Of-File was reached.

_putc

int _putc(int x,FILE *f){
    return --f->cnt >= 0 ? *f->ptr++ = x : _flushbuf(x,f);
}

与_getc的完结相似,将写入的字符放到ptr指向的地方,并将ptr向后活动一人。当缓冲区满时,调用_flushbuf将缓冲区内容刷新到文件中。

_flushbuf

int _flushbuf(int x,FILE *f){

    if((f->flag & (_WRITE | _EOF | _ERR)) != _WRITE){     //判断文件是否可写
        return EOF;
    }

    int n;
    int bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;
    if(f->base != NULL){
        n = write(f->fd,f->base,f->ptr - f->base);      //判断需要写入多少字节
        if(n != f->ptr - f->base){
            f->flag |= _ERR;
            return EOF;
        }
    }else{
        if((f->base = (char *)malloc(bufsize)) == NULL){
            f->flag |= _ERR;
            return EOF;
        }
    }

    if(x != EOF){
        f->cnt = bufsize - 1;
        f->ptr = f->base;
        *f->ptr++ = x;
    }else{          //当写入EOF时,代表强制刷新缓冲区内容到文件中
        f->cnt = bufsize;
        f->ptr = f->base;
    }
    return x;
}

_flushbuf的处理进度:

  • 判断文件是或不是可写。
  • 当已分配过缓冲区时,将缓冲区的始末通过系统调用write写入文件中。
  • 当未有分配过缓冲区时,分配缓冲区。
  • 判定当写入的字符为EOF时,表明调用此函数的目标为恐吓刷新缓冲区,不写入字符。将cnt赋值为BUFSIZ,ptr赋值为缓冲区首地址base。
  • 当写入字符不为EOF时,表明缓冲区已满,要求将缓冲区刷新到文件中。cnt为BUFSIZE –
    1,将写入的字符x放到到缓冲区的第二格,然后将ptr向后移动三个char单位。

在意,调用write函数时,写入的字节数不可能写死为一依然BUFSIZ:

n = write(f->fd,f->base,f->ptr - f->base);      //判断需要写入多少字节

C语言 4

如上海教室,大家要求写入base至ptr之间的多寡,而不是BUFSIZ,因为我们恐怕会强制刷新缓冲区而不是等到缓冲区满了才刷新缓冲区。

再有有个别:当大家想要强制刷新缓冲区时,第二个参数x该传入什么啊?K&汉兰达传递的是字符0,但是作者认为那样会传染缓冲区,所以小编的兑现是流传三个出奇字符EOF,依据EOF来做分歧的拍卖:

if(x != EOF){
    f->cnt = bufsize - 1;
    f->ptr = f->base;
    *f->ptr++ = x;
}else{          //当写入EOF时,代表强制刷新缓冲区内容到文件中
    f->cnt = bufsize;
    f->ptr = f->base;
}

当缓冲区满时,刷新缓冲区后缓冲区的变现:
C语言 5
当强制刷新缓冲区时,缓冲区的表现:
C语言 6
可是依照K&Kuga的不二等秘书诀来强制刷新缓冲区时,缓冲区的显示:
C语言 7

如此那般会污染缓冲区,所以本身的贯彻是传播EOF来强制刷新缓冲区。

_fflush _fflush(FILE *f)的效果是把缓冲区内容写入文件。当参数为空时,会刷新全体文件:

int _fflush(FILE *f){

        int res = 0;
    if(f == NULL){
        for(int i = 0; i < OPEN_MAX; i++){          //当参数为NULL时,刷新所有的文件流
                       if((f->flag & _WRITE) && (_fflush(&_iob[i]) == -1)){  //有一个出错即返回-1
                           res = EOF;
                       }
        }
        }else{
            if(f->flag & _WRITE){
                    _flushbuf(EOF,f);
            }else{
                res = EOF;
            }
        }

    if(f->flag & _ERR){     //出错
                res = EOF;
    }
        return res;
}

_fflush的处理进程:

  • 认清参数是不是为空,要是为空的话,遍历_iob数组,将具有文件流都强制刷新。
  • 只要参数不为空,判断文件是或不是可写,再调用_flushbuf进行刷新,注意此处传递给_flushbuf的参数是EOF。还有某个便是判定_flushbuf是不是出错不是判断再次回到值是不是为-一,因为参数为EOF时的重返值也为-一,所以这边用flag
    & _E本田UR-VHighlander判断是或不是出错。

留意,那里我们只针对可写的文本流实行操作,忽略了只读的公文流:

If the stream was open for reading, the behavior depends on the specific implementation. In some implementations this causes the input buffer to be cleared.

针对只读的文本流,分裂系统处理的办法不均等,有的系统会清空缓冲区。

_fclose

int _fclose(FILE *f){
    int ret;
    if((ret = _fflush(f)) != EOF){
        free(f->base);
        f->base = NULL;
        f->ptr = NULL;
        f->fd = 0;
        f->flag = 0;        
                f->cnt=0;
    }

    return 0;
}

fclose调用fflush函数,有限协理在文件关闭前将缓冲区中的内容刷到文件中,并且释放掉缓冲区的内部存储器空间。

_fseek 关于fseek的介绍请看fseek

int _fseek(FILE *f,long offset,int origin){

    int rc;

    if(f->flag & _READ) {
        if(origin == 1) {
            offset -= f->cnt;
        }
        rc = lseek(f->fd,offset,origin);
                f->cnt = 0;             //将缓冲区剩余字符数清0
    }else if(f->flag & _WRITE) {
                rc = _fflush(f);       //强制刷新缓冲区
        if(rc != EOF) {
            rc = lseek(f->fd,offset,origin);
        }
    }

    return rc == -1 ? EOF : 0;
}

当文件流为可读时,见下图:
C语言 8

由于有缓冲区的存在,大家直觉上的文书指针地点和真正的文本指针地点是见仁见智的,差了cnt个单位长度。所以当我们设置移动offset个长度时,真实的文件指针须要活动offset-cnt个单位长度(offset为正数或然负数)。
从此以后大家需求将cnt置为0,以便下次读取时将缓冲区的数据更新。
当origin为0大概二时,直接调动lseek即可。

而当文件流为可写时,见下图:
C语言 9

诚实的文书指针地方与大家直觉上的文本指针地方差了ptr –
base个单位长度,即大家新写入缓冲区的内容长度,所以大家直接调用_fflush即可。(K&奥迪Q7中央直属机关接调用的write,不过笔者认为这么没有重置ptr指针的地方和cnt,那样的话base与ptr之间的情节会被刷入到文件中三遍)。

当文件是以a情势打开时,fseek无效:

a+     Open for reading and appending (writing at end of file).  The file is created if it does not exist.  The initial file 
       position for  reading is at the beginning of the file, but output is always appended to the end of the file.

_getchar

int _getchar(){
  return _getc(stdin);
}

咱俩得以窥见,_getchar调用的便是_getc,只不过_getc能够流传任意的公文指针,而对_getchar来说,_getc传入的是stdin,也正是{0,NULL,NULL,_READ,0}

C语言 10

  • 当调用getchar时,首先去stdin结构体中的缓存取数据,若是缓存为空,会在_fillbuf中的int n = read(f->fd,f->ptr,BUFSIZ); //系统调用read处阻塞住,等待用户输入字符。
  • 当行业内部输入(stdin)连接的是终极时,终端I/O会采取规范形式输入处理:对于极端输入以行为单位开展拍卖,对于各种读请求,终端设备输入队列会回到一行(用户输入的字符会缓存在顶峰输入队列中,直到用户输入二个行定界符,输入队列中的数据会重临给read函数)。
  • 那壹行数据会缓存在标准I/O缓冲区中,下次调用getchar时会重临缓冲区第3个字符。当缓冲区数据被读光时,重复上述进程。

_putchar

int _putchar(int x){
    return _putc(x,stdout);
}

我们得以窥见,_putchar调用的正是_putc,只不过_putc能够流传任意的文书指针,而对_putchar来说,_putc传入的是stdout,也正是{0,NULL,NULL,_WRITE,1}

C语言 11

  • 调用putchar时,数据会缓存在stdout中的缓冲中。
  • 当stdout的缓冲棉被服装满时,会调用write将数据写入到stdout中,stdout将数据写入到极点设备出口队列中,输出队列将数据写入到终极。

完全代码

#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

#define EOF -1
#define BUFSIZ 1024
#define OPEN_MAX 20      //打开的最大文件数
#define PERMS 0666

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

extern FILE _iob[OPEN_MAX];

//八进制
enum _flags {
    _READ = 01,     
    _WRITE = 02,    
    _UNBUF = 04,     //不进行缓冲
    _EOF = 010,     
    _ERR = 020  
};

FILE _iob[OPEN_MAX] = {
        {0,NULL,NULL,_READ,STDIN_FILENO},
        {0,NULL,NULL,_WRITE,STDOUT_FILENO},
        {0,NULL,NULL,_WRITE|_UNBUF,STDERR_FILENO}
};

#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])

int _ferror(FILE *f){
    return f-> flag & _ERR;
}

int _feof(FILE *f){
    return f-> flag & _EOF;
}

int _fileno(FILE *f){
    return f->fd;
}

//返回第一个字符
int _fillbuf(FILE *f){

    int bufsize;

    if((f->flag & (_READ | _EOF | _ERR)) != _READ){     //判断文件是否可读
        return EOF;
    }

    bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;

    if(f->base == NULL){            //没有分配过缓冲区
        if((f->base = (char *)malloc(bufsize)) == NULL){
            return EOF;
        }
    }

    f->ptr = f->base;
    int n = read(f->fd,f->ptr,BUFSIZ);      //系统调用read
    if(n == 0){         //到达文件结尾
        f->base = NULL;
        f->cnt = 0;
        f-> flag |= _EOF;
        return EOF;
    }else if(n == -1){      //出错
        f->cnt= 0;
        f->flag |= _ERR;
        return EOF;
    }else{
        f->cnt = --n;
        return *f->ptr++;   
    }
}

int _flushbuf(int x,FILE *f){

    if((f->flag & (_WRITE | _EOF | _ERR)) != _WRITE){
        return EOF;
    }

    int n;
    int bufsize = f->flag & _UNBUF ? 1 : BUFSIZ;
    if(f->base != NULL){
        n = write(f->fd,f->base,f->ptr - f->base);      //判断需要写入多少字节
        if(n != f->ptr - f->base){
            f->flag |= _ERR;
            return EOF;
        }
    }else{
        if((f->base = (char *)malloc(bufsize)) == NULL){
            f->flag |= _ERR;
            return EOF;
        }
    }

    if(x != EOF){
        f->cnt = bufsize - 1;
        f->ptr = f->base;
        *f->ptr++ = x;
    }else{          //当写入EOF时,代表强制刷新缓冲区内容到文件中
        f->cnt = bufsize;
        f->ptr = f->base;
    }
    return x;
}

/**
 * @brief _fflush
 * @param f
 * @return
 */
int _fflush(FILE *f){

        int res = 0;
    if(f == NULL){
                for(int i = 0; i < OPEN_MAX; i++){          //当参数为NULL时,刷新所有的文件流
                       if((f->flag & _WRITE) && (_fflush(&_iob[i]) == -1)){  //有一个出错即返回-1
                           res = EOF;
                       }
        }
        }else{
            if(f->flag & _WRITE){
                    _flushbuf(EOF,f);
            }else{
                res = EOF;
            }
        }

        if(f->flag & _ERR){     //出错
                res = EOF;
    }
        return res;
}

int _fclose(FILE *f){
    int ret;
    if((ret = _fflush(f)) != EOF){
        free(f->base);
        f->base = NULL;
        f->ptr = NULL;
        f->fd = 0;
        f->flag = 0;     //@TODO
    }

    return 0;
}

int _fseek(FILE *f,long offset,int origin){

    int rc;

    if(f->flag & _READ) {
        if(origin == 1) {
            offset -= f->cnt;
        }
        rc = lseek(f->fd,offset,origin);
                f->cnt = 0;             //将缓冲区剩余字符数清0
    }else if(f->flag & _WRITE) {
                rc = _fflush(f);       //强制刷新缓冲区
        if(rc != EOF) {
            rc = lseek(f->fd,offset,origin);
        }
    }

    return rc == -1 ? EOF : 0;
}


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

int _putc(int x,FILE *f){
    return --f->cnt >= 0 ? *f->ptr++ = x : _flushbuf(x,f);
}

int _getchar(){
  return _getc(stdin);
}

int _putchar(int x){
    return _putc(x,stdout);
}

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;
}

int main(int argc,char *argv[]){


    FILE *f = _fopen("zyc.txt","a");    
    /*char c;
      for(int i = 0; i < 10; i++){
      c = _getc(f);
      }*/

    /*for(int i = 0; i < 9; i++){
      _putc('6',f);
      }

      _fseek(f,-5,1);

      for(int i = 0; i < 9; i++){
      _putc('8',f);
      }

      _fclose(f);*/

        int c;
        while((c = _getchar()) != '\n'){
            _putchar(c);            
        }

        _fclose(stdout);

    return 0;
}

地点提到的局地函数在Answer to Exercise 8-3, page
179
中有更详实的贯彻。

参考资料: