如何在C语言中清空输入缓冲区?

114

我有以下的程式:

int main(int argc, char *argv[])
{
    char ch1, ch2;
    printf("Input the first character:"); // Line 1
    scanf("%c", &ch1);
    printf("Input the second character:"); // Line 2
    ch2 = getchar();

    printf("ch1=%c, ASCII code = %d\n", ch1, ch1);
    printf("ch2=%c, ASCII code = %d\n", ch2, ch2);

    system("PAUSE");
    return 0;
}

正如上述代码的作者所解释的那样: 程序无法正常工作,因为在第1行当用户按下Enter键时,它会在输入缓冲区中留下2个字符:Enter键(ASCII码13)和\n(ASCII码10)。因此,在第2行中,它将读取\n,而不会等待用户输入字符。

好的,我明白了。但我的第一个问题是:为什么第二个getchar()ch2 = getchar();)不读取Enter key (13),而是读取\n字符?

接下来,作者提出了两种解决此类问题的方法:

  1. 使用fflush()

  2. 编写一个像这样的函数:

void
clear (void)
{
    while ( getchar() != '\n' );
}

这段代码实际上是有效的。但我无法解释它是如何工作的。因为在 while 语句中,我们使用了 getchar() != '\n',这意味着读取除了 '\n' 以外的任何单个字符?如果是这样,那么 '\n' 字符是否仍然留在输入缓冲区中?


