如何防止scanf()无限等待输入字符?

18

我希望在控制台应用程序中实现以下功能:

  1. 如果用户输入一个字符,应用程序将执行相应的任务。例如,如果用户输入 1,则程序将执行任务 1;如果用户输入 q,则程序将退出;
  2. 如果用户没有输入任何内容,则程序将每 10 秒钟执行一次默认任务(时间不需要非常严格)。

这是我的代码:

#include <stdio.h>
#include <time.h>

char buff[64];
char command;

while(command != 'q')
{
   begin:
   printf(">> ");
   scanf("%s", buff);
   command = buff[0];

   switch (command)
   {
       case '1':
       // task 1 code will be added here;
          break;
       case '2':
       // task 2 code will be added here;
          break;
       case 'q':
          printf("quit the loop.\n");
          break;
   }

   // wait for 10 seconds;
   Sleep(10000);
   // default task code will be added here;

  if(command != 'q')
  {
     goto begin;
  }

}

但问题是,如果没有输入字符,程序将陷入scanf()函数的这一行并永远等待。所以我想知道如何在一定时间后跳过scanf()函数,我的意思是例如,如果1秒钟没有输入,程序可以继续执行,以此来实现上面列出的第二件事。

平台是Windows,如果有关系的话。

我已经删除了while()后面的分号,这是一个明显的错误。


2
我不认为有一个标准的方法来实现这个 - 即超时。你的目标平台是哪个? - cnicutar
1
你在哪个平台上?如果你在Linux上,你可以使用pollselect来查看STDIN_FILENO上是否发生了什么。 - hetepeperfan
跳转到开始?你正在测试while条件(它在“;”处结束,因此删除它),所以你不需要它(相反,使用“continue”),也不需要“if”。要做更多的“交互式”输入,可以查看ncurses库;有一些关于SO的文献可能会对您感兴趣,例如这里;也许您可以安排一些超时 - 但是当然,您的代码将取决于该库,该库不是标准库的一部分。 - ShinTakezou
可能是在C中实现TFTP的超时的重复问题。 - Lee Duhem
您尚未初始化“command”,因此第一个循环未定义。另外,请删除while循环末尾的“;”,否则您的程序将不会执行循环。 - phuclv
6个回答

13
尝试使用select()函数。这样你就可以等待10秒钟,直到你可以从stdin中读取而不会阻塞。如果select()返回零,则执行默认操作。我不知道这是否适用于Windows,因为它符合POSIX标准。如果你在Unix/Linux上进行开发,请尝试man select命令。
我刚写了一个使用select的可行示例:
#include <stdlib.h>
#include <stdio.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>

#define MAXBYTES 80

int main(int argc, char *argv[])
{
        fd_set readfds;
        int    num_readable;
        struct timeval tv;
        int    num_bytes;
        char   buf[MAXBYTES];
        int    fd_stdin;

        fd_stdin = fileno(stdin);

        while(1) {
                FD_ZERO(&readfds);
                FD_SET(fileno(stdin), &readfds);

                tv.tv_sec = 10;
                tv.tv_usec = 0;

                printf("Enter command: ");
                fflush(stdout);
                num_readable = select(fd_stdin + 1, &readfds, NULL, NULL, &tv);
                if (num_readable == -1) {
                        fprintf(stderr, "\nError in select : %s\n", strerror(errno));
                        exit(1);
                }
                if (num_readable == 0) {
                        printf("\nPerforming default action after 10 seconds\n");
                        break;  /* since I don't want to test forever */
                } else {
                        num_bytes = read(fd_stdin, buf, MAXBYTES);
                        if (num_bytes < 0) {
                                fprintf(stderr, "\nError on read : %s\n", strerror(errno));
                                exit(1);
                        }
                        /* process command, maybe by sscanf */
                        printf("\nRead %d bytes\n", num_bytes);
                        break; /* to terminate loop, since I don't process anything */
                }
        }

        return 0;
}

注意:下面的poll()示例也可以,没有问题。对于其余部分,我选择将可用字节读入缓冲区(最多MAXBYTES)。之后可以进行(s)扫描。 (scanf()不太适合我,但这是个人口味问题)。


