当输入'\n'时,包含getchar()的循环为什么会退出?

4

我曾经和K&R一起工作,基础部分中它广泛使用 getchar() 函数进行输入。但问题是我无法完全理解其行为。

下面是一段示例代码:

#include <stdio.h>

int main() {
    char c,i;
    char line[10000];
    i = 0;

    while((c=getchar()) != EOF && c!= '\n') {
        line[i++] = c;
    }

    printf("%s",line);
}

这段代码的运行符合预期。

我对此有疑问:为什么当我按下回车键时,程序就终止了?它是如何知道换行是终止条件的,而我还在输入并且程序停留在c=getchar()?

我知道这不是像scanf()那样的默认getchar()行为,因为当我删除换行条件时,程序不会在换行处终止。也许我的问题超出了getchar(),是一个一般性的问题。

假设我的输入是Hello,我按下回车键。

首先,变量c变成了'H',它被存储在行里,然后是'e',然后是'l',再然后是'l',最后是'o',之后遇到换行符循环终止。这很容易理解。

我想知道为什么它会在我按下回车键后开始读取字符。我本来希望它等待换行,然后再写一些字符。


1
当您输入一个换行符时,c != '\n' 为假,因此整个 while 条件也是如此。循环结束。无论如何,此代码都表现出未定义的行为line 没有终止,并且作为未初始化的自动变量,没有保证已经放置了终止符。因此将其作为 %s 格式说明符的参数传递,这需要一个终止的字符串,会引发 UB,并且最多是一种赌博。 - WhozCraig
但是换行条件c != '\n'中包含了c。当我写Hello时,无论c是什么,都是H。因此,除非你说循环在我写作时运行,否则终止没有意义。 - mr.loop
所以你在问为什么stdin通常是行缓冲吗?(顺便提一下,因为这通常是这样)。 - WhozCraig
1
为什么在c仍为空的情况下调用c != '\n'? - 并没有。一旦换行符进入流中,缓冲区就会被发送,在您的情况下每次消耗一个字符。顺便说一句,根据我的经验,造成你遇到的缓冲��终端,而不是实际运行时。当通过IO重定向提交输入(因此没有终端插入)时,绕过了终端行缓冲。 - WhozCraig
@M.M 是的,但这不改变问题。 - mr.loop
显示剩余2条评论
5个回答

3

了解这段代码有两个部分,同时还有一个错误,chqrlie已经提出了修复的好建议。

第0部分:为什么应该使用int来读getchar

正如许多人评论的那样,如果你要使用getchar读取,那么使用char c是很危险的,因为getchar()返回带符号的整数,特别是EOF,通常被定义为-1表示文件结束。标准的char可能有也可能没有符号——这会导致您的程序无法识别-1/EOF。所以我们将第一行改为:

int c,i; 

第一部分:为什么\n很特殊

根据 man 所述,getchar() 等同于 getc(stdin),除了它可能被实现为一个宏,该宏会对其流(在本例中为 stdin)进行多次求值。

重要的是,每次调用getchar时,它会从输入中“消耗”一个字符。 只要还有字符可以返回,每次调用getchar都会返回输入中的下一个字符。 如果没有字符剩余,则返回EOF

现在,标准输入流 stdin 通常是行缓冲的,这意味着程序将无法访问实际字符,直到行以\n终止。 您可以使用此程序测试:

#include <stdio.h>

int main() {
    int c,i;
    char line[10000];
    i = 0;

    while((c=getchar()) != EOF && c!= 'a') { // <-- replaced `\n` with `a`
        line[i++] = c;
    }

    printf("%s",line);
}

如果你运行它,直到按下 \n 键,它还不会有任何作用;但是一旦按下,输入将在第1个 a (不包括)处终止。请注意,之后的输出将是未定义的,因为不能保证之后会有一个\0来终止字符串。为避免这种问题,请参见最后重新编写的程序。
第二部分:为什么循环条件起作用的方式如此
您可以将循环条件重写如下。这样更容易看出发生了什么:
// loop condition looks up next char, tests it against EOF and `\n`
while((c=getchar()) != EOF && c!= '\n') { line[i++] = c; }

