如果您的系统支持 POSIX.1-2001 中标准化的 termios,那么您可以操作标准输入终端以不缓冲您的输入。考虑以下示例:
#define _POSIX_C_SOURCE 200809L
#include <stdlib.h>
#include <unistd.h>
#include <termios.h>
#include <signal.h>
#include <string.h>
#include <stdio.h>
#include <errno.h>
/* SIGINT handler */
static volatile sig_atomic_t done = 0;
static void handle_done(int signum)
{
if (!done)
done = signum;
}
static int install_done(const int signum)
{
struct sigaction act;
memset(&act, 0, sizeof act);
sigemptyset(&act.sa_mask);
act.sa_handler = handle_done;
act.sa_flags = 0;
if (sigaction(signum, &act, NULL) == -1)
return errno;
return 0;
}
/* Reverting terminal back to original settings */
static struct termios terminal_config;
static void revert_terminal(void)
{
tcsetattr(STDIN_FILENO, TCSAFLUSH, &terminal_config);
}
int main(void)
{
int c;
/* Set up INT (Ctrl+C), TERM, and HUP signal handlers. */
if (install_done(SIGINT) ||
install_done(SIGTERM) ||
install_done(SIGHUP)) {
fprintf(stderr, "Cannot install signal handlers: %s.\n", strerror(errno));
return EXIT_FAILURE;
}
/* Make terminal input noncanonical; not line buffered. Also disable echo. */
if (isatty(STDIN_FILENO)) {
struct termios config;
if (tcgetattr(STDIN_FILENO, &terminal_config) == 0 &&
tcgetattr(STDIN_FILENO, &config) == 0) {
config.c_lflag &= ~(ICANON | ECHO);
config.c_cc[VMIN] = 1; /* Blocking input */
config.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &config);
atexit(revert_terminal);
}
}
/* Set standard input unbuffered. */
setvbuf(stdin, NULL, _IONBF, 0);
printf("Press Ctrl+C to exit.\n");
fflush(stdout);
while (!done) {
c = fgetc(stdin);
if (c == EOF)
printf("Read EOF%s\n", ferror(stdin) ? " as an error occurred" : "");
else
printf("Read %d = 0x%02x\n", c, (unsigned int)c);
fflush(stdout);
}
return EXIT_SUCCESS;
}
#define 行告诉您的 C 库头文件为基于 GNU 的系统公开 POSIX.1 功能。
每当接收到 INT (Ctrl+C)、TERM 或 HUP 信号时,都会设置 done 标志。 (如果您断开与终端的连接,例如关闭终端窗口,则会发送 HUP 信号。)
terminal_config 结构将包含原始终端设置,由注册为退出函数的revert_terminal() 使用,以将终端设置恢复为程序启动时读取的原始设置。
如果标准输入是终端,函数isatty(STDIN_FILENO) 返回 1。如果是这样,我们获取当前终端设置,并将它们修改为非规范模式,并要求每次读取都阻塞,直到至少读取一个字符。 (如果您设置了.c_cc[VMIN]=0 和.c_cc[VTIME]=0,I/O 函数往往会有点混乱,因此如果没有待处理的输入,fgetc() 返回 0。通常它看起来像 stdio.h I/O 的 EOF功能。)
接下来,我们告诉 C 库不要在内部缓冲标准输入,使用 setvbuf()。通常,C 库为标准输入使用输入缓冲区,以提高效率。但是,对我们来说,这意味着 C 库会缓冲输入的字符,而我们的程序在输入时可能不会立即看到它们。
同样,标准输出也被缓冲以提高效率。 C 库应该将所有完整的行刷新到实际的标准输出,但我们可以使用 fflush(stdout) 调用来确保我们写入 stdout 的所有内容都在此时刷新到实际的标准输出。
在main() 中,我们有一个简单的循环,它读取按键,并以十进制和十六进制打印它们。
请注意,当传递信号时,例如 INT 信号,因为您键入了 Ctrl+C,将信号传递到我们的handle_done() 信号处理程序会中断fgetc() 呼叫(如果有待处理)。这就是为什么当您按下 Ctrl+C 时会看到 Read EOF 的原因;如果你之后检查ferror(stdin),你会看到它返回非零(这表明发生了错误)。本例中的“错误”是EINTR,“被信号中断”。
还请注意,当您按下某些特定键时,例如光标或功能键,您会看到生成多个字符,通常以 27 和 91 开头(十进制;0x1B 十六进制 0x5B;"\033[" 如果表示为 C 字符串文字)。这些通常但不总是 ANSI 转义序列。一般来说,它们是特定于终端的代码,可以使用terminfo 数据库通过tigetstr()、tigetnum() 和tigetflag() 获得。
一种更便携的方法是使用 Curses 库;在大多数系统上是ncurses,在Windows 机器上是PDCurses。它们不仅提供了更简单的界面,而且还以特定于终端的方式来实现,以实现跨系统的最大兼容性。
使用 Curses 函数的 C 程序可以针对任何 Curses 库进行编译,因此可以在 Linux、Mac 和 Windows 机器上编译和运行相同的 C 源文件。但是,ncurses 确实包含相当多的扩展,这些扩展可能是其他 Curses 库(如 PDCurses)所不提供的。