prev up next   top/contents search

comp.lang.c FAQ 列表· 问题 19.1

如何在不等待回车键的情况下,从键盘读取单个字符? 如何阻止字符在键入时回显到屏幕上?


遗憾的是,在 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 的不同变体之间),答案也可能不同; 在回答特定于系统的问题时,请记住适用于您的系统的答案可能不适用于其他人的系统。

但是,由于这些问题经常在此处被问到,因此这里提供了一些常见情况的简短答案。

根据您使用的操作系统以及您可用的库,您或许可以使用以下一种(或多种!)技术

(顺便说一句,请注意,简单地使用setbufsetvbufstdin设置为非缓冲,通常不会允许一次一个字符的输入。)

如果您更改了终端模式,请保存初始状态的副本,并确保无论您的程序如何终止,都将其恢复。

如果您正在尝试编写一个可移植的程序,一个好的方法是定义您自己的一组三个函数,以(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


prev up next   contents search
关于此 FAQ 列表   关于 Eskimo   搜索   反馈   版权

Eskimo North 托管