如何避免使用getchar()读取单个字符时误按回车键?

94

在下面的代码中:

#include <stdio.h>

int main(void) {   
  int c;   
  while ((c=getchar())!= EOF)      
    putchar(c); 
  return 0;
}

我必须按下Enter键才能使用getchar打印出我输入的所有字母,但我不想这样做,我想要的是按下字母后立即看到我输入的字母重复显示而无需按下Enter键。例如,如果我按下字母'a',我希望在它旁边看到另一个'a',以此类推:

aabbccddeeff.....

但是当我按下'a'时,没有任何反应,只有在我按下Enter键时才会出现拷贝内容:

abcdef
abcdef

我该如何做到这点?

我在Ubuntu下使用cc -o example example.c命令进行编译。


6
这很大程度上取决于您的操作系统和终端如何处理输入。如果您指定操作环境,可能会得到更好的答案。 - goldPseudo
http://www.cplusplus.com/forum/articles/19975/ 提供的答案适用于使用WinAPI的Windows系统。 - user1202095
5
https://dev59.com/MnRC5IYBdhLWcg3wD83r - jwaliszko
参见:C语言非阻塞键盘输入 - Gabriel Staples
14个回答

119

这取决于你的操作系统,如果你在类Unix环境中,则ICANON标志默认启用,因此输入会被缓冲直到下一个'\n'EOF。通过禁用规范模式,您将立即获得字符。这在其他平台上也可能可行,但没有简单的跨平台解决方案。

编辑:我看到你指定了使用Ubuntu。昨天我刚发布了类似的内容,但请注意,这将禁用终端的许多默认行为。

#include<stdio.h>
#include <termios.h>            //termios, TCSANOW, ECHO, ICANON
#include <unistd.h>     //STDIN_FILENO


int main(void){   
    int c;   
    static struct termios oldt, newt;

    /*tcgetattr gets the parameters of the current terminal
    STDIN_FILENO will tell tcgetattr that it should write the settings
    of stdin to oldt*/
    tcgetattr( STDIN_FILENO, &oldt);
    /*now the settings will be copied*/
    newt = oldt;

    /*ICANON normally takes care that one line at a time will be processed
    that means it will return if it sees a "\n" or an EOF or an EOL*/
    newt.c_lflag &= ~(ICANON);          

    /*Those new settings will be set to STDIN
    TCSANOW tells tcsetattr to change attributes immediately. */
    tcsetattr( STDIN_FILENO, TCSANOW, &newt);

    /*This is your part:
    I choose 'e' to end input. Notice that EOF is also turned off
    in the non-canonical mode*/
    while((c=getchar())!= 'e')      
        putchar(c);                 

    /*restore the old settings*/
    tcsetattr( STDIN_FILENO, TCSANOW, &oldt);


    return 0;
}

你会注意到,每个字符都出现了两次。这是因为输入立即回显到终端,然后你的程序也使用putchar()将其放回去了。如果你想将输入与输出分离,你还需要关闭ECHO标志。你可以通过简单地将相应的代码行更改为以下内容来实现:

newt.c_lflag &= ~(ICANON | ECHO); 

13
+1 尽管说实话,这个代码水平可能不是原贴作者舒适的水平 ;) - Andomar
3
太好了!这正是我在寻找的! - Denilson Sá Maia
感谢您的帮助和详细的评论,这解决了我的问题。 - kaminsknator
难以置信,如此基础的东西竟然如此难以实现且不可移植。 - Pedro77

76

在Linux系统上,您可以使用stty命令修改终端行为。默认情况下,终端将缓冲所有信息直到按下Enter键后才将其发送给C程序。

一个快速、简单但不太具有可移植性的示例,可从程序内部更改行为:

#include<stdio.h>
#include<stdlib.h>

int main(void){
  int c;
  /* use system call to make terminal send all keystrokes directly to stdin */
  system ("/bin/stty raw");
  while((c=getchar())!= '.') {
    /* type a period to break out of the loop, since CTRL-D won't work raw */
    putchar(c);
  }
  /* use system call to set terminal behaviour to more normal behaviour */
  system ("/bin/stty cooked");
  return 0;
}

