关于EOF(文件结束符)问题的体会

来源:互联网 发布:cytus 剧情 知乎 编辑:程序博客网 时间:2024/05/17 23:40

最近写了些代码,在对文件的操作中发现了很经典的EOF问题,呵呵。

EOF,即end of file,文件结尾,作为文件结束的标志,在程序中常作为判断的一个标志。但在我们平常的程序中却常发生意想不到的结果。

下面这段程序,猜猜它输出的是什么?

char c;

ifstream fin("d://dat");//设d:/dat文件已存在,内容为ab。

while(!fin.eof())

{

    fin >> c;

    cout << c;

}

输出结果是abb,没想到吗?你可能会问,再输出第一个b的时候,文件指针已经指向了EOF,为何不结束?

问题的关键是文件EOF机制是怎样运作的。

我们来谈三个问题:

1、文件指针

当打开一个文件时,文件指针位置为0,并不是指向第一个字符,即第一个字符的位置为1。这一点我们可以通过peek()函数验证。peek()返回的是当前文件指针下一个位置的字符。所以有:

ofstream fo("d://dat");

fo << 'h';

fo.close();

ifstream fi("d://dat");

char temp = fi.peek();

cout << temp;

会显示h。

还有,用fo.seekp(0,ios::beg),得文件指针为0;fo.seekp(0,ios::end),得文件指针指向最后一个字符。

2、关于EOF

很多朋友认为文件尾有EOF,这是错误的。EOF是流的状态标志。在 C++中,是在读取文件失败时才产生EOF。所以第一个程序中,在输出第一个b时,产生了EOF,再输出第二个b时读取到EOF,循环结束。

3、解决EOF困惑的办法

我感觉在判断文件结束上,最好的方法就是判断文件指针相对于开头的位置,是否等于文件长度。即:

long filelen;

ifstream fin("d://dat");//设d:/dat文件已存在,内容为ab。

fin.seekg(0,ios::end);

filelen = fin.tellg();//获取文件长度

fin.seekg(0,ios::beg);

while (1)

{

    if (filelen == fin.tellg())//到达文件尾,即指向EOF

    {

        flag = true;

        break;

    }

    读取数据...

}


当然还有别的方法,就是用peek()的预读性。

peek()返回当前文件指针下一个位置的字符,而指针位置不变。所以我们可以这样:

while (fi.peek()!=EOF)

{

    ...

}

当while循环体中,文件指针指向最后一个字符,若没有fi.peek()!=EOF,则需要再下一个循环中才能触发EOF。而加了fi.peek()!=EOF后,用预读的方法检测出了EOF。呵呵,这个方法挺好的吧!

 

在程序调试过程中发现,有一个程序一直死循环在文件的读取中。http://tuhao.blogbus.com/logs/21306687.html从这边文章了解到了相关信息

主要在于eof()不只是读取文件的最后一个字符,它会接着往后读取,如果此函数返回ture时,则此时返回的文件结束符0xFF才是真正的结尾。

具体内容如下:

在使用C/C++读文件的时候,一定都使用过eof()这个函数来判断文件是否为空或者是否读到文件结尾了,也会在使用这个函数的过程中遇到一些问题,如不能准确的判断是否为空或者是否到了文件尾,以至于有些人可能还会怀疑这个函数是不是本身在设计上就有问题。

先来看看如下这段代码:

#include<iostream>

#include<fstream>

using namespace std;

intmain()

{

charch='x';

ifstream fin("test.txt" /*, ios::binary*/);

if (fin.eof())

{

cout<<"file is empty."<<endl;

return 0;

}


while (!fin.eof())

{

fin.get(ch);

cout<<ch;

}

system("pause");

return 0;

}

编译并运行以上代码,

如果test.txt不存在,程序会形成死循环,fin.eof()永远返回false,

如果test.txt为空,程序打印出一个x字符,

当test.txt中存在一字符串“abcd”且没有换行时,程序打印出“abcdd”,

当存在以上字符串并且有一新的空行时,程序打印出“abcd”加上一空行。


这种现象可能让很多人很迷惑,程序运行的结果似乎很不稳定,时对时错。使用binary模式读时结果一样。在这里,大家可能有一个误区,认为eof()返回true时是读到文件的最后一个字符,其实不然,eof()返回true时是读到文件结束符0xFF,而文件结束符是最后一个字符的下一个字符。如下图所示:

因此,当读到最后一个字符时,程序会多读一次(编译器会让指针停留在最后一个字符那里,然后重复读取一次,这也就是就上面最后一个字符会输出两次的原因。至于是不是所有的编译器都这样处理我就不太清楚了,我使用的VC6,VC8似乎都是这样的)

