在C中从文件读取并打印时,输出结果不正常。

3

我编写了一个程序,可以读取并输出其自身源文件的内容。我的目的主要是学习如何使用I/O流和"FILE"类型。我在Linux Ubuntu 14.04上使用纯文本文档编写了该程序,并使用终端编译和运行程序。以下是从编译到结束的终端内容:

joseph@ubuntu:~/Desktop$ gcc test.c
joseph@ubuntu:~/Desktop$ ./a.out

File Opened

#include<stdio.h>
#define fileLocation ("/home/joseph/Desktop/test.c")
#define MAXREAD 1000

int main(void)
{
    char fileContents[MAXREAD];
    int i;

    FILE *tf;
    tf = fopen(fileLocation, "r");

    printf("File Opened\n");
    for(i=0;fileContents[i] != EOF; i++)
    {
        fileContents[i] = fgetc(tf);
        printf("%c", fileContents[i]);
    }
    fclose(tf);
    printf("\nFile Closed\n");
    return 0;
}
************************************************************

File Closed

这些 * 符号实际上是Unicode (0+FFFD: Replacement Character),但我似乎无法输入它们。

我的问题是,为什么程序没有在最后一个右括号处终止,而是打印了一堆替换字符呢?


文件大小是否小于1000字节?我估计大约450字节,除非使用了一些有趣的编码。 - chux - Reinstate Monica
关闭,文件大小为381字节。 - Magister Ludi
这里fileContents缓冲区是不必要的 - 你获取一个字符,输出一个字符 - 不需要缓冲区,并且如果文件大于1000字节,它将被覆盖。 char ch = fgetc(tf); printf("%c", ch);就足够了。如果您确实使用缓冲区,可以使用fread()一次性读取所有内容。 - Clifford
@Clifford fgetc(tf) 返回257个不同的值。其中256个意味着读取了一个char并需要打印。另外一个值EOF则表示文件结束/IO错误。因此需要将结果保存在int而不是char中。 - chux - Reinstate Monica
这非常有帮助,@chux。你关于最佳答案的观点是正确的,我已经进行了更正。这个主题可以关闭了。 - Magister Ludi
显示剩余4条评论
1个回答

3
您的循环顺序不正确。在存储和打印字符值之前,应先检查 EOF 是否存在。您还应确保不超过数组边界。
int main(void)
{
    char fileContents[MAXREAD];
    int i, c;

    FILE *tf = fopen(fileLocation, "r");
    if (tf == NULL)
    {
        perror(fileLocation);
        return EXIT_FAILURE;
    }

    printf("File Opened\n");
    for (i=0; i < MAXREAD && (c = fgetc(tf)) != EOF; ++i)
    {
        fileContents[i] = c;
        fputc(fileContents[i], stdout);
    }
    fclose(tf);
    printf("\nFile Closed\n");
    return 0;
}

你的代码版本包含在一个字符(char)中错误存储EOF并打印出来(这本身就是另一个问题,但通过不存储它来避免)。但这远非你的烦恼的尽头。你用于继续for循环的条件逻辑是错误的。实际上,由于你从未初始化fileContents[],它会导致未定义行为。在每次迭代中,你都在检查一个尚未写入或初始化的数组槽位。接下来阅读如何/为什么的内容。
为什么你要持续打印?
控制表达式fileContents[i] != EOF在每个循环迭代之前被评估。递增表达式i++在每个迭代之后执行,但在下一次控制条件的评估之前执行。根据标准:
"for (clause-1; expression-2; expression-3) statement"
的行为如下:表达式expression-2是控制表达式,在执行循环体之前进行评估。表达式expression-3在执行循环体后作为空表达式进行评估。如果clause-1是一个声明,则它声明的任何标识符的作用域是声明的其余部分和整个循环,包括另外两个表达式;在第一次评估控制表达式之前按照执行顺序到达。如果clause-1是一个表达式,则在第一次评估控制表达式之前,将其作为空表达式进行评估。
直白地说,你刚刚保存在fileContents[i]中的EOF永远不会被检查,因为在下一次评估之前i被增加了。从上面的描述中可以看出这是有道理的。这正是简单循环的原因:
for (i=0; i<N; ++i)
    dostuff;

i < Nfalse 时,循环退出。除非在 dostuff 中有意外的修改,否则循环将以 i = N 结束。

再次强调,在增量步骤之后执行 eval,因此在您的情况下:

for(i=0; fileContents[i] != EOF; i++)

控制表达式fileContents[i] != EOF在每次进入循环体之前进行评估。增量表达式发生在循环体之后,但在下一次控制表达式的评估之前。在循环体中,您将EOF存储到当前值为i的索引的插槽中。然后,当循环体完成时,i被递增,然后才检查一个还未写入任何东西的插槽。这将继续进行直到某个点,如果你运气不好,你会在新更新的i索引上发现一个等效于EOF的值。因此,您会终止程序(但很可能,在那之前就已经崩溃了)。

我同意这些问题,但是除非数组越界(UB),为什么要多个'*'? - chux - Reinstate Monica
@WhozCraig,我现在明白了关于“i索引在fileContents [i]!= EOF之前被递增”,导致问题。因此对于OP来说,它只是缓冲区中或过去的随机垃圾最终停止了循环。 - chux - Reinstate Monica
显然有人持不同意见。欢迎进行合理的讨论以反驳上述观点,但我不会抱太大期望。 - WhozCraig
@chux: 我们并不反对,我的观点是关于缓冲区是不必要的,并且溢出的危险是微不足道的。在循环中,您可以直接打印 c ,将 fileContents[i] 赋值没有任何意义。感谢您针对原始问题代码进行回答,而该评论并不是对代码的批评,而是指向OP的一个观点。 - Clifford
我的朋友告诉我,无论只有一个字符,总是应该使用某种缓冲区,因为这是一个好习惯。这就是为什么我使用它而不是直接读取/打印的原因。 - Magister Ludi
显示剩余3条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接