请注意,这并不是最优解,因为它只是假设stty cooked是程序退出时你想要的行为,而没有检查原始终端设置。此外,由于在原始模式下跳过了所有特殊处理,许多按键序列(如CTRL-CCTRL-D)在不显式处理程序的情况下实际上不会按照你的预期工作。
您可以通过man stty获得更多控制终端行为的方式,具体取决于您想要实现的功能。

4
使用 system 是个不好的主意。 - Sapphire_Brick
太多的原始代码...我正在寻找一个解决方案,使我不必等待回车键。这个解决方案破坏了所有输入终端的功能。 - ABC

14

getchar()是一个标准函数,在许多平台上需要按下“ENTER”键才能获得输入,因为平台会缓冲输入直到按下该键。许多编译器/平台支持非标准的getch()函数,它不关心ENTER(绕过平台缓冲,将ENTER视为普通键)。


1
+1 正确,getchar() 只有在你按下回车键后才开始逐个读取字符。 - Andomar
34
getchar() 不关心回车键,它只处理从标准输入流中传来的任何内容。行缓冲往往是由操作系统/终端定义的行为。 - goldPseudo
1
@goldPseudo:没错,但几乎所有平台都会缓冲输入,所以在大多数实际情况下,您将看到这种行为。如果支持getch(),它将管理或忽略缓冲。我知道一些旧的DOS实现绕过操作系统缓冲区直接与硬件交互。其他实现可能会刷新stdin以获得相同的行为。 - Eric J.
11
然而,导致你需要按回车的并不是getchar()函数本身,而是所处的环境。 - Benji XVI
1
非常简洁易懂。其他排名靠前的答案只是抛出了压倒性的不必要的技术细节。 - mzoz
如果输入缓冲区在getchar()之外的基础知识是“不必要的技术细节”,那么您可能不应该编写C代码。这个答案是通常观察到的行为的一个很好的总结,但当问题是如何避免使用getchar()时需要按下“ENTER”键时,输入缓冲区的具体细节则一点也不“不必要” - 实际上,这些细节是非常必要的。 - Andrew Henle

6

我喜欢Lucas的回答,但我想进一步阐述一下。在termios.h中有一个名为cfmakeraw()的内置函数,man将其描述为:

cfmakeraw() sets the terminal to something like the "raw" mode of the
old Version 7 terminal driver: input is available character by
character, echoing is disabled, and all special processing of
terminal input and output characters is disabled. [...]

这基本上与Lucas建议的相同,甚至更多,您可以在手册页中查看它设置的确切标志:termios(3)

用例

int c = 0;
static struct termios oldTermios, newTermios;

tcgetattr(STDIN_FILENO, &oldTermios);
newTermios = oldTermios;

cfmakeraw(&newTermios);

tcsetattr(STDIN_FILENO, TCSANOW, &newTermios);
c = getchar();
tcsetattr(STDIN_FILENO, TCSANOW, &oldTermios);

