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

Why does a loop containing getchar() exit when '\n' is entered?

我在与 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',之后遇到换行符,循环终止。很好理解。

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

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

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

祝您学习 C 语言愉快,不要害怕提问!

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

while 循环行为是

while (condition_is_true)
    Do_Something;

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

c=getchar()

它是逻辑检查 (c != EOF) 的一部分,它在您的程序中始终为真(您正在阅读 stdin)。因此,执行超出 && 的条件(shortcircuiting 确保在逻辑 and 操作数中从左到右评估直到他们是真的。

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

Then, and after then, the string line will be printed.

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

第 0 部分:为什么要使用 intgetchar

一起阅读

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

int c,i; 

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

根据 mangetchar() 等同于 getc(stdin),后者等同于 fgetc() 除了它可以作为一个计算其流的宏来实现(stdin,在这种情况下)不止一次。

重要的是,每次调用它时,它都会从其输入中消耗一个字符。每次调用 getchar return 都会从输入中输入 下一个 字符,只要有字符到 return。如果 none 仍然存在,则 return 改为 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(不包括)。请注意,之后的输出将是未定义的,因为无法保证之后会有一个 [=35=] 来终止字符串。为了避免这个陷阱,请在最后查看重写的程序。

第 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"
}

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

对于初学者,变量 c 应声明为

int c;

否则条件

(c=getchar()) != EOF

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

其次,类型 char 在任何情况下都不能保存等于 10000 的值,即字符数组的大小。所以变量 i 应该至少声明为 short int.

类型

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

    line[i++] = c;

可以超出字符数组写入。

最后,结果字符数组 line 不包含字符串,因为终止零字符 '[=34=]' 未附加到输入的字符序列中。结果这个电话

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] = '[=14=]';

    puts( line );
}

也就是循环不断填充字符数组,直到字符数组linespace足够

i + 1 < N 

用户不中断输入

( c = getchar() ) != EOF

并且没有按回车键完成输入字符串

c != '\n'

循环后附加终止零

    line[i] = '[=18=]';

现在数组line包含一个在语句

中输出的字符串
    puts( line );

因此,例如,如果用户将键入此字符序列

Hello world!

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

中的字符

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

{ 'H', 'e', 'l', 'l', 'o', ' ', 'W', 'o', 'r', 'l', 'd', '!', '[=21=]' }

即输出。

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

  • c 应该具有类型 int 以容纳所有由 getc() 编辑的值 return,即所有类型 unsigned char 的值(在大多数情况下当前系统 0255) 和特殊的负值 EOF(通常为 -1)。
  • i 也应该有类型 int,或者可能 size_t 以正确索引到 line 数组。如果您输入的行长于 127 个字符,则具有 char 类型的已发布代码可能具有未定义的行为。
  • 你应该测试 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] = '[=10=]';   // null terminate the array.

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

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

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

调查这件事就像剥洋葱一样:当你穿过一层层的皮肤时,你会发现更多的层要刮掉,这让你哭:)