我正在尝试理解 getchar() != EOF。

42

我正在阅读《C程序设计语言》一书,目前为止我理解了所有内容。但是当我遇到getchar()putchar()时,我无法理解它们的用途,更具体地说,下面的代码是做什么的。

main()
{
    int c;
    while ((c = getchar()) != EOF)
       putchar(c);
}

我理解main()函数,整数c的声明和while循环,但是我对while循环中的条件感到困惑。这段C代码的输入是什么,输出又是什么?


22
它只是回显你在终端输入的内容,直到你按下输入结束控制码。在Windows上是Ctrl+Z,在*nix上是Ctrl+D。 - Hans Passant
9个回答

39

这段代码可以更清晰地写成:

main()
{
    int c;
    while (1) {
        c = getchar();            // Get one character from the input
        if (c == EOF) { break; }  // Exit the loop if we receive EOF ("end of file")
        putchar(c);               // Put the character to the output
    }
}

当没有更多输入时,会接收到EOF字符。在从实际文件读取输入而不是用户输入(这是文件的特殊情况)的情况下,该名称更有意义。


[顺便提一下,通常应将main函数编写为int main(void)。]


1
@TheGameiswar,请也阅读我的新补充 - Antti Haapala -- Слава Україні
2
EOF 不是一个字符。 - Ayxan Haqverdili

28

getchar() 是一个从标准输入读取字符的函数。EOF 是在C语言中用来表示已经达到文件结尾的特殊字符。

通常情况下,当你的标准输入不是来自控制台(比如文件)时,getchar() 函数将返回一个 EOF 字符。

如果你在类 Unix 系统中以这种方式运行程序:

$ cat somefile | ./your_program

如果你使用getchar()函数从somefile中获取字符,当somefile结束后,getchar()将会返回每个字符以及EOF

如果你像下面这样运行你的程序:

$ ./your_program

通过控制台发送 EOF (在 Unix 中按下 CTRL+D 或在 Windows 中按下 CTRL+Z),然后 getchar() 也会返回 EOF,执行将结束。


使用 getchar() 演示多态的好例子。从文件(输入)中读取,也可以从键盘(输入)中读取。 - overexchange
你甚至可以使用 ./your_program < input_file 的方式。 - DevUt

9
现行的C标准编写的代码应该是:
#include <stdio.h>

int main(void)
{
    int c;
    while ((c = getchar()) != EOF)
       putchar(c);
}

循环可以改写为:
int c;
while (1) {
    c = getchar();
    if (c != EOF)
        putchar(c);
    else
        break;
}

这段代码的含义是:

  • 重复以下步骤:
    • 标准输入流 获取并存储下一个字符("字节")到变量 c
    • 如果在读取此字符时没有出现异常情况
      • 则将存储在变量 c 中的字符输出到 标准输出流
    • 否则
      • 跳出循环

许多编程语言通过 抛出异常 来处理异常情况以打破正常程序流程,但 C 并不如此。相反,可能失败的函数会有一个返回值,并且任何异常情况都通过特殊返回值来表示,您需要从给定函数的文档中检查该值。对于 getchar 函数,C11 标准的文档说明如下 (C11 7.21.7.6p3):

  1. getchar 函数从由 stdin 指向的输入流中返回下一个字符。如果流已到达文件结尾,则设置流的文件结尾指示器,并使 getchar 返回 EOF;如果读取出错,则设置流的错误指示器,使 getchar 返回 EOF

另外还说明了 EOF 是一个小于 0 的整数常量,而任何普通返回值都大于等于 0 —— 这个值是零扩展为 int 的无符号字符。

当输入流到达文件结尾时就意味着所有输入都已被使用完毕。在标准输入流中,可以通过在 Unix/Linux 终端上按下 Ctrl+D,或者在 Windows 控制台窗口中按下 Ctrl+Z 来引发这种情况。另一个可能性是程序从文件或管道中接收输入而不是从键盘中接收输入——那么只要该输入被完全使用,就会发出文件结尾信号。

cat file | ./myprogram

或者

./myprogram < file