switch (c) {
    case 113: // q
        printf("\n\n");
        exit(0);
        break;
    case 105: // i
        printf("insert\n");
        break;
    default:
        break;

1
请注意,cfmakeraw()POSIX termios.h 的非便携、非标准扩展,并不一定可用。 - Andrew Henle

6

输入/输出(I/O)是操作系统的一项功能。在许多情况下,操作系统只有在按下“回车”键后才会将键入的字符传递给程序。这使得用户能够在将其发送到程序之前修改输入(例如退格和重打)。对于大多数目的来说,这很好用,为用户提供了一致的界面,并使程序免于处理这些细节。但在某些情况下,程序需要在按下键时立即获取字符。

C库本身处理文件,并不关心数据如何进入输入文件。因此,语言本身没有办法获取按键。而是与平台相关的。由于您没有指定操作系统或编译器,我们无法为您查找。

此外,标准输出通常为了效率而被缓存。这是通过C库实现的,因此有一个C的解决方法,那就是在每个字符写入后加上fflush(stdout);。之后,字符是否立即显示取决于操作系统,但我熟悉的所有操作系统都会立即显示输出,所以通常不是问题。


5

既然您正在使用Unix衍生版(Ubuntu),这里有一种方法可以做到这一点——虽然不建议,但它可以工作(只要您能准确输入命令):

echo "stty -g $(stty -g)" > restore-sanity
stty cbreak
./your_program

当你对程序感到厌倦时,可以使用中断来停止程序。

sh restore-sanity
  • 'echo'命令将当前终端设置保存为一个脚本,以便恢复它们。
  • 'stty'命令关闭大部分特殊处理(例如,Control-D没有任何效果),并在字符可用时立即将字符发送给程序。这意味着您不能再编辑您的输入。
  • 'sh'命令还原您原来的终端设置。

如果“stty sane”可以准确地恢复您的设置,则可以节省空间。'-g'的格式在不同版本的'stty'中不可移植(因此,在Solaris 10上生成的内容在Linux上无法使用,反之亦然),但是该概念在任何地方都适用。“stty sane”选项在我所知道的情况下不是普遍可用的(但在Linux上是可用的)。


3
你可以包含 'ncurses' 库,并使用 getch() 代替 getchar()

3

"如何避免使用 getchar() 时按下 Enter 键?"

首先,终端输入通常是行缓冲或全缓冲。这意味着操作系统将来自终端的实际输入存储在缓冲区中。通常,当在 stdin 中提供例如 \n 时,该缓冲区会被刷新到程序中。这通常是通过按下 Enter 键来完成的。

getchar() 只是在链的末端。它没有能力实际影响缓冲过程。


"我该怎么做?"

首先放弃使用getchar(),如果你不想使用特定的系统调用来显式更改终端行为,就像其他答案中很好地解释的那样。

不幸的是,没有标准库函数可以在单个字符输入时清除缓冲区,因此也没有可移植的方法。但是,有一些基于实现和不可移植的解决方案。


在Windows/MS-DOS中,conio.h头文件中有getch()getche()函数,它们可以做你想要的事情——读取单个字符而无需等待换行符刷新缓冲区。请注意保留HTML标签。 getch()getche()的主要区别在于,getch()不会立即在控制台中输出实际输入的字符,而getche()会。额外的"e"代表回显。
#include <stdio.h>
#include <conio.h>   

int main (void) 
{
    int c;

    while ((c = getche()) != EOF)
    {
        if (c == '\n')
        {
            break;
        }

        printf("\n");
    }

    return 0;
}

在Linux中,获得直接字符处理和输出的方法是使用ncurses库中的cbreak()echo()选项以及getch()refresh()例程。
请注意,您需要使用initscr()初始化所谓的标准屏幕,并使用endwin()例程关闭它。
示例:
#include <stdio.h>
#include <ncurses.h>   

int main (void) 
{
    int c;

    cbreak(); 
    echo();

    initscr();

    while ((c = getch()) != ERR)
    {
        if (c == '\n')
        {
            break;
        }

        printf("\n");
        refresh();
    }

    endwin();

    return 0;
}

注意:你需要使用-lncurses选项调用编译器,以便链接器可以搜索并找到ncurses库。

1

是的,你也可以在 Windows 上这样做,以下是代码,使用 conio.h 库

#include <iostream> //basic input/output
#include <conio.h>  //provides non standard getch() function
using namespace std;

int main()
{  
  cout << "Password: ";  
  string pass;
  while(true)
  {
             char ch = getch();    

             if(ch=='\r'){  //when a carriage return is found [enter] key
             cout << endl << "Your password is: " << pass <<endl; 
             break;
             }

             pass+=ch;             
             cout << "*";
             }
  getch();
  return 0;
}

6
问题标记为[tag:c],而不是[tag:c++]。 - Spikatrix
@CoolGuy 那又怎样?如果有人打开完全相同的问题,但将标签从c更改为c ++,这会对任何人有所帮助吗? - Andreas Haferburg
@AndreasHaferburg 因为我们不会在这里找到C++的答案,尤其是那些写得很糟糕的,_眨眼_。 - Sapphire_Brick

1

我在目前正在处理的一个任务中遇到了这个问题/疑问。 这也取决于你从哪里获取输入。 我正在使用

/dev/tty

在程序运行时获取输入,因此需要与命令相关联的文件流。

在我要测试/目标的Ubuntu机器上,需要更多操作。

system( "stty -raw" );

或者

system( "stty -icanon" );

我不得不添加 --file 标志,以及命令的路径,就像这样:

system( "/bin/stty --file=/dev/tty -icanon" );

现在一切都很好。


2
请注意,此解决方案依赖于操作系统。 - Ottavio Campana

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