问题出来了,就要找出对应的解决之道,要解决以上的问题,只需要调整一下条件语句即可:

fin.peek() == EOF或fin.get(ch)

再来看一下另外一种情况:

#include <iostream>

#include <fstream>

#include <string>

using namespace std;

int main()

{

string str;

ifstream fin("test.txt"/*, ios::binary*/);

if (fin.peek() == EOF)

{

cout << "file is empty."<<endl;

return 0;

}

while (!fin.eof())

{

fin >> str;

cout << str;

}

system("pause");

return 0;

}

上述代码在VC8下编译运行,发现,当文件结尾没有空行时,结果正确,当结尾有空行时,最后一个字符串将被重复输出一次, 而VC6的情况则有所不同,没有重复输出,但输出了一个空行。

因此,为了保证在不同的编译器下得到一致的我们期望的结果,将条件语句做一下修改:

fin >> str

综上所述,我们可以得到以下结论:

1. 判断文件是否为空时使用peek函数,若peek返回EOF则文件为空;

2. 读取文件过程中,读取非char型时,使用peek判断文件尾将不再适用,循环判断条件应改用>>操作符进行读取,若读入char型缓冲区,peek函数会表现得很好。

以下是其它的一些相关资料,先记着。。

(1) 字节的读取


在正常的情况下, getc 以 unsigned char 的方式读取文件流, 扩张为一个整数,并返

回. 换言之, getc 从文件流中取一个字节, 并加上24个零,成为一个小于256的整数,

然后返回.


int c;

while ((c = fgetc (rfp))!= -1) // -1就是 EOF

fputc (c, wfp);


上面 fputc 中的 c 虽然是整数, 但在 fputc 将其写入文件流之前, 又把整数的高24位

去掉了, 因此 fgetc, putc 配合能够实现文件复制. 到目前为止, 把 c 定义为

char仍然是可行的, 但下面我们将看到,把 c 定义为 int 是为正确判段文件是否结束.


(2) 判断文件结束.


多数人认为文件中有一个EOF,用于表示文件的结尾. 但这个观点实际上是错误的,在文

件所包含的数据中,并没有什么文件结束符. 对getc 而言, 如果不能从文件中读取,

则返回一个整数 -1,这就是所谓的EOF. 返回 EOF 无非是出现了两种情况,一是文件已

经读完; 二是文件读取出错,反正是读不下去了.


请注意: 在正常读取的情况下, 返回的整数均小于256, 即0x0~0xFF. 而读不出返回的

是 0xFFFFFFFF. 但, 假如你用fputc把 0xFFFFFFFF 往文件里头写, 高24位被屏蔽,写入的将

是 0xFF. // lixforalpha 请注意这一点


(3) 0xFF 会使我们混淆吗?


不会, 前提是, 接收返回值的 c 要按原型定义为 int.


如果下一个读取的字符将为 0xFF, 则


int c;

c = fgetc (rfp); // c = 0x000000FF;

if (c != -1) // 当然不等, -1 是 0xFFFFFFFF

fputc (wfp); // 噢, OXFF 复制成功.


字符0xFF, 其本身并不是EOF.


(4) 将 c 定义 char


假定下一个读取的字符为 0xFF 则


char c;

c = fgetc (rfp); // fgetc(rfp)的值为 0x000000FF, 暗中降为字节, c = 0xFF

if (c != -1) // 字符与整数比较? c 被带符号(signed)扩展为0xFFFFFFFF, 喔噢,

条件成立,文件复制提前退出.


while ((c=fgetc(rfp))!=EOF) 中的判别条件成立, 文件复制结束! 意外中止.


(5) 将 c 定义为 unsigned char;


当读到文件末尾, 返回 EOF 也就是 -1 时,


unsigned char c;

c = fgetc (rfp); // fgetc (rfp)的值为EOF,即-1,即0xFFFFFFFF, 降格为字节, c=0xFF

if ( c!= -1) // c 被扩展为 0x000000FF, 永远不回等于 0xFFFFFFFF


所以这次虽然能正确复制 0xFF, 但却不能判断文件结束. 事实上,在 c 为 uchar 时,

c != -1 是永远成立的, 一个高质量的编译器, 比如 gcc会在编译时指出这一点.


(6) 为何需要feof?

FILE *fp;

fp 指向一个很复杂的数据结构, feof 是通过这个结构中的标志来判断文件是否结束的.

如果文件用 fgetc 读取, 刚好把最后一个字符读出时, fp 中的EOF标志不会打开,这时

用feof判断,将会得到文件尚未结束的结论.


fgetc 返回 -1 时, 我们仍无法确信文件已经结束, 因为可能是读取错误! 这时我们

需要 feof 和 ferror.