这里有一个类似的问题,附带示例 - https://dev59.com/0lrUa4cB1Zd3GeqPmro1。 - Graeme
非常感谢Ronald和Graeme。然而,我尝试了Graeme建议的示例,但命令窗口将永远显示“无法读取您的输入”,并且两个显示之间没有延迟。因此,我想知道在配置select()函数及其初始化时是否应该注意其他事项,例如FD_ZEROFD_SET。非常感谢。 - user3208536
谢谢你的代码,Ronald。我刚试了一下,但是num_readable的值总是-1,我无法使用键盘输入任何内容。而且我找不到头文件“unistd.h”,所以我无法使用“read()”函数。我正在Windows 7上使用Microsoft Visual C++ 2010 Express。 - user3208536
好的,我没有注意到你使用的是Windows。我不是很擅长Windows,但是read()函数并没有做什么特别的事情。基本上,您需要一个从stdin读取而不会阻塞的函数,如果它无法读取适合其缓冲区的字符,则不会阻塞。在Windows中,我认为它被称为ReadFile()。您可能想要使用FormatMessage()而不是strerror()。 - Ronald
嗨,Ronald,很抱歉再次打扰你。问题是在“输入命令:”后,我无法输入任何字符,并且select()函数的返回值始终为-1,屏幕将显示:“select中的错误:没有错误”。你对这个问题有什么想法吗? - user3208536
np。一些谷歌搜索揭示了在Windows下,select()调用针对套接字。任何其他形式的异步I/O都使用ReadFile()函数,与npOverlapped参数结合使用。这意味着您将不得不使用Windows I/O接口。 但并非一切都已经失去。也许你很幸运。参数readfds将为所有可读文件设置位,可以通过if (FD_ISSET(fd_stdin, readfds)) ...进行测试,而不是返回值。这可能有效,也可能无效,但值得一试。 - Ronald

9

以下是一个使用 poll 实现的工作示例(可能是在 Linux 上最“正确”的方法):

#include <unistd.h>
#include <poll.h>
#include <stdio.h>

int main()
{
    struct pollfd mypoll = { STDIN_FILENO, POLLIN|POLLPRI };
    char string[10];

    if( poll(&mypoll, 1, 2000) )
    {
        scanf("%9s", string);
        printf("Read string - %s\n", string);
    }
    else
    {
        puts("Read nothing");
    }

    return 0;
}

超时是 poll 的第三个参数,单位是毫秒 - 以下示例将等待 2 秒钟以获得标准输入上的输入。Windows 有 WSAPoll,它应该工作方式类似。

2
WSAPoll仅适用于套接字。 - Matteo Italia

3
问题在于程序将陷入scanf()函数的行中,永远等待输入字符。 在while后面删除分号。

2
你绝对是对的关于分号。然而,如果用户没有输入任何内容,这如何解决程序在10秒后执行默认任务的问题? - hetepeperfan
1
这只是关于第一个问题的事情。 - haccks

2
很遗憾,您所要求的在普通的ISO C中是不可能实现的。然而,大多数平台都提供了特定于平台的扩展功能,可以提供您需要的功能。
由于您说明您的问题适用于Microsoft Windows,您正在寻找的两个函数是WaitForSingleObjectReadConsoleInput。您将无法使用函数scanf或C风格的流(FILE*)来实现此功能。
函数WaitForSingleObject允许您等待特定对象处于已发信号状态。它还允许您设置超时时间(在您的情况下应为10秒)。
一种 WaitForSingleObject 可以等待的对象类型是控制台句柄。当有新输入可供 ReadConsoleInput 读取时,它将处于已触发状态。根据我的测试,它无法与 ReadConsole 可靠地配合使用,因为即使 WaitForSingleObject 指示有等待输入,函数 ReadConsole 仍然会有时阻塞。这是因为与 ReadConsoleInput 相反,函数 ReadConsole 将过滤一些事件。因此,尝试使用 ReadConsole 读取一个事件实际上可能会尝试读取多个原始事件,如果没有未经过滤的原始事件可用,则会导致函数阻塞。函数 ReadConsoleInput 没有这个问题,因为它直接处理原始事件而不过滤任何事件。

