会错意表错情,搭错车上错床——“度日如年”的故事及“feof()”的故事

1. “度日如年”的故事

  一个幼儿园小盆友,看到了“度日如年”这个成语,以为是天天过年的意思,于是活学活用、借题发挥:“祝大家在新的一年里天天‘度日如年’”。
这就是会错了意,表错了情。
  “度日如年”的故事讲完了,下面是这个故事的C语言版。

2. feof()的故事

View Code
/*
例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 );

}

 

 

posted @ 2012-01-27 14:09 garbageMan 阅读(1474) 评论(16) 编辑 收藏

 回复 引用 查看   
#1楼 2012-01-27 14:41 AD8018      
真不幸,楼主最后一段自认为正确的代码,
AD找到了两处瑕疵 --- 一个bug,一个不算bug的小问题。

具体是什么,俺也不说了,读者找找吧。

 回复 引用 查看   
#2楼 2012-01-27 15:53 xujif      
引用 if((abc = fopen("D:\\ABC.TXT","rb")) == NULL )
{
printf("打开文件失败\n");
return !0;
}

if((abc_b = fopen("D:\\ABC_B.TXT","wb")) == NULL )
{
printf("打开文件失败\n");
return !0;
}
...while( (ch = fgetc(s) ) != EOF )
fputc( ch , t );

这才是天大的搭错车,用fgetc操作二进制文件,难道lz不知道二进制文件里可能有0x1a存在吗?

刚才去测试了下lz的代码
不过很巧妙的是,这个代码没有bug,能正常复制
因为不知lz是不是刻意把 ch用int来申明,如果用char来申明ch,复制到0x1a马上就会结束循环,导致后面的无法复制。
原因就是 EOF是0xff,char的-1,用int比较就是0xffffffff
自然不会相等。

 回复 引用 查看   
#3楼 2012-01-27 16:03 AD8018      
楼上,二进制没问题的
 回复 引用 查看   
#4楼 2012-01-27 16:31 xujif      
引用AD8018:楼上,二进制没问题的

lz的代码的确“没问题”
但是把 int ch; 换成 char ch 就有大问题了。
随便构造一个包含0x1a的二进制文件就会发现

 回复 引用 查看   
#5楼 2012-01-27 17:45 诺贝尔      
c的流操作函数确实很容易撞板。
 回复 引用 查看   
#6楼[楼主] 2012-01-27 17:51 garbageMan      
引用xujif:
这才是天大的搭错车,用fgetc操作二进制文件,难道lz不知道二进制文件里可能有0x1a存在吗?

刚才去测试了下lz的代码
不过很巧妙的是,这个代码没有bug,能正常复制
因为不知lz是不是刻意把 ch用int来申明,如果用char来申明ch,复制到0x1a马上就会结束循环,导致后面的无法复制。
原因就是 EOF是0xff,char的-1,用int比较就是0xffffffff
自然不会相等。

你的意思是fgetc()不能操作二进制文件?
我不记得有这种禁忌
你能说一下你的依据吗

我之所以用"rb"就是考虑到文本文件后面可能有一个讨厌的0x1a
用文本方式打开,这个地方需要特殊处理一下

我的确是刻意用int的
在有些系统上用char会出毛病,
不知道你注意到没有,fgetc()返回值的类型就是int

EOF是0xff 这个说法是错误的

 回复 引用 查看   
#7楼 2012-01-27 18:01 xujif      
@garbageMan
fput* fget*包括fprintf之类都应该是处理 文本文件的
而处理二进制文件的输入输出只有 fread和fwrite或者write read之类的块读写函数。
因为文本文件是靠 0x1a来判断末尾的,而二进制里包含0x1a的概率并不是很低

你可以试试用fwrite把char a[]={'a','b',0x1a,'A','B'}写入到文件,用fgetc 来看看它能读出几个。
当然,fgetc请赋值到int。

至于fgetc返回int我就不知道为什么了,不过,从这个函数看起来,你难道不认为是返回的应该是char吗。

所以,要复制文件,还是老老实实用fread fwrite吧。

 回复 引用 查看   
#8楼[楼主] 2012-01-27 18:02 garbageMan      
引用xujif:
引用AD8018:楼上,二进制没问题的

lz的代码的确“没问题”
但是把 int ch; 换成 char ch 就有大问题了。
随便构造一个包含0x1a的二进制文件就会发现


你有没有想过
引用把 int ch; 换成 char ch

这本身就是错的?

 回复 引用 查看   
#9楼 2012-01-27 18:17 xujif      
@garbageMan
还真是,用char作为fgetc的返回值的确是错的,这倒是以前没注意的地方。

 回复 引用 查看   
#10楼[楼主] 2012-01-27 19:41 garbageMan      
引用xujif:
@garbageMan
fput* fget*包括fprintf之类都应该是处理 文本文件的
而处理二进制文件的输入输出只有 fread和fwrite或者write read之类的块读写函数。
因为文本文件是靠 0x1a来判断末尾的,而二进制里包含0x1a的概率并不是很低

你可以试试用fwrite把char a[]={'a','b',0x1a,'A','B'}写入到文件,用fgetc 来看看它能读出几个。
当然,fgetc请赋值到int。

至于fgetc返回int我就不知道为什么了,不过,从这个函数看起来,你难道不认为是返回的应该是char吗。

所以,要复制文件,还是老...

“文本文件是靠 0x1a来判断末尾”,这种说法有些过时
现代的文本文件几乎都不用这个了,而是通过长度判断
之所以保留0x1a判断结尾是为了保持与DOS的兼容
所以你说的问题,我认为若是把二进制文件当作文本文件时存在
但把文本作为二进制打开应该是一点问题都没有的
我记得在UNIX中压根就没有文本文件这种概念
都一律作为二进制流处理

fgetc返回int的原因是它需要返回EOF,这个值可能并非是char类型所能表示的,因此接受这个返回值的也应该是int。
K&R 的1.5.1提到过这个问题

 回复 引用 查看   
#11楼[楼主] 2012-01-27 19:49 garbageMan      
@xujif
引用你可以试试用fwrite把char a[]={'a','b',0x1a,'A','B'}写入到文件,用fgetc 来看看它能读出几个。
当然,fgetc请赋值到int。


我试了
没有任何问题
读出了5个
a 97
b 98
 26
A 65
B 66
(分别用wb和rb打开)

 回复 引用 查看   
#12楼[楼主] 2012-01-27 19:54 garbageMan      
引用诺贝尔:c的流操作函数确实很容易撞板。

IO设备种类繁多,而且有时还涉及到OS
所以IO有时显得比较复杂
不过C的抽象工作还是不错的
只是有时不容易理解

 回复 引用 查看   
#13楼 2012-01-27 20:01 xujif      
@garbageMan
我说错了 赋值到char 就只能读出3个了

 回复 引用 查看   
#14楼 2012-01-28 14:34 Leon Sharp      
楼上各位说代码有问题的童鞋请看一下fgetc的定义,ch应该定义为int型。楼主没错。
 回复 引用 查看   
#15楼 2012-02-09 11:48 AD8018      
1楼提到的一个问题和一个bug,答案已经公布,在此
http://bbs.chinaunix.net/thread-3669127-1-1.html

 回复 引用 查看   
#16楼 2012-02-09 12:59 AD8018      
靠,专家有本身不要改啊。
看到我指出问题,一声不吭的将

if((abc_b = fopen("D:\\ABC_B.TXT","wb")) == NULL )
{
printf("打开文件失败\n");
return EXIT_FAILURE;
}

改成

if((abc_b = fopen("D:\\ABC_B.TXT","wb")) == NULL )
{
printf("打开文件失败\n");
fclose(abc);
return EXIT_FAILURE;
}