【问题标题】:C - how to handle user input in a while loopC - 如何在while循环中处理用户输入
【发布时间】:2014-10-01 14:53:11
【问题描述】:

我是 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”(应该如此),然后继续打印提示“请选择一个选项”两次。如果我输入 N 个字符并按 Enter 键,则会打印 N 次提示。

当我使用限制为 2 的 fgets() 时,也会发生同样的行为。

如何让这个循环正常工作?它应该只取输入的第一个字符,然后根据输入的内容执行一次。

编辑

因此使用 fgets() 和更大的缓冲区可以工作,并停止重复提示问题:

fgets(choice, 80, stdin);

这种帮助:How to clear input buffer in C?

【问题讨论】:

  • if (*choice == '2') printf("Hi\n"); else printf("Oops: %x\n", *choice & 0xff );
  • 我可能会使用 fgets 和更大的缓冲区,比如 80 个字符左右。或者在其中插入另一个循环,直到下一个换行符。
  • 与其说while(1),不如直接说while(choice != 'q')。然后把第一个 if 语句全部去掉
  • google 清除输入缓冲区(虽然不要使用 fflush),你不能在脏缓冲区上可靠地使用 getc
  • @Dylan 当您按下2<enter> 时,2 和换行符都将在stdin 上排队等待阅读。因此,您的 getc 循环消耗 2,再次运行以消耗换行符。这会产生额外的提示,但由于您没有\n 的开关盒,因此仅此而已。然后它最终再次打印提示并等待下一个输入。要带走的主要思想是getc 要求您自己管理输入缓冲区中的换行符,这可能会很痛苦。 fgets 方法通常更简单。

标签: c


【解决方案1】:

当您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并适当地处理它。
  • @JonathanLeffler:这就是我们使用-Wall -pedantic 编译的原因...我将把它添加到我的答案中,因为我们正在努力完成;-P
  • 感谢 fflush() 的额外帮助。那个while循环解决了这个问题。
【解决方案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 的“Understanding Unix/Linux Programming”。第 6 章详细解释了上述所有内容。

【讨论】:

  • 谢谢!我看到切换终端模式作为其他问题的解决方案提供,但不知道如何去做。问题现已解决,但这是我以前不知道的一个有趣方面。
【解决方案3】:

发生这种情况的原因是因为标准输入被缓冲了。

当你到达代码行时 *choice = getc(stdin);无论您键入多少个字符,getc(stdin) 都只会检索第一个字符。因此,如果您键入“foo”,它将检索“f”并将 *choice 设置为“f”。字符“oo”仍在输入缓冲区中。此外,您按回车键产生的回车字符也在输入缓冲区中。因此,由于缓冲区不是空的,所以下次循环执行时,而不是等待你输入一些东西,getc(stdin);将立即返回缓冲区中的下一个字符。函数 getc(stdin) 将继续立即返回缓冲区中的下一个字符,直到缓冲区为空。因此,一般情况下,当你输入一个长度为 N 的字符串时,它会提示你 N 次。

您可以通过使用 fflush(stdin); 刷新缓冲区来解决此问题。紧接在 *choice = getc(stdin);

行之后

编辑:显然其他人说不要使用 fflush(stdin);照他说的去做。

【讨论】:

  • 我听说 fflush() 也应该避免使用,但感谢输入缓冲区的解释。现在更有意义了。
  • @Dylan:在我的回答中添加了为什么应该避免使用fflush(stdin);。基本上:它的行为是未定义的,未定义的行为是不好的
  • 请参阅Using fflush(stdin)? 了解更多信息。请注意,它在 Windows 上得到定义和支持,并且(尽管手册页中有一些相反的证据)在实践中其他地方不支持。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2017-01-27
  • 2013-11-16
  • 2020-02-15
  • 2017-08-29
  • 2015-12-31
  • 2022-07-09
  • 2019-08-03
相关资源
最近更新 更多