这是我的程序,它使用了上述函数,并且正好做到了您的要求。

#define WIN32_LEAN_AND_MEAN

#include <Windows.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

void DoDefaultTask()
{
    printf( "Doing default task.\n" );
}

void DoTask1()
{
    printf( "Doing task #1.\n" );
}

void DoTask2()
{
    printf( "Doing task #2.\n" );
}

int main(void)
{
    INPUT_RECORD ir;
    HANDLE hConInput = GetStdHandle(STD_INPUT_HANDLE);
    DWORD dwReadCount;
    bool should_prompt = true, quit = false;

    while ( !quit )
    {
        //prompt user for input
        if ( should_prompt )
        {
            printf( "Please select an option: " );
            should_prompt = false;
        }

        //flush output
        fflush( stdout );

        switch ( WaitForSingleObject( hConInput, 10000 ) )
        {
        case WAIT_OBJECT_0:

            //attempt to read input
            if ( !ReadConsoleInput( hConInput, &ir, 1, &dwReadCount ) || dwReadCount != 1 )
            {
                fprintf( stderr, "Unexpected input error!\n" );
                exit( EXIT_FAILURE );
            }

            //only handle key-down events
            if ( ir.EventType != KEY_EVENT || !ir.Event.KeyEvent.bKeyDown )
                break;

            //echo output, if character is printable
            if ( isprint( (unsigned char)ir.Event.KeyEvent.uChar.AsciiChar ) )
            {
                printf( "%c", ir.Event.KeyEvent.uChar.AsciiChar );
            }

            printf( "\n" );

            switch ( ir.Event.KeyEvent.uChar.AsciiChar )
            {
            case '1':
                DoTask1();
                break;
            case '2':
                DoTask2();
                break;
            case 'q':
                printf( "Quitting program...\n" );
                quit = true;
                break;
            default:
                printf( "Unknown command.\n" );
            }

            should_prompt = true;

            break;

        case WAIT_TIMEOUT:
            printf( "Timeout!\n" );
            DoDefaultTask();
            should_prompt = true;
            break;

        default:
            fprintf( stderr, "unexpected error!" );
            exit( EXIT_FAILURE );
        }
    }

    return 0;
}

这个程序有以下行为:
Please select an option: 1
Doing task #1.
Please select an option: 2
Doing task #2.
Please select an option: 3
Unknown command.
Please select an option: 4
Unknown command.
Please select an option: Timeout!
Doing default task.
Please select an option: Timeout!
Doing default task.
Please select an option: 1
Doing task #1.
Please select an option: 3
Unknown command.
Please select an option: 2
Doing task #2.
Please select an option: 1
Doing task #1.
Please select an option: Timeout!
Doing default task.
Please select an option: q
Quitting program...

2
尝试使用alarm(3)。
#include <stdio.h>
#include <unistd.h>
int main(void)
{
   char buf [10];
   alarm(3);
   scanf("%s", buf);
   return 0;
}

我想知道函数alarm()在哪个库中,谢谢。 - user3208536

2
如他人所言,实现真正异步IO的最好方法是使用select(...)
但是,想要快速且简单地实现你所需的功能,可以使用getline(...),该函数每次返回读取的字节数(在IO操作时不会阻塞),并在没有读取到字节时返回-1。
以下是getline(3)手册页中的内容:
// The following code fragment reads lines from a file and writes them to standard output.  
// The fwrite() function is used in case the line contains embedded NUL characters.

char *line = NULL;
size_t linecap = 0;
ssize_t linelen;
while ((linelen = getline(&line, &linecap, fp)) > 0)
    fwrite(line, linelen, 1, stdout);

我想知道函数getline(...)在哪个库中,以及第三个参数fp的含义是什么,谢谢。 - user3208536
这里是从man getline直接摘抄的内容:“标准C库(libc,-lc)”。#include <stdio.h>fp是文件指针,您正在从中读取,例如标准输入或其他内容。 - mellowfish

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接