写一下fopen/getc/putc等C库的粗略实现,参考了K&R,但是有几点根据自己理解的小改动,下面再具体说一下^_^
写这篇文章主要是帮助自己理解下标准I/O库大体是怎么工作的。
fopen与open之间的关系
操作系统提供的接口即为系统调用。而C语言为了让用户更加方便的编程,自己封装了一些函数,组成了C库。而且不同的操作系统对同一个功能提供的系统调用可能不同,在不同的操作系统上C库对用户屏蔽了这些不同,所谓一次编译处处运行。这里open为系统调用,fopen为C库提供的调用。
C库对的读写操作封装了一个缓冲区。试想假如用户频繁的对文件读写少量字符,会频繁的进行系统调用(read函数),而系统调用比较耗时。C库自己封装了一个缓冲区,每次读取特定数据量到缓冲区,读取时优先从缓冲区读取,当缓冲区内容被读光后才进行系统调用将缓冲区再次填满。
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已经越界了,如下图:
本文来自网络收集,不代表计算机技术网立场,如涉及侵权请联系管理员删除。
ctvol管理联系方式QQ:251552304
本文章地址:https://www.ctvol.com/c-cdevelopment/482046.html