在读文件的时候,很重要的一个条件是:文件是否结束,因为文件已经结束了,就不用继续读文件了。
判断文件结束,有许多方式,比如读取文本文件和二进制文件就可以使用不同的方式,下面分别进行详细介绍和举例。
EOF的值为-1,所以往往使用EOF也可以判断文件是否结束,一般用在非格式化文本文件读取中,如果在格式化文本读取时使用EOF来判断,在某些情况下是会出错的。
函数fgetc返回的值为一个字符,当文件结束时,返回EOF,因为文本文件中可打印字符没有字符的值是-1,所以,可以用EOF来判断文件是否结束了。
这也是唯一的一个可以使用EOF来判断文件是否结束,而且永远正确的函数,前提是必须是读文本文件(格式化或者非格式化都可以支持)。
我们来看一下使用fgetc和EOF来检测文件是否结束的例子,代码如下:
void EOF_test_getc(const char* file_name) { int ch = 0; int count = 0; FILE *file = fopen(file_name,"r"); if(!file) return; while(1) { ch = fgetc(file); if(ch == EOF) { printf("reach the end of file,the char number is %d\n",count); break; } else { count++; putchar(ch); } } fclose(file); }在代码中,我们使用fgetc来读取文件中的每一个字符,如果读取到的字符是EOF,则结束读取,每读取一个字符,就对count++,以统计文件中字符的个数。运行效果如图所示。
但是出现了一个奇怪的问题,程序中统计出来的字符是98个,但是文件大小却是102个字节,相差了4个字符,那4个字符到哪里去了?
在《C语言文件读写(1)-文本文件读操作》
中提到,在Windows上,如果写入行结束符'\n',系统会自动替换为'\r\n',在读取的时候,会自动把'\r\n'转换为'\n',因为我们这个文件有4行,所以文件中就多了4个'\r',这就是为什么文件的实际大小会比读取出来的字符多了4个的原因。
注意,这种判断文件结束的方式只能针对文本文件,不能用在二进制文件上面,因为二进制文件的内容什么值都可以存储,-1也会是其中的一个值。
fscanf和fscanf_s有时候也可以用EOF来判断文件是否结束,而且在前面的几篇文章中也确实使用了这种方式来判断,但是,如果文本文件中的内容一旦有一点错,scanf中的format字符串不能匹配,则永远都不会返回EOF这个值,就会造成死循环,所以在使用fscanf或者fscanf_s的时候,不推荐使用返回值EOF来判断文件是否结尾,而是使用feof函数来检测,后面会介绍。
函数fgets用来获取一行的文本文件数据,如果返回为NULL,则表示文件结束了或者读取错误,所以有时候也可以使用返回值是否为NULL来判断文件是否结束,示例代码如下:
void read_text_by_gets(const char* file_name) { char buffer[128]={0}; FILE *file = fopen(file_name,"rt"); if(!file) return; while(NULL != (fgets(buffer,sizeof(buffer),file))) { //显示每一次读取到的内容 printf("%s",buffer); } fclose(file); }这在绝大多数的时候都可以判断文件是否真的结束了,但是,如果读取文件出错了,也会返回NULL,这就不能区分是文件真的结束了,还是由于别的原因读取错误,所以,并不推荐用这种方式来检测文件是否结束。
我们在使用fread来读取二进制文件的时候,往往可以通过返回值是否等于count的值来判断文件是否结束,大多数情况下是没有问题的,示例代码如下:
void read_binary_file(const char* file_name) { struct Student stu={0}; FILE *file = fopen(file_name,"rb"); if(!file) return; while(fread(&stu,sizeof(stu),1,file) == 1) { printf("学号:%d 姓名:%s 学院:%s 分数:%.2f\n",stu.ID,stu.Name,stu.College,stu.Score); } fclose(file); }在示例代码中,fread因为我们每次只读一个Student的大小,所以如果返回的值小于1的话,则说明文件结束了。
同样的原因,fread如果出错了的话,返回值也是可能小于1的,因此这个判断也不是100%准确的。
feof的原型为:
int feof( FILE *stream );
返回值是一个整数,如果为0,表示文件没有结束,如果非0,表示文件结束。
但是要特别注意一点的是,这个函数的实现机制是这样的:
是在下一次读取的时候判断是否到了文件末尾,如果是,则设置文件结束标志,它并不是在调用feof这个函数的时候去检查数据来判断是否到了文件的末尾。所以这个函数的使用,很多朋友都会用错,以为这是一个真正检查文件是否结束的函数,当然,它确实是,但是它不是即时的。
我们先看一下文本文件的例子,仍然以刚才的student.txt为例,代码如下:
void feof_test_getc(const char* file_name) { int ch = 0; int count = 0; FILE *file = fopen(file_name,"r"); if(!file) return; while(!feof(file)) { count++; ch = fgetc(file); putchar(ch); } printf("reach the end of file,the count is %d\n",count); fclose(file); } int main(int argc, char* argv[]) { feof_test_getc("student.txt"); return 0; }这个代码几乎是我遇到的所有初学C语言甚至是学了C语言很长时间的朋友的写法,确实,看起来没有任何问题,我们来看一下运行结果,如图所示。
请注意,count的值是99,而且最后一行还多输出一个字符,这就是为什么在论坛上会经常看到为什么会多出一行,多出一个之类的讨论。
这确实就是缺乏对feof的理解造成的,我们以为feof就是一个实时监测文件是否结尾的函数,其实并不是,当你使用fgetc或者任何别的函数读取文件内容的时候,如果读取的内容刚好是文件的最后一个字符,这个时候调用feof的话,返回仍然是0,因为它并不会去真的检查文件是否结尾,它要依赖下一次的读取操作,下一次再次读取的时候,发现文件已经到了结尾,则设置文件结束标志,feof调用的时候才会返回非0值。
现在来演示一下一个代码,可以更好地理解feof的工作机制,代码如下:
void feof_test() { int test = 0; char ch = 't'; char str[20]="test"; FILE *file1,*file2,*file3; //第一种情况 file1=fopen("test1.dat","w+"); fprintf(file1,"%c",ch); rewind(file1); fscanf(file1,"%c",&ch); if(feof(file1)) { printf("文件1结束\n"); } else { printf("文件1没有结束\n"); } fclose(file1); //第二种情况 file2=fopen("test2.dat","w+"); fprintf(file2,"%d",10); rewind(file2); fscanf(file2,"%d",&test); if(feof(file2)) { printf("文件2结束\n"); } else { printf("文件2没有结束\n"); } fclose(file2); //第三种情况 file3=fopen("test3.dat","w+"); fprintf(file3,"%s",str); rewind(file3); fscanf(file3,"%s",str); if(feof(file3)) { printf("文件3结束\n"); } else { printf("文件3没有结束\n"); } fclose(file3); }在这个测试中,写入三个文件,然后分别打开对应的文件从头开始读取,然后判断feof是否返回非0值,其中第一种情况,返回0,第二种和第三种返回非0。
先看一下运行结果,再来解释为什么。结果如图所示。
为什么会产生这个差别呢?先说第一种情况,写入了一个字符,然后读取一个字符,虽然文件中只有一个字符,但是读取一个字符就够了,所以并不知道文件是否结束。
第二种情况,写入数字10到文件,然后读取一个整数,为什么就检查到文件结束了呢?因为读取一个整数的时候,会一直读,直到读取到空格或者别的结束符或者超过整数范围的时候才会结束,因为文件中存的是10,所以会一直读到文件结束,10是没有超过整数范围的。
第三种情况和第二种情况一样,读取一个字符串的时候也要一直读,直到遇到空格或者tab之类的,由于文件中只有test这四个字符,自然就会读到文件尾了。
所以第二和第三种情况都会返回非0值,表示文件结束了。