FILE类型
在C语言中,有三个预定义的文件流:stdin,stdout,stderr,在VC++6的stdio.h中,它们的定义如下:
#define stdin (&_iob[0])
#define stdout (&_iob[1])
#define stderr (&_iob[2])
分别取自_iob数组下标为0、1、2的元素地址。
_iob的定义是一个FILE类型的数组:
/*
* FILE descriptors; preset for stdin/out/err (note that the __tmpnum field
* is not initialized)
*/
FILE _iob[_IOB_ENTRIES] = {
/* _ptr, _cnt, _base, _flag, _file, _charbuf, _bufsiz */
/* stdin (_iob[0]) */
{ _bufin, 0, _bufin, _IOREAD | _IOYOURBUF, 0, 0, _INTERNAL_BUFSIZ },
/* stdout (_iob[1]) */
{ NULL, 0, NULL, _IOWRT, 1, 0, 0 },
/* stderr (_iob[3]) */
{ NULL, 0, NULL, _IOWRT, 2, 0, 0 },
};
FILE本质上是结构体_iobuf的类型别名:
struct _iobuf {
char *_ptr; // 文件指针
int _cnt; // 缓冲区剩余大小
char *_base; // 缓冲区起始位置
int _flag; // 标志/状态
int _file; // 文件唯一标识(_iob数组下标)
int _charbuf;
int _bufsiz; // 缓冲区大小
char *_tmpfname;
};
typedef struct _iobuf FILE;
结构体_iobuf中的_ptr,_base,_cnt,_bufsiz是与文件缓冲区相关的字段。
在使用fopen函数打开一个文件后,就会从_iob数组中选择一个空闲的元素分配给新的FILE。
源码调试
先准备一段C代码
#include <stdio.h>
int main(int argc, char *argv[], char *env[])
{
FILE *fp = NULL;
fp = fopen("c:/test.txt", "r+");
if (fp == NULL)
{
fp = fopen("c:/test.txt", "w+");
if (fp == NULL)
{
puts("Open file error");
goto EXIT_PROC;
}
}
fprintf(fp, "hello: %s\n", argv[0]);
fflush(fp);
EXIT_PROC:
if (fp != NULL)
{
fclose(fp);
fp = NULL;
}
return 0;
}
将断点打在fopen处:
逐层步入:
一直到这里调用了系统函数CreateFile:
待fopen返回之后,fp便分配了值,而该值恰好是_iob数组下标为3的元素地址,0、1、2已经被预先占用了,所以新打开的文件从3开始。
在监视窗口中展开fp的内容可见,缓冲区相关的字段都为0,说明文件刚打开时,缓冲区尚未分配。
_flag = 0x80,转换为二进制是10000000,其中一个bit位置1,表示_iob数组中该元素已被占用。
_file = 3,代表文件标识,也是它在_iob数组中的下标。
文件缓冲区只有在文件真正被读写时才会分配,比如执行完fprintf后:
_base = 0x00382a80,代表文件缓冲区的起始地址;
_ptr是文件指针,指示当前写入到缓冲区的哪个位置;
_bufsiz = 0x1000,说明缓冲区大小是4096字节;
_cnt = 0x0fb1,说明缓冲区还剩余4017字节,也就是说刚刚写入了4096-4017=79字节数据;
_flag=0x8a,二进制为10001010,新增了两个置1的bit位。
在内存视图中,跳转到_base指向的内存区域,发现下面有大量0xCDCD填充的字节,很明显这是一块堆内存(可以参考我的另一篇文章:[原创]VC++6调试状态下的堆结构),堆块起始地址在向前偏移32字节处也就是0x00382a60,根据堆块的附加信息可知:blockType = 2也就是_CRT_BLOCK,是C运行时库所使用的类型。
注意,此时仅仅是将数据写入了内存缓冲区,实际磁盘文件中并没有内容:
如果想要将缓冲区的内容写入到磁盘,可以等待缓冲区满后自动触发存盘,也可以手动调用fflush手动同步,步入到fflush函数内部:
这里调用了WriteFile系统函数:
WriteFile执行后,磁盘中的文件便有了内容:
fflush函数返回后,缓冲区相关字段也发生了变化:
_ptr回到与_base相同的位置,_cnt置为0,此时在逻辑上表示了缓冲区中无内容的状态,但堆上的残留值并未清空;
_flag = 0x88,二进制为10001000,减少了一个置1的bit位。
接下来进入fclose函数内部:
从注释中可以得知,fclose做了4件事:
刷新文件流缓存
释放缓冲区
关闭文件
删除临时文件
这里调用_flush和fflush函数内部一样,会将缓冲区中的剩余数据写入磁盘,接下来调用_freebuf:
_freebuf执行完后,发现缓冲区已经变为0xFEEE填充的字节,说明堆块已经被释放;
_ptr 和 _base 都置为0;
_flag = 0x80,二进制为10000000,又减少了一个置1的bit位;
接下来步入_close函数,这里调用了系统函数CloseHandle用于关闭文件,fh的值为3,也就是文件所在_iob数组的下标。
在CloseHandle执行前,试图删除文件会失败,因为此时文件尚未关闭:
待CloseHandle执行过后,文件已关闭,即可正常删除:
在fclose函数末尾,有一个将_flag清0的操作,表示_iob数组中该元素已被释放,下次再打开新文件可以使用该元素了。
总结
打开文件相当于在_iob数组中占用了一个元素;
标准库函数fopen调用了系统函数CreateFile;
文件读写不会直接操作磁盘,而是操作缓冲区;
fflush会调用WriteFile将缓冲区中的内容写入磁盘,同时将缓冲区逻辑上清空;
标准库函数fclose调用了系统函数CloseHandle。