【问题标题】:Why does a loop containing getchar() exit when '\n' is entered?为什么输入 '\n' 时包含 getchar() 的循环会退出?
【发布时间】:2021-01-24 10:06:16
【问题描述】:

我与 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()时,它如何知道换行符是终止条件?

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

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

首先,c 变量变为'H',它被存储在行中,然后是'e',然后是'l',然后是'l',然后是'o',然后它遇到换行符并且循环终止.很好理解。

我想知道为什么它在我按回车后开始读取字符。我希望换行并写更多字符。

【问题讨论】:

  • 当您输入换行符时,c != '\n' 为 false,因此整个 while 条件也是如此。至此结束循环。无论如何,此代码都表现出未定义的行为line 未终止,并且作为未初始化的自动变量,不能保证终止符已经到位。因此,将其作为参数传递给 %s 格式说明符,该格式说明符要求终止字符串,调用 UB,充其量只是一场赌博。
  • 但换行条件c != '\n' 中包含 c。当我写Hello 时,现在不管c 是什么H。所以,除非你说循环在我写的时候正在运行,否则终止没有意义。
  • 所以你问为什么/是否stdinline buffered ? (因为它通常是,仅供参考)。
  • @WhozCraig 我只是想知道为什么 c != '\n' 在 c 仍然为空或 H 时调用。或者当我按下回车时输入终止的其他原因
  • “为什么 c 仍然为空时调用 c != '\n'” - 不是。一旦换行符在流中,就会发送缓冲区,在您的情况下,一次消耗一个字符。顺便说一句,根据我的经验,它是终端,而不是实际的运行时,在做你正在经历的缓冲。当通过 IO 重定向提交输入时(因此没有终端插入),终端行缓冲被规避。

标签: c loops c-strings getchar


【解决方案1】:

理解该代码有两个部分,还有一个错误,chqrlie 提出了一个很好的论点来修复。

第 0 部分:为什么应该使用 intgetchar 进行阅读

正如许多人评论的那样,如果您打算使用getchar 阅读,使用char c 是危险的,因为getchar() 返回有符号整数,最值得注意的是EOF——通常是#defined 作为@ 987654331@ 表示文件结束。标准 char may or may not have a sign - 这会使您的程序无法识别 -1 / EOF。所以让我们把第一行改成

int c,i; 

第 1 部分:为什么 \n 很特别

根据mangetchar() 等价于getc(stdin),它等价于 fgetc(),除了它可以实现为一个评估其流的宏(stdin,在这种情况下)超过一次。

重要的是,每次调用它时,它都会消耗其输入中的一个字符。只要有要返回的字符,每次调用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;但是当按下时,输入将在第一天完成a(不包括在内)。请注意,之后的输出将是未定义的,因为不能保证之后会有\0 来终止字符串。为了避免这个陷阱,请在最后查看重写的程序。

第 2 部分:为什么循环条件会这样工作

您可以如下重写循环条件。这样可以更轻松地查看正在发生的事情:

// 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 语言,而是解释某一行,即问题中的那一行,是如何工作的。
  • @chqrlie 你是对的,得到!= EOF 问题的一部分。已解决。
  • 好多了!我只想在printf("%s\n", line); 中添加一个尾随换行符,以确保在终端上正确显示输出,因为line 没有换行符。
【解决方案2】:

程序不正确,可以调用未定义的行为。

对于初学者,变量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!

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

所以数组将包含以下字符串

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

输出。

【讨论】:

    【解决方案3】:

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

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

    这是修改后的版本:

    #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;
    }
    

    关于控制台响应程序输入请求的行为,它是由实现定义的,但通常涉及两层缓冲:

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

    研究这个感觉就像剥洋葱:当你穿过皮肤层时,你会发现更多层要刮掉,这让你哭泣:)

    【讨论】:

      【解决方案4】:

      由于它是来自 K&R 的示例,并且由于它不是您问题的中心问题,所以让我们回顾一下应该是 int cchar c(因为 getchar () 返回一个 int)。你会发现很多问题可以更好地解释它。

      while 循环的行为是

      while (condition_is_true)
          Do_Something;
      

      您的条件包含一个总是被执行的赋值:

      c=getchar()
      

      这是逻辑检查 (c != EOF) 的一部分,在您的程序中始终为真(您正在从 stdin 读取)。因此,&amp;&amp; 之外的条件将被执行(shortcircuiting 确保在逻辑 and 中的操作数从左到右进行计算,直到它们为真。

      后一个条件是c != '\n'"Hello" 字符串中的所有字符都将为 false,并且所有字符都将存储在您的 line 数组中。但是一旦插入换行符,由于之前的赋值将\n 放入c,条件变为假,执行退出循环(因此,换行符不会存储在line 数组中) .

      然后,然后,字符串line 将被打印出来。

      【讨论】:

      • 好吧,有点清楚了。但是the previous assignment put \n into c,所以基本上在我编写 c 时,它会更新字符并检查条件,然后它会重置为 H,然后循环开始运行?
      • @mr.loop 每当您在标准输入中写入内容时,getchar 都会返回它并将其分配给c。如果它不是换行符,则执行循环并在 getchar 处再次阻塞,直到插入新字符。这一直持续到插入的字符是换行符。
      【解决方案5】:

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

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

      玩得开心学习 C,不要害怕提问!

      【讨论】:

      • until you press Enter key。那是从哪里来的?因为这不是 getchar() 默认行为,请尝试删除换行条件。现在,我再次说 c 在我写作时没有任何东西,或者充其量它有 H,那么为什么 c != '\n' 适用。
      • 'a' 替换'\n' 仍然会产生一个工作程序,它会读取到并且不包括'a'。是的,缓冲区只会在换行时清空,但这不是 OP 的问题。
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-11-06
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多