问如何在不等待回车键的情况下,从键盘读取单个字符? 如何阻止字符在键入时回显到屏幕上?
答遗憾的是,在 C 语言中没有标准或可移植的方法来做到这些。标准中甚至没有提及屏幕和键盘等概念,它只处理简单的字符 I/O “流”。
计算机程序的输入通常要经过几个阶段。 在最低层,操作系统内部的设备相关例程处理与特定设备(如键盘、串行线、磁盘驱动器等)接口的细节。 在此之上,现代操作系统倾向于具有设备无关的 I/O 层,统一对任何文件或设备的访问。 最后,C 程序通常通过 stdio 库的可移植函数与操作系统的 I/O 设施隔离开来。
在某些层面,交互式键盘输入通常被收集起来,并一次一行地呈现给请求程序。 这让操作系统有机会以一致的方式支持输入行编辑(退格/删除/擦除等),而无需将其构建到每个程序中。 只有当用户满意并按下回车键(或等效键)时,该行才可供调用程序使用。 即使调用程序看起来正在一次读取一个字符(使用getchar等),第一次调用也会阻塞,直到用户键入完整的一行,此时可能会有许多字符变得可用,并且许多字符请求(例如getchar调用)将快速连续地得到满足。
当程序想要立即读取每个到达的字符时,它的行动方案将取决于行集合发生在输入流的哪个位置,以及如何禁用它。 在某些系统(例如 MS-DOS、VMS 在某些模式下)下,程序可以使用不同的或修改后的 OS 级别输入调用来绕过一次一行的输入处理。 在其他系统(例如 Unix、VMS 在其他模式下)下,负责串行输入的操作系统部分(通常称为“终端驱动程序”)必须置于一种关闭一次一行处理的模式,之后所有对常用输入例程(例如read, getchar等)的调用将立即返回字符。 最后,少数系统(特别是较旧的、面向批处理的大型机)在外围处理器中执行输入处理,这些处理器无法被告知执行一次一行输入以外的任何操作。
因此,当您需要执行一次一个字符的输入(或禁用键盘回显,这是一个类似的问题)时,您必须使用特定于您正在使用的系统的技术,假设它提供了一种。 由于 comp.lang.c 面向 C 语言定义支持的主题,因此通过参考特定于系统的新闻组(例如 comp.unix.questions 或 comp.os.msdos.programmer)以及这些组的 FAQ 列表,您通常会获得更好的答案。 请注意,即使在类似系统的变体之间(例如,在 Unix 的不同变体之间),答案也可能不同; 在回答特定于系统的问题时,请记住适用于您的系统的答案可能不适用于其他人的系统。
但是,由于这些问题经常在此处被问到,因此这里提供了一些常见情况的简短答案。
根据您使用的操作系统以及您可用的库,您或许可以使用以下一种(或多种!)技术
(顺便说一句,请注意,简单地使用setbuf或setvbuf将stdin设置为非缓冲,通常不会允许一次一个字符的输入。)
如果您更改了终端模式,请保存初始状态的副本,并确保无论您的程序如何终止,都将其恢复。
如果您正在尝试编写一个可移植的程序,一个好的方法是定义您自己的一组三个函数,以(1)将终端驱动程序或输入系统设置为一次一个字符的模式(如果需要),(2)获取字符,以及(3)在程序完成时将终端驱动程序返回到其初始状态。 (理想情况下,这样一组函数可能是 C 标准的一部分,总有一天。)
例如,这是一个微小的测试程序,它打印接下来的十个字符的十进制值,无需等待回车键。 它使用如上所述的三个函数编写,然后是 curses、经典 Unix、System V Unix 和 MS-DOS 的三个函数的实现。 (与此列表关联的在线存档包含更完整的功能集。)
#include <stdio.h> main() { int i; if(tty_break() != 0) return 1; for(i = 0; i < 10; i++) printf(" = %d\n", tty_getchar()); tty_fix(); return 0; }
三个函数的这个实现是针对 curses 的
#include <curses.h> int tty_break() { initscr(); cbreak(); return 0; } int tty_getchar() { return getch(); } int tty_fix() { endwin(); return 0; }
这是 “经典” (V7, BSD) Unix 的代码
#include <stdio.h> #include <sgtty.h> static struct sgttyb savemodes; static int havemodes = 0; int tty_break() { struct sgttyb modmodes; if(ioctl(fileno(stdin), TIOCGETP, &savemodes) < 0) return -1; havemodes = 1; modmodes = savemodes; modmodes.sg_flags |= CBREAK; return ioctl(fileno(stdin), TIOCSETN, &modmodes); } int tty_getchar() { return getchar(); } int tty_fix() { if(!havemodes) return 0; return ioctl(fileno(stdin), TIOCSETN, &savemodes); }
System V Unix 的代码类似
#include <stdio.h> #include <termio.h> static struct termio savemodes; static int havemodes = 0; int tty_break() { struct termio modmodes; if(ioctl(fileno(stdin), TCGETA, &savemodes) < 0) return -1; havemodes = 1; modmodes = savemodes; modmodes.c_lflag &= ~ICANON; modmodes.c_cc[VMIN] = 1; modmodes.c_cc[VTIME] = 0; return ioctl(fileno(stdin), TCSETAW, &modmodes); } int tty_getchar() { return getchar(); } int tty_fix() { if(!havemodes) return 0; return ioctl(fileno(stdin), TCSETAW, &savemodes); }
最后,这是 MS-DOS 的实现
int tty_break() { return 0; } int tty_getchar() { return getche(); } int tty_fix() { return 0; }
关闭回显留给读者作为练习。
有关终端(键盘和屏幕)I/O 编程的详细信息,请参阅特定于您的操作系统的 FAQ 列表、书籍或文档集。 (请注意,需要处理的细节可能还有很多,例如要禁用的特殊字符以及要切换的更多模式位,比上面提到的要多。)
另请参见问题 19.2。
其他链接: 更多解决方案
参考资料:PCS Sec. 10 pp. 128-9, Sec. 10.1 pp. 130-1
POSIX Sec. 7