为啥 scanf() 在此代码中导致无限循环?

Posted

技术标签:

【中文标题】为啥 scanf() 在此代码中导致无限循环?【英文标题】:Why is scanf() causing infinite loop in this code?为什么 scanf() 在此代码中导致无限循环? 【发布时间】:2010-12-15 12:27:54 【问题描述】:

我有一个小型 C 程序,它只从标准输入读取数字,每个循环周期一个。如果用户输入了一些 NaN,则应将错误打印到控制台并再次返回输入提示。在输入“0”时,循环应该结束,给定的正/负值的数量应该打印到控制台。这是程序:

#include <stdio.h>

int main()

    int number, p = 0, n = 0;

    while (1) 
        printf("-> ");
        if (scanf("%d", &number) == 0) 
            printf("Err...\n");
            continue;
        

        if (number > 0) p++;
        else if (number < 0) n++;
        else break; /* 0 given */
    

    printf("Read %d positive and %d negative numbers\n", p, n);
    return 0;

我的问题是,在输入一些非数字(如“a”)时,这会导致无限循环一遍又一遍地写入“-> Err...”。我想这是一个 scanf() 问题,我知道这个函数可以用更安全的函数代替,但这个例子是为初学者准备的,只知道 printf/scanf、if-else 和循环。

我已经阅读了this question 的答案并浏览了其他问题,但没有真正回答这个具体问题。

【问题讨论】:

许多密切相关的 SO 问题,包括:***.com/questions/1669821 针对所有答案和提示:添加 while (getchar() != '\n');在if语句中的“继续”之前对我来说真的很好,并且(希望)解决了所有/大部分问题。此外,这对初学者来说是可以合理解释的:)。 另见Using fflush(stdin) 【参考方案1】:

scanf() 将“a”留在输入缓冲区中以备下次使用。无论如何,您可能应该使用getline() 来读取一行,然后使用strtol() 或类似名称进行解析。

(是的,getline() 是 GNU 特定的,而不是 POSIX。那又如何?问题被标记为“gcc”和“linux”。getline() 也是读取一行文本的唯一明智选择,除非你愿意全部手动完成。)

【讨论】:

你不能依赖非标准扩展来处理像用户输入这样重要的事情,除非在你自己的树中提供它们以防它们不存在。如果您编辑您的答案以反映这一点,我将撤回我的反对票。 @tinkertim:问题在Linux上指定gcc,保证strtol可用 另外,至少提示如何打开此类扩展可能会有所帮助:) @Andomar:我对 getline() 有异议;) @TimPost getline() 和 getdelim() 最初都是 GNU 扩展。它们在 POSIX.1-2008 中被标准化。【参考方案2】:

我认为您只需要在继续循环之前刷新缓冲区即可。类似的东西可能会完成这项工作,尽管我无法从这里测试我正在写的内容:

int c;
while((c = getchar()) != '\n' && c != EOF);

【讨论】:

"过早的优化是万恶之源" ...但是切换常量:'\n'EOF 更有可能出现:) 您希望EOF 100% 保证出现;否则,您要么拥有非常快的键盘,要么拥有非常慢的 CPU while 语句条件中的上述复杂性是不必要的。 @ilgaar 你是什么意思?我觉得很好。【参考方案3】:

在扫描之前刷新输入缓冲区:

while(getchar() != EOF) continue;
if (scanf("%d", &number) == 0) 
    ...

我本来打算建议fflush(stdin),但显然这会导致undefined behavior。

响应您的评论,如果您希望显示提示,您必须刷新输出缓冲区。默认情况下,仅当您打印换行符时才会发生这种情况。喜欢:

while (1) 
    printf("-> ");
    fflush(stdout);
    while(getchar() != EOF) continue;
    if (scanf("%d", &number) == 0) 
    ...

【讨论】:

在 if 语句之前添加这个 while 循环确实会导致错误的程序行为。准确的说,第一次输入后没有出现“->”提示,可能是对的,也可能是错的。 您的while 循环将消耗所有内容,包括'\n' Afaik fflush() 在每个系统上的工作方式不同。至少在我的 Linux 机器上,fflush(stdout) 无助于显示“->”提示。此外,在这里调用 setvbuf() 也无济于事。【参考方案4】:

