C语言 - 如何在while循环中处理用户输入

3

我是C语言的新手,我有一个简单的程序,在while循环内获取用户输入,如果用户按下'q'键则退出:

while(1)
{
   printf("Please enter a choice: \n1)quit\n2)Something");
   *choice = getc(stdin);

   // Actions.
   if (*choice == 'q') break;
   if (*choice == '2') printf("Hi\n");
}

当我运行这个程序并按下 'q' 时,程序确实正确退出。但是如果我按下 '2',程序首先会打印出 "Hi"(正如它应该做的),然后继续打印两次提示 "Please choose an option"。如果我输入 N 个字符并按下回车键,提示将被打印 N 次。
当我使用带有 2 限制的 fgets() 时,也会发生同样的情况。
如何使这个循环正常工作?它应该只接受输入的第一个字符,然后根据输入执行一次操作。
编辑:
因此,使用更大的缓冲区与 fgets() 一起使用可以解决重复提示的问题:
fgets(choice, 80, stdin);

这有点帮助:如何在C中清除输入缓冲区?

1
if (*choice == '2') printf("Hi\n"); else printf("Oops: %x\n", *choice & 0xff ); - wildplasser
1
我可能会使用fgets和一个更大的缓冲区,比如80个字符左右。或者在那里添加另一个循环,读取直到下一个换行符。 - Fred Larson
3
为什么不改成while(choice != 'q'),而不是while(1)呢?然后把第一个if语句完全删除。 - urnotsam
1
谷歌清除输入缓冲区(***不要使用fflush***),在脏缓冲区上无法可靠地使用getc - Elias Van Ootegem
1
@Dylan 当你按下 2<enter> 时,数字 2 和一个换行符都会被排队读取到 stdin 中。因此,你的 getc 循环会先消耗掉数字 2,然后再运行一次以消耗掉换行符。这就产生了额外的提示,但由于你没有为 \n 编写 switch case,所以它只是这样做了。然后它最终再次打印提示并等待下一个输入。需要记住的主要思想是,getc 要求你自己管理输入缓冲区中的换行符,这可能很痛苦。使用 fgets 方法通常更简单。 - Gene
显示剩余3条评论
3个回答

3

当你使用getc获取输入时,重要的是要注意用户输入了多个字符:至少,stdin包含2个字符:

2\n

getc获取用户输入的“2”时,尾随的\n字符仍然留在缓冲区中,因此您需要清除它。在这里最简单的方法是添加以下内容:

if (*choice == '2')
    puts("Hi");
while (*choice != '\n' && *choice != EOF)//EOF just in case
    *choice = getc(stdin);

应该可以解决问题了

为了完整起见:
请注意,getc返回的是int而不是char。请确保使用-Wall -pedantic标志进行编译,并始终检查您使用的函数的返回类型。

使用fflush(stdin);清除输入缓冲区很诱人,在某些系统上,这将起作用。然而:此行为未定义:标准明确规定fflush仅用于更新/输出缓冲区,而不是输入缓冲区

C11 7.21.5.2 fflush函数,fflush仅适用于输出/更新流,而不适用于输入流

但是,一些实现(例如Microsoft)支持fflush(stdin);作为扩展。依靠它,虽然违背了C背后的哲学。C旨在具有可移植性,并通过遵循标准来确保代码具有可移植性。依赖特定的扩展会削弱这种优势。


还要注意getc()返回的是一个int,你应该测试是否为EOF并做出适当处理。 - Jonathan Leffler
@JonathanLeffler:这就是为什么我们使用“-Wall -pedantic”进行编译的原因...我会将其添加到我的答案中,因为我们正在尝试完整;-P - Elias Van Ootegem
感谢关于fflush()的额外说明。那个while循环解决了这个问题。 - IllegalCactus

