会错意表错情,搭错车上错床——“度日如年”的故事及“feof()”的故事
1. “度日如年”的故事
一个幼儿园小盆友,看到了“度日如年”这个成语,以为是天天过年的意思,于是活学活用、借题发挥:“祝大家在新的一年里天天‘度日如年’”。
这就是会错了意,表错了情。
“度日如年”的故事讲完了,下面是这个故事的C语言版。
2. feof()的故事
/*
例10.2 将一个磁盘文件中的信息复制到另一个磁盘文件中。今要求将上例建立的file1.dat文件中的内容复制到另一个磁盘文件file2.dat中。
*/
#include <stdio.h>
#include <stdlib.h>
int main()
{FILE *in,*out;
char ch,infile[10],outfile[10];
printf("输入读入文件的名字:");
scanf("%s",infile);
printf("输入输出文件的名字:");
scanf("%s",outfile);
if((in=fopen(infile,"r"))==NULL)
{printf("无法打开此文件\n");
exit(0);
}
if((out=fopen(outfile,"w"))==NULL)
{printf("无法打开此文件\n");
exit(0);
}
while(!feof(in))
{ch=fgetc(in);
fputc(ch,out);
putchar(ch);
}
putchar(10);
fclose(in);
fclose(out);
return 0;
}
这段代码毛病很多,这里只谈其核心部分,即while语句部分。
这条语句貌似首先“检查in所指的文件是否结束”,如果“!foef(in)”不为0,则从in流中读一个字符,然后写入out流。看起来很美,并且据说“运行结果是将file1.dat文件中的内容复制到file2.dat中。”
然而,这只不过是一厢情愿的错觉而已。如果仔细检查一下就会发现,文件file2.dat比文件file1.dat长一个字节;如果手头有UltraEdit之类的十六进制编辑器,不难发现多出的字符的值很可能是FFH,即十进制的255。
这个多出来的字符是怎么来的呢?主要原因有两个:对feof()函数的误解和对fgetc()函数的不求甚解:
为了知道对文件的访问是否完成,只须看文件读写位置是否移到文件的末尾。用feof函数可以检查到文件读写位置标记是否移到文件的末尾,即磁盘文件是否结束。feof(in)是检查in所指向的文件是否结束。如果是,则函数值为1(真),否则为0(假)。
————谭浩强 ,《C程序设计》(第四版),清华大学出版社,2010年6月,p340~341
fgetc:
调用形式:fgetc(fp)
功能:从fp指向的文件读入一个字符
返回值:读成功,带回所读的字符,失败则返回文件结束标志EOF(即-1)
————谭浩强 ,《C程序设计》(第四版),清华大学出版社,2010年6月,p338
在这种错误认识的指导下,由于会错了意,难免会表错情。
3. feof()函数及fgetc()函数的真正含义
首先,feof()函数并非“检查”“文件读写位置标记是否移到文件的末尾”,feof()函数检查的是流的end-of-file标记。end-of-file标记和读写位置标记虽然同属于FILE类型结构体记录的内容,但它们根本就是两回事。如果feof()函数检测到了end-of-file标记,返回一个int类型的非零值(不一定时1),否则返回int类型的0。
那么,end-of-file标记是记录流控制数据的FILE类型结构体对象中固有的吗?也不是,这个end-of-file标记是由fgetc()这样的函数所设置的。当fgetc()函数发现输入流中不存在数据后,除了返回一个EOF,还会设置FILE对象中的end-of-file标记,在很多实现中这个标记用一个“位”表示。
由此可见,即使流中没有数据的情况下,feof()函数也不一定返回非零值。只有在流中没有数据并且fgetc()之类的函数继续读取失败之后,fgetc()函数才能检查到流的end-of-file标记。
也就是说,feof()函数并不能告诉你流是否已经到了结尾,它所能告诉你的只不过是,而且仅仅是,前一次读取失败的原因是否因为到了流的结尾(读取失败的另一个原因是发生了错误)。
为了说明这一点,下面进行一项测试。
首先,在D:盘的根目录下建立一个文本文件ABC.TXT,并在其中写入ABC三个字符。
然后,运行下面程序:
#include <stdio.h> #include <stdlib.h> int main( void ) { FILE *abc; if((abc = fopen("D:\\ABC.TXT","rb")) == NULL ) { printf("打开文件失败\n"); return !0; } for(int i = 0 ; i < 5 ; i++ ) { int ch; int eof_before_read,eof_after_read; eof_before_read = feof(abc) ; ch = fgetc( abc ); eof_after_read = feof(abc) ; printf( "读入%c(%d)前后feof()的值分别为:%d,%d\n", ch,ch,eof_before_read,eof_after_read ); } fclose(abc); return 0; }
这段程序的运行结果是:
读入A(65)前后feof()的值分别为:0,0
读入B(66)前后feof()的值分别为:0,0
读入C(67)前后feof()的值分别为:0,0
读入(-1)前后feof()的值分别为:0,16
读入(-1)前后feof()的值分别为:16,16
由此不难看出,在读入C之后(已经到了流的结尾),feof()函数的返回值依然是0,只是再次试图读取字符之后,feof()的返回值才成了16。这是由于fgetc()函数发现已经没有字符可读,在对应的FILE结构体数据中设置了end-of-file标记的缘故。
4. feof()函数的真正用途
feof()函数只能事后诸葛亮地告诉我们读入是如何结束的,它根本不能用于拷贝的循环控制。把feof()函数用于拷贝的循环控制,不但是会错意表错情,而且简直是搭错了车上错了床。
feof()函数的正确用法之一是:
#include <stdio.h> #include <stdlib.h> void file_copy(FILE * , FILE * ); int main( void ) { FILE *abc; FILE *abc_b; if((abc = fopen("D:\\ABC.TXT","rb")) == NULL ) { printf("打开文件失败\n"); return EXIT_FAILURE; } if((abc_b = fopen("D:\\ABC_B.TXT","wb")) == NULL ) { printf("打开文件失败\n"); fclose(abc); return EXIT_FAILURE; } file_copy( abc_b , abc ); if( feof(abc) != 0 ) { printf("拷贝正常结束\n"); fclose(abc); fclose(abc_b); return EXIT_SUCCESS; } if( ferror (abc) != 0 ) { printf("拷贝过程中发生错误,目标文件可能并不正确\n"); fclose(abc); fclose(abc_b); return EXIT_FAILURE; } } void file_copy( FILE * t, FILE *s ) { int ch; while( (ch = fgetc(s) ) != EOF ) fputc( ch , t ); }