另请参见:[我无法刷新标准输入。 我怎样才能在C中刷新stdin?] (https://dev59.com/O3I95IYBdhLWcg3wtwVe) - Gabriel Staples
1
要明确的是,“Enter键”不是一个字符。你所说的“Enter键”实际上是回车符(\r)。当你按下Enter键时,根据你的系统,它可能会发出\r(旧版Mac系统),\r\n(Windows)或\n(几乎所有其他操作系统,包括OSX/macOS)。已经有答案解释了C运行时如何在文本模式流中将Windows的\r\n折叠成\n - ShadowRanger
20个回答

127
由于在第1行,当用户按Enter键时,它将在输入缓冲区中留下2个字符:Enter键(ASCII码13)和\n(ASCII码10),所以程序无法正常工作。因此,在第2行,它将读取\n并且不会等待用户输入字符。
你在第2行看到的行为是正确的,但这并不是完全正确的解释。对于文本模式流,您的平台使用什么行尾(回车符(0x0D)+换行符(0x0A),裸CR或裸LF)并不重要。C运行时库将为您处理:您的程序将只看到'\n'表示换行符。
如果您输入一个字符并按Enter键,则该输入字符将被第1行读取,然后第2行将读取'\n'。请参阅comp.lang.c FAQ中的我正在使用scanf %c来读取Y / N响应,但稍后的输入被跳过。 至于提出的解决方案,请参见(再次从comp.lang.c FAQ中):

基本上意味着唯一可移植的方法是执行:

int c;
while ((c = getchar()) != '\n' && c != EOF) { }

您的getchar() != '\n'循环有效是因为一旦调用getchar(),返回的字符已经被从输入流中删除。

此外,我感到有义务劝阻您完全不要使用scanf为什么每个人都说不要使用scanf?我应该使用什么代替?


1
这将会挂起等待用户按下回车键。如果你只想简单地清除标准输入,可以直接使用termios。https://dev59.com/o2Qm5IYBdhLWcg3w0Bq5#23885008 - ishmael
1
@ishmael 你的评论没有意义,因为按下 Enter 键并不是必需的来清空标准输入(stdin);而是必需的来获取输入。这也是在 C 中读取输入的唯一标准方式。(如果你想立即读取按键输入,那么你将需要使用非标准库。) - jamesdlin
2
@jamesdlin 在Linux上,getchar()会等待用户按下Enter键。只有在此之后,它们才会跳出while循环。据我所知,termios是一个标准库。 - ishmael
2
@ishmael 不,你两点都错了。这个问题是关于如何在读取输入后清除stdin中剩余的输入,在标准C中,提供该输入需要先键入一行并按Enter键。在输入该行之后,getchar()将读取并返回这些字符;用户不需要再次按Enter键。此外,虽然termios可能在Linux上很常见,但它不是C标准库的一部分。 - jamesdlin
使用 scanf 本身并不是一个好的实践。使用 getline/fgetssscanf 更好。scanf 函数族中空格折叠行为是否可取,取决于您的情况。 - jamesdlin
显示剩余2条评论

53

你也可以这样做:

fseek(stdin,0,SEEK_END);

哇... 顺便问一下,标准是否表示 fseek 可用于 stdin - ikh
4
我不确定,我认为它没有提到,但是stdin是FILE类型,而且fseek接受一个FILE参数。我测试了一下,在Mac OS X上可以工作,但在Linux上不行。 - Ramy Al Zuhouri
请解释一下。如果作者能写出这种方法的影响,那将是一个很好的答案。是否存在任何问题? - EvAlex
12
@EvAlex:这个有很多问题。如果在你的系统上可以运行,那就很好;但是如果不能运行,那也不奇怪,因为没有任何保证它会在标准输入是交互设备(或者是像管道、套接字或 FIFO 这样的非可寻址设备)时正常工作(还有其他一些可能导致它失败的方式)。 - Jonathan Leffler
在Windows上运行得非常好!比其他人建议的使用循环清除键盘缓冲区的方法更加干净和高效。非常感谢! - Anurag S Sharma
显示剩余4条评论

19

清除已经尝试部分读取的行直到结尾的便携式方法是:

int c;

while ( (c = getchar()) != '\n' && c != EOF ) { }

该函数读取并丢弃字符,直到遇到表示文件结尾的\n。如果输入流在行末被关闭,它还会检查EOF。变量c的类型必须是int(或更大)才能容纳EOF的值。

没有可移植的方法来确定当前行后面是否有更多行(如果没有,则getchar将阻塞输入)。


为什么 while((c=getchar())!=EOF); 不起作用?它是一个无限循环。我无法理解 EOF 在 stdin 中的位置。 - Rajesh
1
@Rajesh 如果还有更多的输入要来,那么它将一直保持清晰,直到输入流被关闭。 - M.M
@Rajesh 是的,你说得对。如果在调用此函数时stdin上没有要刷新的内容,则会由于被卡在无限循环中而阻塞(挂起)。 - Jason Enochs
2
@AbhishekMane 但是一个字节是8位,而256需要9位。如果将getchar()的结果分配给char,则无法处理任何8位值以及一个附加的标志值。此外,stdio.h#define EOF (-1)。C语言有char、signed char、unsigned char,char可能在0..255或-128..127之间,具体取决于架构。man getchar非常明确:“fgetc()从流中读取下一个字符并将其作为无符号字符转换为int返回,或在文件结束或错误时返回EOF。”因此(int) -1或(int) 0..255。将其塞入char变量中会丢失重要信息。 - Paul_Pedant
1
@Paul_Pedant明白了,谢谢。EOF不是字符。getchar()返回的是char(0,1,2,...255) + EOF。为了处理这个问题,getchar(_)只能返回int类型。所以C必须是int类型,对吧? - Abhishek Mane
显示剩余3条评论

13

这几行代码:

int ch;
while ((ch = getchar()) != '\n' && ch != EOF)
    ;

它不仅读取换行符('\n')之前的字符,而是读取流中的所有字符(并且丢弃它们),直到遇到下一个换行符或EOF为止。 为了使测试结果为真,必须先读取换行符;所以当循环停止时,换行符是最后读取的字符,但它已经被读取了。

至于为什么会读取换行符而不是回车符,这是因为系统已将回车符转换为换行符。 当按下Enter键时,表示该行结束...但流包含一个换行符,因为这是该系统的正常行末标记。 这可能与平台有关。

此外,在输入流上使用 fflush() 并不适用于所有平台;例如在Linux上通常无效。


注意:如果getchar()由于文件结束而返回EOF,则while(getchar() != '\n');是一个无限循环。 - chux - Reinstate Monica
为了检查EOF,可以使用int ch; while ((ch = getchar()) != '\n' && ch != EOF); - Dmitri

10
但是我不明白它是如何工作的?因为在while语句中,我们使用了getchar() != '\n',这意味着读取除'\n'以外的任何单个字符?? 那么输入缓冲区中仍然保留'\n'字符吗?
你可能没有意识到的一点是,在比较之前getchar()会从输入缓冲区中移除该字符。因此当你达到'\n'时,它被消耗掉,然后你跳出循环。

8

scanf 是一个奇怪的函数,电影《全面启动》(WarGames)中有一句经典台词与之相关:“唯一的获胜策略是不参与游戏”。

如果你发现自己需要“清空输入”,那么你已经输了。获胜的策略不是拼命寻找某种神奇的方式来清空令人恼火的输入,而是用一种不同(更好)的方式进行输入,这种方式不涉及在输入流上留下未读取的输入,并使其停留在那里并引起问题,从而不得不尝试清空它。

基本上有三种情况:

  1. 您正在使用 scanf 读取输入,但它会在输入流中保留用户的换行符,因此该残留的换行符会被后续对 getcharfgets 的调用错误读取。 (这就是您最初提出的情况。)

  2. 您正在使用 scanf 读取输入,但它会在输入流中保留用户的换行符,因此该残留的换行符会被后续对 scanf("%c") 的调用错误读取。

  3. 您正在使用 scanf 读取数字输入,但用户正在键入非数字文本,而非数字文本会留在输入流中,这意味着下一次对 scanf 的调用也会失败。

在这三种情况下,似乎正确的做法是“清除”有问题的输入。您可以尝试这样做,但最好的情况下这很繁琐,最坏的情况下根本无法做到。 最终我相信,尝试清除输入是错误的方法,并且有更好的方法,具体取决于您担心的情况:

在情况1下,更好的解决方案是不要混合使用scanf和其他输入函数。要么全部使用scanf进行输入,要么全部使用getchar和/或fgets进行输入。要想全部使用scanf进行输入,可以将对getchar的调用替换为scanf("%c") ——但请参见第2点。理论上,可以将对fgets的调用替换为scanf("%[^\n]%*c"),但这会带来各种进一步的问题,我不建议这样做。即使您需要使用scanf的解析功能,也可以使用fgets读取行,然后在事后使用sscanf进行解析。
在第二种情况下,更好的解决方案是,永远不要使用scanf("%c")。改用scanf(" %c")即可。这个神奇的额外空格使所有的差别都消失了。(为什么额外的空格有帮助和它做了什么有一个很长的解释,但超出了本答案的范围。)
在第三种情况下,恐怕没有好的解决方案。scanf存在许多问题,其中一个问题是其错误处理非常糟糕。如果您想编写一个简单的程序来读取数字输入,并且可以假设用户在提示时始终输入正确的数字输入,那么scanf("%d")可以是一个足够 - 仅仅足够 - 的输入方法。但也许您的目标是做得更好。也许您想提示用户输入一些数字,并检查用户是否确实输入了数字,如果没有,则打印错误消息并要求用户重试。在这种情况下,我认为基于scanf您不能达到此目标。您可以尝试,但这就像给一个扭动的婴儿穿上连体衣:当您试图将第二只手臂放入时,两条腿和一只手臂已经扭动出来了。使用scanf尝试读取和验证可能不正确的数字输入太费力了。使用其他方式实现要容易得多。
你会发现我列出的这三种情况都以 "您正在使用scanf读取输入..." 开头,这里有一个不可避免的结论。请参见这个问题:除了scanf,我还能用什么进行输入转换? 现在,我意识到我仍然没有回答你实际提出的问题。当人们问:“我怎么做X?”时,如果所有的答案都是“你不应该想要做X”,那真的很令人沮丧。如果你真的非常想清空输入,那么除了这里其他人给你的答案之外,还有两个好问题,其中包含了相关的答案:

6

你可以尝试

scanf("%c%*c", &ch1);

%*c可以接受并忽略换行符。

还有一种方法

不要使用fflush(stdin),因为它会导致未定义的行为,你可以这样写:

while((getchar()) != '\n');

不要忘记在 while 循环后加上分号。

4
  1. "%*c"扫描任何字符(但不保存),无论是换行符还是其他字符。代码依赖于第二个字符是一个换行符。
  2. while((getchar())!='\n');是一个无限循环,如果由于文件结束而返回EOF,则应该退出循环。
- chux - Reinstate Monica
1
第二种方法取决于条件,如换行符、空字符、EOF等(上面是换行符)。 - kapil

4

我很惊讶没有人提到这一点:

scanf("%*[^\n]");

2

如何在C语言中清空或刷新stdin输入缓冲区?

cppreference.com社区维基上fflush()参考文献(重点在加粗字体)声明:

对于输入流(以及上一次操作为输入的更新流),行为未定义。

因此不要对stdin使用fflush()

如果您的目标是“刷新”stdin以删除所有在stdin缓冲区中等待的字符,则最佳方法是手动使用getchar()getc(stdin)(相同的内容),或者使用read()(将stdin用作第一个参数)(如果使用POSIX或Linux)。 此处此处的投票最高的答案都是这样做的:
int c;
while ((c = getchar()) != '\n' && c != EOF);

我认为一个更清晰(更易读)的方法是这样做。当然,我的评论使我的方法看起来比实际上要长得多:

/// Clear the stdin input stream by reading and discarding all incoming chars up
/// to and including the Enter key's newline ('\n') char. Once we hit the
/// newline char, stop calling `getc()`, as calls to `getc()` beyond that will
/// block again, waiting for more user input.
/// - I copied this function
///   from "eRCaGuy_hello_world/c/read_stdin_getc_until_only_enter_key.c".
void clear_stdin()
{
    // keep reading 1 more char as long as the end of the stream, indicated by
    // `EOF` (end of file), and the end of the line, indicated by the newline
    // char inserted into the stream when you pressed Enter, have NOT been
    // reached
    while (true)
    {
        int c = getc(stdin);
        if (c == EOF || c == '\n')
        {
            break;
        }
    }
}

我在这里的两个文件中使用此函数,例如。请查看这些文件中的上下文,以了解何时清除 stdin 可能最有用:

  1. array_2d_fill_from_user_input_scanf_and_getc.c
  2. read_stdin_getc_until_only_enter_key.c

个人备忘录:我最初在这里发布了这个答案,但是后来删除了那个答案,只留下这一个作为我的唯一答案。


1
我在尝试实现解决方案时遇到了问题。
while ((c = getchar()) != '\n' && c != EOF) { }

我为可能遇到相同问题的人发布了一个小调整的“Code B”。

问题出在程序一直捕捉'\n'字符,而不管回车字符如何,以下是让我遇到问题的代码。

Code A

int y;

printf("\nGive any alphabetic character in lowercase: ");
while( (y = getchar()) != '\n' && y != EOF){
   continue;
}
printf("\n%c\n", toupper(y));

调整是为了在while循环中评估条件之前“捕获”(n-1)个字符,以下是代码:
代码B
int y, x;

printf("\nGive any alphabetic character in lowercase: ");
while( (y = getchar()) != '\n' && y != EOF){
   x = y;
}
printf("\n%c\n", toupper(x));

可能的解释是,为了使while循环中断,必须将值“\n”赋给变量y,因此它将是最后一个分配的值。
如果我在解释、代码A或代码B方面有所遗漏,请告诉我,我在C语言方面还很新。
希望能对某些人有所帮助。

你应该在 while 循环内处理你的 "expected" 字符。循环结束后,你可以认为 stdinclean/clear 的,而 y 将保存 '\n' 或 EOF。当没有换行符存在且输入会在按下 [ENTER] 之前溢出缓冲区时,将返回 EOF。*-- 你可以有效地使用 while((ch=getchar())!='\n'&&ch!=EOF); 来消耗 stdin 输入缓冲区中的每个字符。* - veganaiZe

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