正如上面的片段所述,实际上有两种不同的情况会导致getchar返回EOF: 要么到达了文件结尾,要么发生了实际错误。这不能仅从返回值中推断出来。相反,您必须使用feofferror函数。feof(stdin)在标准输入达到文件结尾时返回一个true值。ferror(stdin)如果发生错误则返回true。
如果发生实际错误,由<errno.h>定义的变量errno将包含错误代码;函数perror可用于自动显示带有前缀的人类可读的错误消息。因此,我们可以扩展示例:
#include <stdio.h>
#include <errno.h> // for the definition of errno
#include <stdlib.h> // for exit()
int main(void)
{
    int c;
    while ((c = getchar()) != EOF)
       putchar(c);

    if (feof(stdin)) {
        printf("end-of-file reached\n");
        exit(0);
    }
    else if (ferror(stdin)) {
        printf("An error occurred. errno set to %d\n", errno);
        perror("Human readable explanation");
        exit(1);
    }
    else {
        printf("This should never happen...\n");
        exit('?');
    }
}

触发文件结束,可以在Linux上新建一行后使用Ctrl+D(显示为^D):
% ./a.out
Hello world
Hello world
^D
end-of-file reached

注意这里的输入是行缓冲的,因此输入不会与输出在同一行中交错。

同样地,我们可以通过使用管道来实现相同的效果。

% echo Hello world | ./a.out
Hello world
end-of-file reached

触发错误有点棘手。在bash和zsh shell中,可以关闭标准输入使其不从任何地方获取内容,方法是在命令行末尾添加<&-
% ./a.out <&-
An error occurred. errno set to 9
Human readable explanation: Bad file descriptor

坏文件描述符EBADF表示标准输入 - 文件描述符号0无效,因为它根本没有被打开。

另一种有趣的生成错误的方法是从目录读取标准输入 - 这会导致Linux上的errno设置为EISDIR

% ./a.out < / 
An error occurred. errno set to 21
Human readable explanation: Is a directory

实际上,putchar 的返回值也应该被检查——它在发生错误时同样会返回 EOF ,或者返回已写入的字符。
while ((c = getchar()) != EOF) {
    if (putchar(c) == EOF) {
        perror("putchar failed");
        exit(1);
    }
}

现在我们可以通过将标准输出重定向到/dev/full来测试 - 但是有一个陷阱 - 由于标准输出被缓冲,我们需要写入足够的内容以立即刷新缓冲区而不是在程序结束时。我们从/dev/zero获得无限的零字节:

 % ./a.out < /dev/zero > /dev/full
 putchar failed: No space left on device

提示:在存储getchar()的返回值时,始终使用int类型的变量非常重要。即使它读取一个字符使用有符号/无符号/普通的char都是错误的


@TonyTannous必须完成赏金任务:D - Antti Haapala -- Слава Україні
感谢您的努力和帮助。 - TheGameiswar

6

getchar()函数从键盘(即stdin)读取一个字符。

在给定的while循环的条件中,在每次迭代之前调用getchar()并将接收到的值赋给整型变量c

现在,必须理解,在C语言中,标准输入(stdin)就像文件一样。也就是说,输入有缓冲区。输入将保留在缓冲区中,直到实际使用它。 stdin实际上是标准输入流

getchar()返回输入缓冲区中的下一个可用值。

该程序基本上显示从键盘读取的任何内容,包括空格符\n(换行)、空格等。

也就是说,输入是用户通过键盘提供的输入(stdin通常表示键盘)。 而输出则是我们所提供的输入。

我们提供的输入逐个字符地读取并作为字符处理,即使我们将它们作为数字输入也是如此。

getchar()仅在到达文件结尾时返回EOF。我们在这里关心的“文件”实际上是stdin本身(标准输入)。

想象一下一个存在的文件,其中通过键盘提供的输入正在被存储。那就是stdin。 这个“文件”就像一个无限的文件。所以没有EOF

如果我们提供的输入比getchar()一次可以处理的更多(在按回车之前),则额外的值仍将存储在未使用的输入缓冲区中。 getchar()将从输入中读取第一个字符,将其存储在c中,并用putchar(c)打印 。

while循环的下一次迭代中,上一次迭代期间给出但仍在stdin中的额外字符通过while ((c = getchar()) != EOF)c=getchar()一起被获取。 现在,同样的过程重复进行,直到输入缓冲区中没有剩余内容为止。

这使得看起来putchar()返回一个字符串而不是每次返回单个字符,如果在一个迭代中输入了多个字符。

例如,如果输入为
abcdefghijkl
输出将会是相同的
abcdefghijkl