scanf 只消费与格式字符串匹配的输入,返回消费的字符数。任何与格式字符串不匹配的字符都会导致它停止扫描并将无效字符留在缓冲区中。正如其他人所说,您仍然需要在继续之前将无效字符从缓冲区中清除。这是一个非常肮脏的修复,但它会从输出中删除有问题的字符。

char c = '0';
if (scanf("%d", &number) == 0) 
  printf("Err. . .\n");
  do 
    c = getchar();
  
  while (!isdigit(c));
  ungetc(c, stdin);
  //consume non-numeric chars from buffer

编辑:修复了一次性删除所有非数字字符的代码。不再为每个非数字字符打印多个“Errs”。

Here 是对 scanf 的一个很好的概述。

【讨论】:

如果输入是“abc”,该代码将打印“Err...”三倍。 是的,这里是个不错的贫民区。我会稍微调整一下。 现在如果输入是“ab-10”,它会错误地从输入中删除减号并读取“10”作为下一个数字。 我知道它很旧,但只需将其更改为 while (!isdigit(c) &amp;&amp; c != '-');,这也应该有助于减号。 这仍然会导致多个输入行,尝试4tt44t 会给你-&gt; Err. . .t4 甚至不会给你任何错误,但仍然是多个输入线路:-&gt; -&gt; 【参考方案5】:

由于其他答案指出的scanf 的问题,您真的应该考虑使用另一种方法。我总是发现scanf 对任何严肃的输入读取和处理都太有限了。最好用fgets 读入整行,然后使用strtokstrtol 之类的函数处理它们(顺便说一句,它们会正确解析整数并准确告诉你无效字符的开始位置)。

【讨论】:

【参考方案6】:

使用fgets()sscanf(),而不是使用scanf() 并且必须处理具有无效字符的缓冲区。

/* ... */
    printf("0 to quit -> ");
    fflush(stdout);
    while (fgets(buf, sizeof buf, stdin)) 
      if (sscanf(buf, "%d", &number) != 1) 
        fprintf(stderr, "Err...\n");
       else 
        work(number);
      
      printf("0 to quit -> ");
      fflush(stdout);
    
/* ... */

【讨论】:

fgets() 读取一些缓冲区,如果它从一开始就不包含格式,则整行将被丢弃。这可能是不可接受的(但可能是需要的,这取决于要求)。【参考方案7】:

我有类似的问题。我只用scanf解决了。

Input "abc123&lt;Enter&gt;" 看看它是如何工作的。

#include <stdio.h>
int n, num_ok;
char c;
main() 
    while (1) 
        printf("Input Number: ");
        num_ok = scanf("%d", &n);
        if (num_ok != 1) 
            scanf("%c", &c);
            printf("That wasn't a number: %c\n", c);
         else 
            printf("The number is: %d\n", n);
        
    

【讨论】:

这仍然不能完全解决问题,因为如果您输入字母数字字符的组合,例如6y: Input Number: 6y 将导致:The number is: 6 Input Number: That wasn't a number: y 程序通过以下方式读取输入字符字符,当它在输入中找到一个数字字符时,它认为输入是一个数字,当它找到一个非数字时它认为它不是一个数字,但不能确定6y完全不是一个数字,当然,在这个过程中,由于[Enter] 键仍然存在于缓冲区中,也会出现同样的问题。【参考方案8】:

在某些平台(尤其是 Windows 和 Linux)上,您可以使用 fflush(stdin);

#include <stdio.h>

int main(void)

  int number, p = 0, n = 0;

  while (1) 
    printf("-> ");
    if (scanf("%d", &number) == 0) 
        fflush(stdin);
        printf("Err...\n");
        continue;
    
    fflush(stdin);
    if (number > 0) p++;
    else if (number < 0) n++;
    else break; /* 0 given */
  

  printf("Read %d positive and %d negative numbers\n", p, n);
  return 0;

【讨论】:

请阅读Using fflush(stdin)——尤其是问题的cmets——以获取有关此信息。它可以在 Windows 上运行,因为 Microsoft 记录了它的作用;在实践中(据我所知)它在其他任何地方都不起作用,尽管一些文档表明相反。 它现在可以在 Linux 上运行(或者我应该说 glibc)。以前没有,不知道什么时候改的。但是上次我在 mac 上尝试它崩溃了,而且它不在标准中,所以我在这个答案中添加了一个关于可移植性的警告。 当前版本不适合我。 $ ldd --versionldd (Debian GLIBC 2.19-18+deb8u9) 2.19。那应该提供所有需要的信息。有人知道为什么吗? fflush input stream 只为与 seekable files 关联的输入流定义(例如,磁盘文件但不是管道或终端)。 POSIX.1-2001 没有指定刷新输入流的行为,POSIX.1-2008 有,但仅限于描述的有限方式。 使用fflush(stdin) 会导致未定义的行为,并且不能保证可移植。【参考方案9】:

我有同样的problem,但我找到了一个有点老套的解决方案。我使用fgets() 读取输入,然后将其提供给sscanf()。对于无限循环问题,这不是一个糟糕的解决方案,并且通过一个简单的 for 循环,我告诉 C 搜索任何非数字字符。下面的代码不允许像123abc 这样的输入。

#include <stdio.h>
#include <ctype.h>
#include <string.h>

int main(int argc, const char * argv[]) 

    char line[10];
    int loop, arrayLength, number, nan;
    arrayLength = sizeof(line) / sizeof(char);
    do 
        nan = 0;
        printf("Please enter a number:\n");
        fgets(line, arrayLength, stdin);
        for(loop = 0; loop < arrayLength; loop++)  // search for any none numeric charcter inisde the line array
            if(line[loop] == '\n')  // stop the search if there is a carrage return
                break;
            
            if((line[0] == '-' || line[0] == '+') && loop == 0)  // Exculude the sign charcters infront of numbers so the program can accept both negative and positive numbers
                continue;
            
            if(!isdigit(line[loop]))  // if there is a none numeric character then add one to nan and break the loop
                nan++;
                break;
            
        
     while(nan || strlen(line) == 1); // check if there is any NaN or the user has just hit enter
    sscanf(line, "%d", &number);
    printf("You enterd number %d\n", number);
    return 0;

【讨论】:

我看你用的是goto,不要! "scanf 被认为是一个损坏的函数" - 嗯,它很难使用,但sscanf 也有大部分相同的困难。在这两种情况下,请谨慎使用。 @MM scanf() 不是一个损坏的函数,如果一个操作员不知道scanf() 的工作原理以及如何使用它,那么操作员可能还没有阅读scans() 的手册scanf() 不能因此受到责备。 我的引号中的文字是从另一条评论中引用的文字,该评论已被删除【参考方案10】:

您好,我知道这是一个旧线程,但我刚刚完成了一项学校作业,遇到了同样的问题。 我的解决方案是我使用 gets() 来获取 scanf() 留下的内容。

这里是稍微改写的 OP 代码;可能对他没有用,但也许它会帮助那里的其他人。

#include <stdio.h>

    int main()
    
        int number, p = 0, n = 0;
        char unwantedCharacters[40];  //created array to catch unwanted input
        unwantedCharacters[0] = 0;    //initialzed first byte of array to zero

        while (1)
        
            printf("-> ");
            scanf("%d", &number);
            gets(unwantedCharacters);        //collect what scanf() wouldn't from the input stream
            if (unwantedCharacters[0] == 0)  //if unwantedCharacters array is empty (the user's input is valid)
            
                if (number > 0) p++;
                else if (number < 0) n++;
                else break; /* 0 given */
            
            else
                printf("Err...\n");
        
        printf("Read %d positive and %d negative numbers\n", p, n);
        return 0;
    

【讨论】:

gets 非常不安全,不应该使用(因为这个原因,它已从标准 C 中删除)。 我同意这是一个危险的朋友,我在这里只将它用于一个小型应用程序(因此主观的 40 字符数组)。如果手头的问题在要求上更客观,那么你^^。【参考方案11】:

尝试使用这个:

if (scanf("%d", &number) == 0) 
        printf("Err...\n");
        break;
    

这对我来说很好......试试这个.. continue 语句不合适,因为 Err.. 应该只执行一次。所以,试试我测试过的break...这对你来说很好用..我测试过....

【讨论】:

【参考方案12】:

晚上好。我最近遇到了同样的问题,我找到了一个可能对很多人有帮助的解决方案。好吧,实际上函数“scanf”在内存中留下了一个缓冲区……这就是导致无限循环的原因。因此,如果您的初始 scanf 包含“null”值,您实际上必须将此缓冲区“存储”到另一个变量。这就是我的意思:

#include <stdio.h>
int n;
char c[5];
main() 
    while (1) 
        printf("Input Number: ");
        if (scanf("%d", &n)==0)   //if you type char scanf gets null value
            scanf("%s", &c);      //the abovementioned char stored in 'c'
            printf("That wasn't a number: %s\n", c);
        
        else printf("The number is: %d\n", n);
    

【讨论】:

scanf("%s", &amp;c) 是类型错误。 %s 采用 char *,而不是 char (*)[5]。此外,由于您没有限制读取的字符数,因此这是等待发生的缓冲区溢出。简单地丢弃输入将是一个更好的主意 (%*s)。【参考方案13】:

输入非数字时会发生错误,并且非数字仍保留在输入缓冲区中。你应该跳过它。即使这种符号组合(例如1a)最初也会被读取为数字 1,我认为您也应该跳过此类输入。

该程序可以如下所示。

#include <stdio.h>
#include <ctype.h>

int main(void) 

    int p = 0, n = 0;

    while (1)
    
        char c;
        int number;
        int success;

        printf("-> ");

        success = scanf("%d%c", &number, &c);

        if ( success != EOF )
        
            success = success == 2 && isspace( ( unsigned char )c );
        

        if ( ( success == EOF ) || ( success && number == 0 ) ) break;

        if ( !success )
        
            scanf("%*[^ \t\n]");
            clearerr(stdin);
        
        else if ( number > 0 )
        
            ++p;
        
        else if ( number < n )
        
            ++n;
        
    

    printf( "\nRead %d positive and %d negative numbers\n", p, n );

    return 0;

程序输出可能看起来像

-> 1
-> -1
-> 2
-> -2
-> 0a
-> -0a
-> a0
-> -a0
-> 3
-> -3
-> 0

Read 3 positive and 3 negative numbers

【讨论】:

【参考方案14】:

为了部分解决您的问题,我只是在 scanf 之后添加了这一行:

fgetc(stdin); /* to delete '\n' character */

下面,您的代码带有以下代码:

#include <stdio.h>

int main()

    int number, p = 0, n = 0;

    while (1) 
        printf("-> ");
        if (scanf("%d", &number) == 0) 
            fgetc(stdin); /* to delete '\n' character */
            printf("Err...\n");
            continue;
        

        if (number > 0) p++;
        else if (number < 0) n++;
        else break; /* 0 given */
    

    printf("Read %d positive and %d negative numbers\n", p, n);
    return 0;

但如果你输入了多个字符,程序会一个接一个地继续,直到出现“\n”。

所以我在这里找到了解决方案:How to limit input length with scanf

你可以使用这条线:

int c;
while ((c = fgetc(stdin)) != '\n' && c != EOF);

【讨论】:

【参考方案15】:

解决方法:0scanf返回时,需要添加fflush(stdin);

原因: 遇到错误时,它似乎将输入字符留在缓冲区中,因此每次调用scanf 时,它都会一直尝试处理无效字符,但从不删除它形成缓冲区。当您调用fflush 时,输入缓冲区(stdin)将被清除,因此将不再重复处理无效字符。

您修改了程序:以下是您的程序进行了必要的修改。

#include <stdio.h>

int main()

    int number, p = 0, n = 0;

    while (1) 
        printf("-> ");
        if (scanf("%d", &number) == 0) 
            fflush(stdin);
            printf("Err...\n");
            continue;
        

        if (number > 0) p++;
        else if (number < 0) n++;
        else break; /* 0 given */
    

    printf("Read %d positive and %d negative numbers\n", p, n);
    return 0;

【讨论】:

【参考方案16】:
// all you need is to clear the buffer!

#include <stdio.h>

int main()

    int number, p = 0, n = 0;
    char clearBuf[256]; //JG:
    while (1) 
        printf("-> ");
        if (scanf("%d", &number) == 0) 
            fgets(stdin, 256, clearBuf); //JG:
            printf("Err...\n");
            continue;
        

        if (number > 0) p++;
        else if (number < 0) n++;
        else break; /* 0 given */
    

    printf("Read %d positive and %d negative numbers\n", p, n);
    return 0;

【讨论】:

以上是关于为啥 scanf() 在此代码中导致无限循环?的主要内容,如果未能解决你的问题,请参考以下文章

为啥这段代码不会导致无限循环?

为啥这段代码会进入无限循环? [复制]

C : while( scanf("%d",&num) != 1 ) 无限循环

为啥我的代码在执行时的初始嵌套 for 循环中进入无限循环?

在while循环中使用scanf进行无限循环

为啥这个回调会产生无限循环