2
表面上看似乎是一个非常简单的问题,但实际上相当复杂。问题的根源在于终端以两种不同的模式运行:原始模式和行模式。行模式是默认模式,表示终端不读取字符,而是读取整行。因此,除非输入整行(或接收到文件结束符),否则您的程序根本不会收到任何输入。终端识别行尾的方法是接收换行符(0x0A),这可以通过按下 Enter 键来引起。更令人困惑的是,在 Windows 机器上,按 Enter 键会生成两个字符(0x0D 和 0x0A)。
因此,您的基本问题是希望有一个单字符界面,但您的终端正处于面向行的(行模式)模式下。
正确的解决方案是将终端切换为原始模式,这样您的程序可以在用户输入时接收字符。此外,建议您在此使用 getchar() 而不是 getc()。区别在于 getc() 将文件描述符作为参数,因此它可以从任何流中读取。而 getchar() 函数仅从标准输入读取,这正是您想要的。因此,它是更具体的选择。在程序完成后,应将终端恢复到原来的状态,因此需要在修改之前保存当前终端状态。
此外,您应该处理终端接收到 EOF(0x04)的情况,用户可以通过按下 CTRL-D 来完成。
以下是执行这些操作的完整程序:
#include    <stdio.h>
#include    <termios.h>
main(){
    tty_mode(0);                /* save current terminal mode */
    set_terminal_raw();         /* set -icanon, -echo   */
    interact();                 /* interact with user */
    tty_mode(1);                /* restore terminal to the way it was */
    return 0;                   /* 0 means the program exited normally */
}

void interact(){
    while(1){
        printf( "\nPlease enter a choice: \n1)quit\n2)Something\n" );
        switch( getchar() ){
            case 'q': return;
            case '2': {
               printf( "Hi\n" );
               break;
            }
            case EOF: return;
        }
    }
}

/* put file descriptor 0 into chr-by-chr mode and noecho mode */
set_terminal_raw(){
    struct  termios ttystate;
    tcgetattr( 0, &ttystate);               /* read current setting */
    ttystate.c_lflag          &= ~ICANON;   /* no buffering     */
    ttystate.c_lflag          &= ~ECHO;     /* no echo either   */
    ttystate.c_cc[VMIN]        =  1;        /* get 1 char at a time */
    tcsetattr( 0 , TCSANOW, &ttystate);     /* install settings */
}

/* 0 => save current mode  1 => restore mode */
tty_mode( int operation ){
    static struct termios original_mode;
    if ( operation == 0 )
        tcgetattr( 0, &original_mode );
    else
        return tcsetattr( 0, TCSANOW, &original_mode ); 
}

正如您所看到的,这个似乎相当简单的问题实际上是非常棘手的。

我强烈推荐一本书来帮助您了解这些问题,它是Bruce Molay的《理解Unix/Linux编程》。第6章详细解释了以上所有内容。


谢谢!我看到在其他问题中提供了切换终端模式作为解决方案,但不确定如何操作。问题现已解决,但这是一个我以前不知道的有趣方面。 - IllegalCactus

1
这种情况发生的原因是stdin被缓存了。当你到达代码行*choice = getc(stdin);时,无论你输入多少个字符,getc(stdin)都只会检索到第一个字符。所以如果你输入"foo",它将检索到'f'并将*choice设置为'f'。字符"oo"仍然在输入缓冲区中。此外,由于按下回车键而产生的回车符也在输入缓冲区中。因此,由于缓冲区不为空,下一次循环执行时,getc(stdin);将立即返回缓冲区中的下一个字符,而不是等待你输入任何内容。函数getc(stdin)将继续立即返回缓冲区中的下一个字符,直到缓冲区为空。因此,通常情况下,当你输入长度为N的字符串时,它将提示你N次。
你可以通过在*choice = getc(stdin);后立即使用fflush(stdin);来清除缓冲区来解决这个问题。
编辑:显然,有人说不要使用fflush(stdin);请遵循他的建议。

1
我听说fflush()也应该避免使用,但感谢您对输入缓冲区的解释。现在更加清晰明了了。 - IllegalCactus
@Dylan:在我的答案中加入了为什么应该避免使用 fflush(stdin);。基本上:它的行为是未定义的,而未定义的行为是不好的。 - Elias Van Ootegem
有关更多信息请参见 使用 fflush(stdin)?。请注意,它在 Windows 上已定义并得到支持,并且(尽管手册页面上有一些证据相反)实际上不受其他地方的支持。 - Jonathan Leffler

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