/*不需要按回车键就能得到一个字符
MS-DOS程序员程序员在转到UNIX系统之后最先提出的一个问题就是“我如何在不按一下回车的情况下从终端读取一个字符?”在UNIX中,终端输入在缺省情况下是被“一锅端”的,也就是说整行输入是被一起处理的,这样行编辑器(backspace、delete等)可以不通过正在运行的程序就能发挥作用。这意外着在读入数据时必须按一下回车键表示输入行结束后才能得到输入的数据。
这个“一次输入一个字符”的特性对于许多种类的软件来说都是非常重要的,但对于PC语言却是小菜一碟。C函数库支持这个特性,通常使用一个称作kbhit()的函数,如果一个字符正在等待被读取,它就会发出提示。Micorsoft和Borland的C编译器提供了getch()(或getche(),它可以使字符在读取的同时回显到屏幕上)来获取单个字符,而不用等待整行结束。
为什么ANSI C不定义一个标准的函数来获取一次按键后的字符。由于没有一种标准的方法,每个系统都采用了不同的方法,这样便使程序失去了可移植性。反对将kbhit()纳入标准的人认为:它在绝大多数情况下是用于游戏软件的,而且还存在其他许多未标准化的终端I/O特性。另外,你可能并不想要一个在某些操作系统中很难实现的标准库函数。赞成它纳入标准的人则认为:它在绝大多数情况下用于游戏软件,而游戏编写者并不需要标准化的其他终端I/O的特性。
在UNIX中,有两种方法可以实现逐字符的输入,一种很难,另一种很容易。容易的方法就是让stty程序来实现这个功能。尽管它是一种间接实现的方法,但对程序而言并无大碍。
*/
//#include
//#include
//int main() {
// int c;
// /*终端驱动处于普通的一次一行模式*/
// system("stty raw");
// /*现在终端驱动处于一次一字符模式*/
// c = getchar();
// system("stty cooked");
// /*终端驱动又回到一次一行模式*/
//}
/*最后一行system("stty cooked");是必要的的,因为程序结束后,终端字符驱动特性的状态将延续下去,在程序把终端设为一种滑稽的模式之后,如果不做修改,它就是始终处于这种模式。这和设置环境变量不同,后者在进程结束后自动消失。
把I/O设置为raw状态可以实现阻塞式读入(blocking read),如果终端没有字符输入,进程就一直等待,直到有字符输入为止。如果需要非阻塞式读入,可以使用ioctl()(I/O控制) 系统调用。它提供一个针对终端特性的良好控制层,可以告诉你在SVr4系统下是否有一个键被按下。这样只有当一个字符被读入时进程才进行读取。这种类型的I/O被称为轮询,就好像不断地询问设备的状态,看看它是否有字符要传给你。
*/
//#include
//int kbhit() {
// int i;
// ioctl(0, FIONREAD, &i);
// return i; /*返回可以读取的字符的计数值*/
//}
//
//int main() {
// int i = 0;
// int c = ' ';
// system("stty raw -echo");
// printf("enter 'q' to quit\n");
// for (;c != 'q'; i++) {
// if (kbhit()) {
// c = getchar();
// printf("\n got %c, on iteration %d", c , i);
// }
// }
// system("stty cooked echo");
//}
调用库函数之后检查errno
每次在使用系统调用(如ioctl())之后,检查一下全局变量errno是一种好的做法,errno隶属于ANSI C标准。
如果一个库函数调用或系统调用遇到了问题,他将会设置errno的值以提示问题的原因。然而,只有当确实出现问题的时候,errno的值才是有效的---库函数或系统调用会使用某种方法来提示这一点(一般是通过它的返回值)。
一个典型的用法大致如下:
errno = 0;
if (ioctl(0, FIONREAD, &i) < 0) {
if (errno == EBADF) printf("errno: bad file number");
if (errno == EINVAL) printf("errno: invalid argument");
}
你可以把检查过程封装到一个单一的函数中,当调试程序时它会在每次系统调用之后自行调用。这个方法在隔离错误方面确实大有帮助。当你知道确有错误发生时,库函数perror()可以打印出错误信息。
curses函数库为它们提供了各种不同的可移植的程序。curses(令人联想到cursor[光标])是一个屏幕
管理调用函数库,在所有流行的平台上均得到实现,下面使用curses取代stty对上面的main函数进行改写:
#include
/*使用curses函数库和前面定义的kbhit()函数*/
int main() {
int c = ' ', i = 0;
initscr(); /*初始化curses函数*/
cbreak();
noecho(); /*按键时不在屏幕上回显字符*/
mvprintw(0, 0, "Press 'q' to quit\n");
refresh();
while (c != 'q') {
if (kbhit()) {
c = getch(); /*不会阻塞,因为我们知道有一个字符正在等待*/
mvprintw(1, 0, "got char '%c' on iteration %d\n", c, ++i);
refresh();
}
}
nocbreak();
echo();
endwin(); /*结束curses*/
}
用cc foo.c -lcurses命令进行编译。有一本叫做UNIX Cruses Explained的好书,它很好地描述了curses函数库。curses函数库只提供了基于字符的屏幕控制函数。与特定的位映射图形窗口化函数库相比,用curses函数库编写的软件在数量上要少得多,但用它编写的软件在可移植性方面却要强得多。
最后,还存在一种非轮询读取方式,每当操作系统准备好一些输入时,就会给你的进程发送一个信号。
如果程序使用了中断驱动的I/O,当它不处理输入时可以在main函数里执行一些其他的处理。如果输入比较零散且程序还有许多事物要处理,这是一种非常有效的资源使用方式。中断驱动程序要复杂得多,使他正常运转的难度也大得多,但它可以使进程更有效地使用CPU时间,而不是白白浪费时间一直等待输入。现在,随着线程的进一步使用,人们对中断驱动的I/O的使用日益减少。
2.信号处理程序应该读入一个字符,它每次被调用时都对自身进行重置。让它在屏幕上回显刚读取的字符,如果读入的字符是q就退出。注意,这仅适用于教学性质的程序。在实际工作中,如果在信号处理程序内调用了任何标准函数库的函数,其结果通常是未定义的。
异步I/O
下面的代码会是基于SVr4的操作系统为每个来自标准输入的字符发送一个中断。
#include
#include
#include
#include
#include
#include
int iteration = 0;
char crlf[] = {0xd, 0xa, 0};
void handler(int s) {
int c = getchar(); /*读入一个字符*/
printf("got char %c, at count %d %s", c, iteration, crlf);
if (c == 'q') {
system("stty sane");
exit(0);
}
}
int main() {
sigset(SIGPOLL, handler); /*建立处理程序*/
system("stty raw -echo");
ioctl(0, I_SETSIG, S_RDNORM); /*请求中断驱动的输入*/
for(;;iteration++) {
/*可以在这里进行一些其他的处理*/
}
}
使用sigset()而不是signal(),就不必再每次都重新注册信号处理程序。