如果您不想要这种行为,可以在putchar(c);之后添加fflush(stdin);。 这将导致循环仅打印每次迭代期间提供的输入中的第一个字符。
例如,如果输入为
adgbad
只有a将被打印出来。

输入只有在按下回车键后才发送到stdin
putchar()getchar()的相反操作。它将输出写入标准输出流(stdout,通常是监视器)。
EOF不是文件中存在的字符。它是由函数作为错误代码返回的东西。
您可能无法正常退出给定的while循环。输入缓冲区将在通过键盘输入任何内容并且stdin不会给出EOF时被清空(用于显示到输出)。
为了手动退出循环,在Linux中可以通过键盘按ctrl+D发送EOF,在Windows中可以通过键盘按ctrl+Z发送EOF
例如:
while ((c = getchar()) != EOF)
{

   putchar(c);
   fflush(stdin);
}
printf("\nGot past!");

如果您按下组合键以给出EOF,则在退出程序之前将显示消息Got past!
如果stdin尚未为空,则需要按两次此键组合。一次清除此缓冲区,然后模拟EOF
编辑:在while ((c = getchar()) != EOF)中,额外的括号围绕c = getchar()是为了确保在将该值与EOF进行比较之前,getchar()返回的值首先被赋给c
如果没有这个额外的括号,表达式实际上会变成while (c = (getchar() != EOF) ),这意味着c可以有两个值之一:1(为真)或0(为假),这显然不是预期的结果。

4
也许你会因为在命令行输入-1并不能结束程序而感到困惑?这是因为getchar()将其读取为两个字符,即负号和数字1。在赋值给c时,该字符被转换为ASCII数值。该数值存储在某个内存位置中,由c访问。
然后,putchar(c)检索此值,查找ASCII表并将其转换回字符,然后进行打印。
我猜在ASCII表中找到-1十进制的值是不可能的,因为该表从0开始。因此,getchar()必须考虑不同平台上的不同解决方案。也许每个平台都有一个相应的getchar()版本?
我只是觉得这个EOF不在常规的ASCII中很奇怪。它本来可以是其中一个不可打印的第一个字符。例如,换行符在ASCII中。
如果你将文件从Windows传输到Linux,会自动更新EOF文件字符吗?

3
这似乎更像是新问题,而不是一个真正的答案。 - MestreLion

1
 getchar()

从输入中获取一个字符。
 c = getchar()

这个赋值语句的值是赋值后左侧的值,或者是已读取字符的值。默认情况下,EOF 的值为 -1
 ((c = getchar()) != EOF)

只要值不是EOF,或者换句话说,只要条件保持为真,循环就会继续迭代。一旦值变成EOF,整个条件的值将为0并且它将打破循环。 c = getchar()周围的额外括号是为了编译器,以强调我们确实想在条件内执行赋值操作,因为它通常会假定您想键入==并发出警告。
 main() {
     int c;
     while ((c = getchar()) != EOF)
         putchar(c);
 }

整段代码实际上会回显您输入的内容。它将字符的值分配给条件内的 c ,然后在循环体中输出它,仅在检测到文件结尾时结束。

1
额外的括号(c = getchar())是为了告诉编译器,我们不想把getchar() != EOF的结果赋值给c。 - Thomas G.

0
main(){
int c;
while ((c = getchar()) != EOF)
   putchar(c);
}

实际上,c=getchar() 提供了用户在控制台输入的字符,并将该值与 EOF 进行比较,EOF 表示文件结尾。EOF 在文件末尾遇到。 (c = getchar()) != EOF 等同于 c != EOF。现在我认为这更容易理解了。如果您有任何进一步的问题,请告诉我。


0

类似于上面的 | 管道命令,您可以在系统上使用重定向来利用上述代码显示文件的所有字符内容,直到它达到通常由 CTRL-Z 或 CTRL-D 表示的结尾(EOF)。

在控制台中: 程序名称 < 文件名1.txt

要创建从 FileName1 读取的副本,您可以执行以下操作: 程序名称 < 文件名1.txt > 输入的副本.txt

这展示了您的程序的多种方式,希望有助于您的理解。

-希望这有所帮助。


0
为什么还没有人说过,int getchar(void)每次调用时返回下一个输入字符,或者在遇到文件结束时返回EOF?虽然这是事实,并且与主题无关!

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