You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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.
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.
写一下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)
:结构体中有flag字段,flag字段可以是以下几种的并集:
我们注意到其中有一个字段,标识不进行缓冲,说明此种情况下每一次读取和输出都调用系统函数。一个例子就是标准错误流stderr : 当stderr连接的是终端设备时,写入一个字符就立即在终端设备显示。
而stdin和stdout都是带缓冲的,明确的说是行缓冲。本文不考虑行缓冲,默认都是全缓冲,即缓冲区满了才刷新缓冲区。(详细可以参考《UNIX环境高级编程》标准I/O库章节)。
现在我们可以初始化stdin、stdout与stderr:
_ferror/_feof/_fileno
_fopen
fopen的处理过程:
_getc
getc的作用是从文件中返回下一个字符,参数是文件指针,即FILE:
对照上面的图示:当缓冲区中还有剩余字符待读取时,读取该字符并返回,并将缓冲区指针向后移动一个char单位,否则就调用_fillbuf函数填满缓冲区,_fillbuf的返回值就是待读取的字符。
这里有一个问题:当读取到最后一个字符时,cnt为0,但是ptr已经越界了,如下图:
![image](https://user-images.githubusercontent.com/14103319/27510029-88ecfda0-593b-11e7-9186-76a742dcedae.png)
这种情况虽然是非法的,但是C语言中保证:数组末尾之后的第一个元素(即&arr[n],或者arr + n)的指针算术运算可以正确执行。下面的例子也是上述的一种应用场景:
当for循环到最后一步时,x也指向了a[10],虽然是非法的,但是C语言保证可以正确执行,只要不出现如下情况就ok:
_fillbuf
我们看下_getc中的_fillbuf的实现,_fillbuf是当缓冲区没有可以读取的字符时,通过系统调用read读取一定字节的数据填满缓冲区,供之后使用:
_fillbuf的处理过程:
这里注意,到达文件结尾和出错都是返回EOF,区别是前者会将flag的_EOF位置1,后者会将flag的_ERR位置1,上游可以通过
feof
和ferror
函数进行判断(这两个函数在上面已经实现过了)。_putc
与_getc的实现相似,将写入的字符放到ptr指向的位置,并将ptr向后移动一位。当缓冲区满时,调用_flushbuf将缓冲区内容刷新到文件中。
_flushbuf
_flushbuf的处理过程:
注意,调用write函数时,写入的字节数不能写死为1或者BUFSIZ:
如上图,我们需要写入base至ptr之间的数据,而不是BUFSIZ,因为我们可能会强制刷新缓冲区而不是等到缓冲区满了才刷新缓冲区。
还有一点:当我们想要强制刷新缓冲区时,第一个参数x该传入什么呢?K&R传递的是字符0,但是我认为这样会污染缓冲区,所以我的实现是传入一个特殊字符EOF,根据EOF来做不同的处理:
当缓冲区满时,刷新缓冲区后缓冲区的表现:
![image](https://user-images.githubusercontent.com/14103319/27513662-ba7c3e60-59a1-11e7-878a-7d117716082b.png)
![image](https://user-images.githubusercontent.com/14103319/27513665-e9aad67e-59a1-11e7-8cf0-5b54021a73b7.png)
![image](https://user-images.githubusercontent.com/14103319/27513672-232bf608-59a2-11e7-89fc-d3105d21b55c.png)
当强制刷新缓冲区时,缓冲区的表现:
但是按照K&R的方式来强制刷新缓冲区时,缓冲区的表现:
这样会污染缓冲区,所以我的实现是传入EOF来强制刷新缓冲区。
_fflush
_fflush(FILE *f)的作用是把缓冲区内容写入文件。当参数为空时,会刷新所有文件:
_fflush的处理过程:
注意,这里我们只针对可写的文件流进行操作,忽略了只读的文件流:
针对只读的文件流,不同系统处理的方式不一样,有的系统会清空缓冲区。
_fclose
fclose调用fflush函数,保证在文件关闭前将缓冲区中的内容刷到文件中,并且释放掉缓冲区的内存空间。
_fseek
关于fseek的介绍请看fseek
当文件流为可读时,见下图:
![image](https://user-images.githubusercontent.com/14103319/27516783-32a99bca-59f3-11e7-8193-2255d409d5b7.png)
由于有缓冲区的存在,我们直觉上的文件指针位置和真实的文件指针位置是不同的,差了cnt个单位长度。所以当我们设置移动offset个长度时,真实的文件指针需要移动offset-cnt个单位长度(offset为正数或者负数)。
之后我们需要将cnt置为0,以便下次读取时将缓冲区的数据更新。
当origin为0或者2时,直接调动lseek即可。
而当文件流为可写时,见下图:
![image](https://user-images.githubusercontent.com/14103319/27516805-8c4e628c-59f3-11e7-9411-e4e83061f19d.png)
真实的文件指针位置与我们直觉上的文件指针位置差了ptr - base个单位长度,即我们新写入缓冲区的内容长度,所以我们直接调用_fflush即可。(K&R中直接调用的write,但是我觉得这样没有重置ptr指针的位置和cnt,这样的话base与ptr之间的内容会被刷入到文件中两次)。
当文件是以a模式打开时,fseek无效:
_getchar
我们可以发现,_getchar调用的就是_getc,只不过_getc可以传入任意的文件指针,而对_getchar来说,_getc传入的是stdin,也就是
{0,NULL,NULL,_READ,0}
。int n = read(f->fd,f->ptr,BUFSIZ); //系统调用read
处阻塞住,等待用户输入字符。_putchar
我们可以发现,_putchar调用的就是_putc,只不过_putc可以传入任意的文件指针,而对_putchar来说,_putc传入的是stdout,也就是
{0,NULL,NULL,_WRITE,1}
。完整代码
上面提到的部分函数在Answer to Exercise 8-3, page 179中有更详细的实现。
参考资料:
The text was updated successfully, but these errors were encountered: