检查标准输入缓冲区是否为空

13

我正在尝试使用字符读取数字字符,但是我不知道stdin缓冲区是否为空。

我的第一个解决方案是在stdin缓冲区中查找\n字符,但是如果我想要输入多个由" "分隔的数字,则这样做行不通。

如何判断stdin缓冲区中是否有字符?

我需要在C中完成,并且具备可移植性。


1
检查EOF(文件结束符)。(它实际上不是一个字符,但如果流为空,则将返回该值。) - user66363
1
不要检查 stdin 是否为空。读取一个字符,检查操作的状态。如果状态不是“OK”,则假定 stdin 为空... if ((c = getchar()) == EOF) /*假设 stdin 为空;实际上可能是其他原因*/; - pmg
5个回答

11

有几种解决方案:

pollselect,超时时间为0 - 这些将立即返回,如果没有数据可用,则结果是-1并伴随着错误代码EAGAIN,否则返回有数据的描述符数量(由于您只检查标准输入,因此只有一个)。

ioctl是使用描述符的瑞士军刀。您需要的请求是I_NREAD

if (ioctl(0, I_NREAD, &n) == 0 && n > 0)
    // we have exactly n bytes to read

然而,正确的解决方案是将使用scanf得到的所有内容作为一行进行读取,然后处理结果 - 这可以使用sscanf很好地完成:

char buf[80]; // large enough
scanf("%79s", buf); // read everything we have in stdin
if (sscanf(buf, "%d", &number) == 1)
    // we have a number

只要您正确处理重新读取、比缓冲区长的字符串和其他现实中的复杂情况,就可以做到。


2
如果我尝试像这样做:while(!feof(stdin)) { c = getchar(); putchar(c); }将会进入一个无限循环。 - Tandura
函数 poll / select / ioctl 都不起作用,因为我需要一个新的库,这个库不包含在标准 IDE(我在学校使用的 Code::Blocks)中,所以解决方案仍然是第三种方法,使用缓冲区读取并检查缓冲区长度是否为 80(在这种情况下),然后我必须继续读取。 - Tandura
1
如果您的stdin从控制台读取,只有在发送EOF字符(据我所知是ctrl+d)时才会到达EOF。 pollselectioctl都是POSIX函数,而且讨论它是否应该在任何系统上都是不相关的话题。另一方面,sscanf解决方案应该可以直接使用。 - aragaer
1
feof 不是用来测试输入是否为空的,而是用来测试先前的输入操作是否已经发生了输入结束条件。在尝试输入操作并指示失败之前,不应该调用 feof - Kaz
好的。我已经删除了关于 feof 的部分,因为对于 STDIN 来说,它只有在 STDIN 被关闭 并且 你已经从中读取了所有内容 并且 再次尝试时才会返回 true。 - aragaer
显示剩余2条评论

4

如果有人通过谷歌来到这里 - 检查 stdin 是否为空的简单 select 解决方案:

fd_set readfds;
FD_ZERO(&readfds);
FD_SET(STDIN_FILENO, &readfds);
fd_set savefds = readfds;

struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 0;

int chr;

int sel_rv = select(1, &readfds, NULL, NULL, &timeout);
if (sel_rv > 0) {
  puts("Input:");
  while ((chr = getchar()) != EOF) putchar(chr);
} else if (sel_rv == -1) {
  perror("select failed");
}

readfds = savefds;

需要使用unistd.hstdlib.hstdio.h库。
可以在这里找到详细的解释。
更新: 感谢DrBeco注意到了select在出错时返回-1 -- 已添加错误处理。
实际上,select返回以下结果:
  • 准备好的描述符数量
  • 如果时间限制到期,则返回0
  • 如果发生错误则返回-1(errno将被设置)

在Linux上可以运行,但在Windows上无法运行(需要可移植性)。 - Tandura
1
在任何POSIX兼容系统上都可以使用@Tandura。通常在Windows上,您必须使用特定的方法,这些方法在其他系统上无法使用。因此,我认为任何*nix都比仅限于Windows更好。但是,是的,它不是完全可移植的。我不知道如何在Windows上操作,也许你知道吗? - stek29
1
请记住,当出现错误时(这也是“true”值),选择返回-1。 - DrBeco
2
如果使用select()进行检查的目的是为了避免挂起,那么如果发送的输入超过了stdinSTDIN_FILENO上的一个底层read()调用中可以缓冲的大小,发布的代码将会挂起。此外,发布的代码无法检测到是否已经有未读数据可供在stdin缓冲区中读取。 - Andrew Henle

1

我受到this的启发,参考了@stek29在这个页面上的帖子,并准备了一个简单的示例,如下所示:

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

int main(void)
{
    fd_set readfds;
    FD_ZERO(&readfds);

    struct timeval timeout;
    timeout.tv_sec = 0;
    timeout.tv_usec = 0;

    char message[50];

    while(1)
    {
        FD_SET(STDIN_FILENO, &readfds);

        if (select(1, &readfds, NULL, NULL, &timeout))
        {
            scanf("%s", message); 
            printf("Message: %s\n", message);
        }

        printf("...\n");
        sleep(1);
    }

    return(0);
}

对我来说没问题!我用 usleep(10); 替换了 sleep,并删除了 printf("...\n"); - datahaki
4
STDIN_FILENO上使用select(),同时使用基于FILE的操作,如在stdin上使用scanf(),可能会导致问题。例如,如果scanf()STDIN_FILENO读取和缓冲了5kb的输入,但只复制了一个30字节行(例如到message),那么stdin缓冲区中仍然有近5000字节的内容,因为在文件描述符级别没有留下任何内容供上面的示例程序查看。不要这样做。 - Andrew Henle
请不要使用 sleep, 在调用 select 之前将 timeout.tv_sec 设置为 1,将 timeout.tv_usec 设置为 0。这样当没有输入时,每个循环仍会等待一秒钟,但是在有任何输入时立即处理您的输入。 - Guntram Blohm

0

有很多方法可以检查stdin是否有可用输入。最具可移植性的方法依次为:selectfcntlpoll

以下是一些逐个案例说明如何执行检查的片段。

#include <stdio.h> /* same old */
#include <stdlib.h> /* same old */
#include <time.h> /* struct timeval for select() */
#include <unistd.h> /* select() */
#include <poll.h> /* poll() */
#include <sys/ioctl.h> /* FIONREAD ioctl() */
#include <termios.h> /* tcgetattr() and tcsetattr() */
#include <fcntl.h> /* fnctl() */

#define BUFF 256

int chkin_select(void);
int chkin_poll(void);
int chkin_ioctl(void);
int chkin_fcntl(void);
int chkin_termios(void);

/*
  Simple loops to test varios options of non-blocking test for stdin
*/

int main(void)
{
    char sin[BUFF]="r";

    printf("\nType 'q' to advance\nTesting select()\n");
    while(sin[0]++ != 'q')
    {
        while(!chkin_select())
        {
            printf("nothing to read on select()\n");
            sleep(2);
        }
        fgets(sin, BUFF, stdin);
        printf("\nInput select(): %s\n", sin);
    }

    printf("\nType 'q' to advance\nTesting poll()\n");
    while(sin[0]++ != 'q')
    {
        while(!chkin_poll())
        {
            printf("nothing to read poll()\n");
            sleep(2);
        }
        fgets(sin, BUFF, stdin);
        printf("\nInput poll(): %s\n", sin);
    }

    printf("\nType 'q' to advance\nTesting ioctl()\n");
    while(sin[0]++ != 'q')
    {
        while(!chkin_ioctl())
        {
            printf("nothing to read ioctl()\n");
            sleep(2);
        }
        fgets(sin, BUFF, stdin);
        printf("\nInput ioctl(): %s\n", sin);
    }

    printf("\nType 'q' to advance\nTesting fcntl()\n");
    while(sin[0]++ != 'q')
    {
        while(!chkin_fcntl())
        {
            printf("nothing to read fcntl()\n");
            sleep(2);
        }
        fgets(sin, BUFF, stdin);
        printf("\nInput fcntl: %s\n", sin);
    }

    printf("\nType 'q' to advance\nTesting termios()\n");
    while(sin[0]++ != 'q')
    {
        while(!chkin_termios())
        {
            printf("nothing to read termios()\n");
            sleep(2);
        }
        fgets(sin, BUFF, stdin);
        printf("\nInput termios: %s\n", sin);
    }

    return EXIT_SUCCESS;
}

/*
   select() and pselect() allow a program to monitor multiple file
   descriptors, waiting until one or more of the file descriptors become
   "ready" for some class of I/O operation (e.g., input possible).  A
   file descriptor is considered ready if it is possible to perform a
   corresponding I/O operation (e.g., read(2) without blocking, or a
   sufficiently small write(2)).
 */
int chkin_select(void)
{
    fd_set rd;
    struct timeval tv={0};
    int ret;

    FD_ZERO(&rd);
    FD_SET(STDIN_FILENO, &rd);
    ret=select(1, &rd, NULL, NULL, &tv);

    return (ret>0);
}

/*  poll() performs a similar task to select(2): it waits for one of a
       set of file descriptors to become ready to perform I/O.

       The set of file descriptors to be monitored is specified in the fds
       argument, which is an array of structures of the following form:

           struct pollfd {
               int   fd;         // file descriptor //
               short events;     // requested events //
               short revents;    // returned events //
           };

       The caller should specify the number of items in the fds array in
       nfds.
*/
int chkin_poll(void)
{
    int ret;
    struct pollfd pfd[1] = {0};

    pfd[0].fd = STDIN_FILENO;
    pfd[0].events = POLLIN;
    ret = poll(pfd, 1, 0);

    return (ret>0);
}

/*
    The ioctl(2) call for terminals and serial ports accepts many
       possible command arguments.  Most require a third argument, of
       varying type, here called argp or arg.

       Use of ioctl makes for nonportable programs.  Use the POSIX interface
       described in termios(3) whenever possible.
*/
int chkin_ioctl(void)
{
    int n;
    ioctl(STDIN_FILENO, FIONREAD, &n);
    return (n>0);
}

/*
       fcntl() performs one of the operations described below on the open
       file descriptor fd.  The operation is determined by cmd.

       fcntl() can take an optional third argument.  Whether or not this
       argument is required is determined by cmd.  The required argument
       type is indicated in parentheses after each cmd name (in most cases,
       the required type is int, and we identify the argument using the name
       arg), or void is specified if the argument is not required.

       Certain of the operations below are supported only since a particular
       Linux kernel version.  The preferred method of checking whether the
       host kernel supports a particular operation is to invoke fcntl() with
       the desired cmd value and then test whether the call failed with
       EINVAL, indicating that the kernel does not recognize this value.
*/
int chkin_fcntl(void)
{
    int flag, ch;

    flag = fcntl(STDIN_FILENO, F_GETFL, 0); /* save old flags */
    fcntl(STDIN_FILENO, F_SETFL, flag|O_NONBLOCK); /* set non-block */
    ch = ungetc(getc(stdin), stdin);
    fcntl(STDIN_FILENO, F_SETFL, flag); /* return old state */

    return (ch!=EOF);
}

/*
 The termios functions describe a general terminal interface that is provided to control asynchronous communications ports.
 This function doesn't wait for '\n' to return!
 */