// loop condition broken up for readability; fully equivalent to above code
while (true) {
   c = getchar();
   if (c == EOF || c == '\n') {
      break; // exit loop
   } else {
      line [i++] = c;
   }
}

结论:改进的代码

#include <stdio.h>
#define BUFSIZE 10000

int main() {
    char line[BUFSIZE]; // avoid magic number
    int c, i = 0;       // initialize at point of declaration
    
    while (i<BUFSIZE-1              // avoid buffer overflow
         && (c=getchar()) != EOF    // do not read past EOF
         && c!= '\n') {             // do not read past end-of-line
        line[i++] = c;
    }

    line[i++] = 0;      // ensure that the string is null-terminated
    printf("%s",line);
    return 0;           // explicitly return "no error"
}

@chqrlie,问题不是“我该如何使用C语言”,而是“为什么这个程序可以工作”。我的目标不是重写整个程序以使其完全正确,也不是教授所有的C语言知识,而是解释一个特定的代码行(即问题中的那一行)是如何工作的。 - tucuxi
@chqrlie,你说得对,获取!= EOF 问题的一部分。已解决。 - tucuxi
非常好!我只会在 printf("%s\n", line); 中添加一个尾随换行符,以确保输出在终端上正确显示,因为 line 没有换行符。 - chqrlie

3
程序不正确,可能会调用未定义行为。
首先,变量 c 应该声明为:
int c;

否则条件
(c=getchar()) != EOF

即使用户试图中断输入,以下条件始终成立。问题在于宏EOF是int类型的负整数值。另一方面,char类型可以表现为unsigned char类型。因此,变量c提升为int类型后将始终包含非负值。

其次,无论如何,char类型都不能容纳等于10000的值,该值是字符数组的大小。因此,变量i应至少声明为short int类型。

while循环应检查索引变量i当前的值是否已经大于或等于字符数组的大小。否则,该语句

    line[i++] = c;

可以超出字符数组的范围进行写入。

最后,结果字符数组line不包含一个字符串,因为终止零字符'\0'未被附加到输入的字符序列中。因此,这次调用:

printf("%s",line);

调用未定义的行为。

程序可能如下所示:

#include <stdio.h>

int main( void ) 
{
    enum { N = 10000 };
    char line[N];

    size_t i = 0;
 
    for ( int c; i + 1 < N && ( c = getchar() ) != EOF && c != '\n'; i++ ) 
    {
        line[i] = c;
    }

    line[i] = '\0';

    puts( line );
}

那么,循环将继续填充字符数组,直到字符数组行中有足够的空间为止。
i + 1 < N 

用户不中断输入

( c = getchar() ) != EOF

而且它不需要按回车键来完成输入字符串

c != '\n'

循环结束后添加终止零。
    line[i] = '\0';

现在数组line包含一个字符串,该字符串将在语句中输出。
    puts( line );

例如,如果用户输入以下字符序列:

Hello world!

接着用户按下回车键(这将在输入缓冲区中发送新行字符'\n'),然后循环将停止迭代。新行字符'\n'不会被写入字符串中。在循环之后,终止零字符'\0'将附加到存储在数组line中的字符。

因此,数组将包含以下字符串

