什么是非阻塞输入
程序获取输入的方式有很多,如 scanf()、getchar()、getche()、getch() 等函数,这些函数在执行的时候,会等待用户进行输入,直到获取输入完毕才会执行后面的代码。如果用户一直不输入,那么程序就一直等待,函数后面的代码得不到执行,这就是阻塞输入。
非阻塞输入是指:用户输入数据后程序可以捕获,用户不输入数据程序也可以继续执行。
应用场景
初学者使用的一般是阻塞式输入,这种方式一般没有任何问题,用户输入完数据再执行后面的代码往往也符合逻辑。
然而在一些情境下面,程序并不想一直等待用户输入,比如在等待输入的同时要处理一些其他事情,或者用户未必会进行输入,或者用户输入时间根本无法确定等等。最典型的例子就是小游戏中,用键盘控制物体的运动,这种情况下一定不能用阻塞输入。
实现原理
在 Windows 系统中,conio.h头文件中的kbhit()函数就可以用来实现非阻塞式键盘监听。
conio.h 是 Windows 下特有的头文件,所以 kbhit() 也只适用于 Windows,不适用于 Linux 和 Mac OS。
用户每按下一个键,都会将对应的字符放到输入缓冲区中,kbhit() 函数会检测缓冲区中是否有数据,如果有的话就返回非 0 值,没有的话就返回 0 值。但是 kbhit() 不会读取数据,数据仍然留在缓冲区,所以一般情况下我们还要结合输入函数将缓冲区种的数据读出。有了kbhit()函数,就可以判断当前是否有键盘输入,如果有那就读取,没有就继续执行其他代码。
在Linux系统中,实现的原理也是一样的,等待一个极短的时间,如果有输入那就获取字符,没有则跳过继续执行。这个过程使用select函数来实现:把标准输入(文件描述符0)添加到待读取文件集合中,如果该集合中有文件可以读取,则select函数会返回一个大于零的值,此时就可以调用函数getchar()进行读取了。
源码分享
input.h 文件:
#ifndef INPUT_H
#define INPUT_H
#if _WIN32
#include <conio.h>
#elif __linux__
#include <termio.h>
#define TTY_PATH "/dev/tty"
#define STTY_US "stty raw -echo -F"
#define STTY_DEF "stty -raw echo -F"
#endif
#define BUFF_SIZE 32
int getInputStr(char inputStr[], int length);
#endif
input.c 文件:
#include "input.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
static char myGetChar(void);
int getInputStr(char inputStr[], int length)
{
static char s_inputBuff[BUFF_SIZE] = {0}; //输入缓冲区
static int s_inputLen = 0; //已获得输入的长度
int getLen = 0; //实际提交的长度
char inputChar = -1; //当前输入字符
if (length > BUFF_SIZE - 1)
{
memset(inputStr, 0, length);
printf("\r\n system error ! \r\n");
exit(-1); //严重错误
}
inputChar = myGetChar();
switch (inputChar)
{
case -1:
break;
case ' ':
case '\r':
case '\n':
getLen = s_inputLen; //提前获取
s_inputBuff[getLen] = '\0';
break;
default:
s_inputBuff[s_inputLen++] = inputChar;
break;
}
if (s_inputLen >= length)
{
getLen = length; //获得了指定长度
s_inputBuff[getLen] = '\0';
}
if (getLen > 0)
{
memcpy(inputStr, s_inputBuff, getLen + 1); //复制需要获取的长度和‘\0’
memset(s_inputBuff, 0, BUFF_SIZE);
s_inputLen = 0;
return getLen;
}
return 0;
}
static char myGetChar(void)
{
char ch = -1;
#if _WIN32
if (kbhit()) //检测缓冲区中是否有数据
{
ch = getche(); //将缓冲区中的数据以字符的形式读出
}
#elif __linux__
fd_set rfds;
struct timeval tv;
system(STTY_US TTY_PATH);
FD_ZERO(&rfds);
FD_SET(0, &rfds);
tv.tv_sec = 0;
tv.tv_usec = 10;
if (select(1, &rfds, NULL, NULL, &tv) > 0)
{
ch = getchar();
}
#endif
return ch;
}