int chkin_termios(void)
{
    struct termios old, new;
    int ch;

    tcgetattr(STDIN_FILENO, &old); /* save settings */

    new = old;
    new.c_lflag &= ~ICANON; /* non-canonical mode: inputs by char, not lines */ 
    new.c_cc[VMIN] = 0; /* wait for no bytes at all */
    new.c_cc[VTIME] = 0; /* timeout */
    tcsetattr(STDIN_FILENO, TCSANOW, &new); /* new settings */

    ch = ungetc(getc(stdin), stdin); /* check by reading and puking it back */

    tcsetattr(STDIN_FILENO, TCSANOW, &old); /* restore old settings */
    return (ch!=EOF);
}

尽量避免使用ioctltermios,它们太具体或者太底层了。此外,你不能真正有意义地使用feof来处理stdin或任何FIFO。你可以保证指针位置,如果你尝试使用ftellfseek,你会得到一个错误(请使用perror)。


参考资料:


1
混合使用文件描述符上的 select() 和通过 fgets() 等调用从 stdin 读取数据无法避免阻塞。数据可能会被读入缓冲的 stdin 中并留在那里,导致未读取的数据永远不会被底层文件描述符上的 select() 检测到。禁用 stdin 上的缓冲也行不通,因为这样一来像 fgets() 这样的调用就可能会阻塞 - 而检查输入的唯一原因就是为了防止这种阻塞。 - Andrew Henle
嗨,安德鲁。感谢您的意见。有两件事情:fgets()不是这些片段的核心。新手程序员甚至难以尝试调用这些函数。我可以补充说,这个问题不仅会出现在select()中。对于使用这些示例的人来说,这是一个公平的警告。如果您恰好知道数据格式,可以使用fgets()scanf()getc()write()或其他方法检查和读取它们。但是,如果您有未经格式化的数据进入,最好逐个字符检查。我上面给出的示例将假定数据以\n结尾(除了一次发送一个字符的termios())。 - DrBeco
第二点:我用重定向或文件写入填充stdin的测试在使用select()时很有效。另一方面,如果您使用重定向填充stdin,则ioctl()会挂起。此外,在这些示例中,注意termios()有非常不同的方法,而且一旦缓冲区中有字符,行为立即指示。这些示例应该由程序员加以驯服,它们并不是“解决所有问题的解决方案”。最重要的是:了解您的数据输入,或做好错误准备。 - DrBeco
即使在字符一次输入模式下使用TTY设备,如果用户将文本块粘贴到终端中,逻辑也无法正常工作。 stdin 流最终会缓冲多个字符。在基于glibc的Linux系统上,setbuf(stdin,NULL); 可以解决此问题。我似乎记得ISO C并没有定义缓冲对输入流产生影响。 - Kaz
POSIX确实指出缓冲区是用于输入和输出的。但它并没有明确说明未经缓冲的输入流将发出一个字节读取;它只是说字节“尽快从源中出现”。这并不排除读取已经可用的多个字节;事实上,一字节读取特别延迟了TTY中已经可用的输入进入缓冲区的传递。 - Kaz

0
int number=-1;   //-1 is default std for error;
int success=0;   //will serve as boolean (0==FALSE;1==TRUE)
char buf[BUFFER_SIZE];   // Define this as convinient (e.g. #define BUFFER_SIZE 100)
char *p=buf;   //we'll use a pointer in order to preserve input, in case you want to use it later

    fgets(buf,BUFFER_SIZE,stdin);  //use fgets() for security AND to grab EVERYTHING from stdin, including whitespaces

    while(*p!='\0'){   //parse the buf
        if(sscanf(p,"%d",&number)==1){   //at each char position try to grab a valid number format, 
            success=1;                   //if you succeed, then flag it.
            break;
        }
        
        p++;   //if you don't succeed, advance the pointer to the next char position
     }   //repeat the cycle until the end of buf (string end char =='\0')

    if (success)
        printf(">> Number=%d at position nº %d.",number,(int)(p-buf));   //you get the position by calculating the diff 
                                                                    //between the current position of the p and the 
                                                                    //beginning position of the buf
    else {
    // do whatever you want in case of failure at grabbing a number
    }

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