{ 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', '\0' }

这就是输出的结果。


2

您的理解基本正确,但代码存在一些问题,输入机制比您推测的更加复杂:

  • c 应该具有 int 类型以容纳所有由 getc() 返回的值,即所有类型为 unsigned char 的值(在大多数当前系统中为 0255)和特殊的负值 EOF(通常为 -1)。
  • i 也应该是 int 类型,或者可能是 size_t 类型,以正确地索引到 line 数组。使用 char 类型的发布代码可能会出现未定义的行为,如果您输入的行超过127个字符。
  • 您应该测试 i 是否保持在数组 line 的边界内。这将需要一个非常长的输入行,但通过从文件重定向很容易实现。
  • 在将 line 作为 %s 格式的参数传递给 printf 之前,必须将其以 null 结尾。

这是修改后的版本:

#include <stdio.h>

int main() {
    int c, i;
    char line[10000];

    i = 0;
    while (i < sizeof(line) - 1 && (c = getchar()) != EOF && c != '\n') {
        line[i++] = c;
    }
    line[i] = '\0';   // null terminate the array.

    printf("%s\n", line);
    return 0;
}

关于控制台在程序输入请求时的行为,它是由实现定义的,但通常涉及2层缓冲:
- FILE流包实现了一个缓冲方案,其中数据以块的形式从系统中读取或写入。可以使用setvbuf()来控制此缓冲。有3个设置可用:无缓冲(对于stderr是默认值),行缓冲(通常是stdin和stdout连接到字符设备时的默认值)以及具有可自定义块大小的完全缓冲(常见大小为512和4096)。 - 当调用getchar()或更一般地调用getc(stream)时,如果流缓冲区中有一个字节,则返回该字节并增加流位置,否则会向系统发出请求以填充缓冲区。 - 如果流连接到文件,则填充缓冲区执行read系统调用或等效操作,除非到达文件结尾或发生读取错误。 - 如果流连接到字符设备,例如终端或类似于图形显示器上的终端窗口的虚拟tty,则另一层缓冲涉及设备驱动程序从输入设备读取输入并以特殊方式处理某些键,例如Backspace删除前一个字符,光标移动键在输入行内移动,Ctrl-D(unix)或Ctrl-Z(windows)表示文件结束。可以通过tcsetattr()系统调用或其他特定于系统的API来控制此缓冲层。交互式应用程序(例如文本编辑器)通常禁用此功能并直接从输入设备检索原始输入。 - 用户键入的键由终端处理以形成输入行,并在用户键入Enter时发送回C流API(它被转换为特定于系统的行尾序列),流函数执行另一组转换(即在旧系统上将CR / LF转换为'\n'),字节行存储在流缓冲区中。当getc()最终有机会返回第一个可用字节时,完整的行已由用户键入并输入,并且在流或设备缓冲区中挂起。
调查这个过程就像剥洋葱一样:当你去掉一层皮时,你会发现更多的层需要刮掉,这会让你哭泣:)

1

由于这是K&R的示例,并且不是您问题的核心问题,让我们解释一下 char c 应该是 int c(因为 getchar() 返回一个 int)。您会发现有很多问题更好地解释了它。

while 循环的行为是

while (condition_is_true)
    Do_Something;

你的 条件 包含了一个赋值操作,它总是会被执行:
c=getchar()

这是逻辑检查的一部分 (c != EOF),在您的程序中它总是为真(您正在从stdin读取)。因此,在&&之后的条件被执行(短路保证在逻辑中,操作数从左到右进行评估,直到它们为真)。
后一个条件是c !='\n'。对于您的"Hello"字符串中的所有字符,它都将为假,并且它们都将存储在line数组中。但是,一旦插入换行符,由于前面的赋值将\n放入c中,条件变为假,执行退出循环(因此,换行符不会存储在line数组中)。
然后,line字符串将被打印出来。

好的,现在有点清楚了。但是“之前的赋值将\n放入c中”,所以基本上当我写入c时,它会被更新为字符并根据条件进行检查,然后它会被重置为H,然后循环开始运行? - mr.loop
@mr.loop,每当您在stdin中写入内容时,getchar会将其返回并分配给c。如果它不是一个换行符,循环就会执行,并且再次被阻塞在getchar处,直到插入新的字符。这一过程会持续进行,直到插入的字符是一个换行符为止。 - Roberto Caboni

-1

这是因为 getchar() 函数的实现方式。该函数首先允许您将字符写入缓冲区,直到按下 enter 键,然后它只从缓冲区获取一个字符。

如果您想直接从键盘获取一个字符,可以使用库 conio.h

学习 C 语言很有趣,不要害怕提问!


直到您按下回车键。这是从哪里来的?因为这不是getchar()的默认行为,请尝试删除换行条件。现在,我再次说c在我写作时没有任何东西,最多只有H,那么为什么c != '\n'适用。 - mr.loop
1
'\n'替换为'a'仍会产生一个可工作的程序,该程序将读取到但不包括'a'。是的,缓冲区仅在换行时被清空,但这并不是问题的关键所在。 - tucuxi

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