当前位置:C++技术网 > 资讯 > C语言文件读写文件尾多出一个字符的原因分析过程和解决过程

C语言文件读写文件尾多出一个字符的原因分析过程和解决过程

更新时间:2015-06-27 01:01:29浏览次数:1+次

    本文是朋友提出的一个问题,即读取一个文件并写入一个新文件,新文件结尾多出了一个字符。而代码看上去很正常,结果出人意料。这里通过逐步的分析,层层递进,一步一步的把问题的原因剖析出来,并提出了两种解决办法。你可以从此分析过程中,领略到跟踪问题的方法和思路,同时也可以对此问题有更加深入的理解。给出的解决办法,也可以切实的解决实际问题。
    一朋友问我了一个问题,说以下代码将一个文件的内容读取出来,然后逐个字符的写入到另一个文件,然后完成后,新文件竟然多了一个文件。很疑问,代码看上去似乎都是正常的。
    代码如下:

#include <stdio.h>
void main()
{
    FILE * fp=0;
    FILE * fp2=0;
    fp = fopen("1.txt","r");
    fp2= fopen("2.txt","w");
    char ch;
    while (!feof(fp))
    {
        ch = fgetc(fp);
        fputc(ch,fp2);
    }
    fclose(fp);
    fclose(fp2);
}

    以上代码看上去确实不存在问题。一切都合乎道理,就是最后多了一个字符。他不清楚为什么,问了我。我一开始也不知道为什么,代码确实看不出任何问题。所以就晚上回去研究一番。
    既然要看多出的一个字符,我们就要看这个多出的字符是什么字符,从而来确定出了什么问题。因为自己的电脑没有其他软件,只有一个VS。要看txt文件的二进制数据,还一下子卡住了。突然想起了VS有一个查看二进制数据的功能。
    VS查看二进制数据的方法。先将文件添加到解决方案中,然后右击“打开方式”,选择二进制打开即可。根本就不需要其他软件。
    这样打开后,发现多了一个0xFF字节,对应为十进制就是-1,即宏EOF,表示文件尾标识。
    然后就想,为什么会多出这个EOF字符呢?所以就使用了断点单步跟踪。给1.txt文件添加123456保存。这个文件就只有6个字符,通过VS二进制查看器看到的。然后跟踪程序,循环了六次后,正常。然后继续,发现理应不进入循环的,结果却进入了循环。这才导致了进入循环后,fgetc读取的是文件尾,返回的EOF(-1)。然后将这个EOF写入了新文件。这样就多出了最后的一个EOF字符,即-1.

    问题水落石出,如何解决呢?我们知道了这是循环判断出问题了,但是除了这个写法,大家都不知道如何去判断。所以,我开始就在内部添加一个队ch变量的字符检测,只有不等于EOF,才会写入新文件,否则忽略。这样,循环在下一次就结束了,不会再进入循环了。如此就解决了这个问题。
    在循环内部添加检测的代码如下:

#include <stdio.h>
void main()
{
    FILE * fp=0;
    FILE * fp2=0;
    fp = fopen("1.txt","r");
    fp2= fopen("2.txt","w");
    char ch;
    while (!feof(fp))
    {
        ch = fgetc(fp);
        if (ch!=EOF)
        {
            fputc(ch,fp2);
        }
    }
    fclose(fp);
    fclose(fp2);
}


    但是本着认真研究问题的态度,所以,继续了探索。试图找到为什么判断会错误的原因。不想在内部检测。
    所以,就跟踪了循环的6次,对fp进行跟踪分析。然后将fp结构的内部参数的变化做了记录。FILE结构的说明,参见文章《C语言的文件操作深入分析之FILE结构体和文件操作机制》。
    通过分析参数变化发现,FILE的_cnt初始化时是0,读取第一个字符时,变成总字符数,读完一个字符,此数字减一。读完最后一个字符变成0。其他各种参数没有变化。然后继续读取最后一个EOF,此时_cnt依然为零。此时的FILE的_ptr是指示当前指针的值,表示当前读取到哪里。读取了EOF后,_ptr就变成了_base一样的值。_base是文件起始的地址。表示当前文件指针回到了起始地址。_flag变成由9变成了25。说明文件读取状态已经发生了变化。此时的状态涵盖了文件读取到了文件尾的信息。此时读取文件就结束。使用feof就可以检测到文件读取完毕了。
    通过跟踪feof函数发现,函数内部实际就是检测_flag参数,将其与_IOEOF(0x0010)位与来检查是否读取完毕的。当读完EOF后,feof就检测到了文件尾,所以就不再进入循环。而读完字符6之后,指针只是指向了文件尾,但是这些状态还没有变过来,所以,feof检测就失败。因此就进入了循环。
    那么,既然如此,我们就不能依赖feof来做检测,或者一定要在循环内部做进一步的检测。如果不想在循环内部做检测,那么,就在循环条件判断换一种,不要使用feof了。使用的新方法,便是对_cnt来检测,即读取剩余的字符数来检测。因为一开始,它是0,所以,我们不能使用while,应该使用do while来做。这时候,没有比do while更适合的写法了。读取了一个字符之后,_cnt的值就是剩下的字符数了,之后就用后面的while正常检测,一旦变成零,即退出循环。
    在循环外检测文件读取状态的代码如下:

#include <stdio.h>
void main()
{
    FILE * fp=0;
    FILE * fp2=0;
    fp = fopen("1.txt","r");
    fp2= fopen("2.txt","w");
    char ch;
    do
    {
        ch = fgetc(fp);
        fputc(ch,fp2);
    }while (((fp)->_cnt != 0));
    fclose(fp);
    fclose(fp2);
}
     这样也就完美解决了这个问题。但是一般情况下,你不清楚feof内部的工作机制,也不知道文件读取的机制,也不知道如何自己检测,那么就在循环内部简单的加个判断即可。如果很清楚,使用后面的代码,一样解决了。