Why is scanf() causing infinite loop in this code?

65,312

Solution 1

scanf consumes only the input that matches the format string, returning the number of characters consumed. Any character that doesn't match the format string causes it to stop scanning and leaves the invalid character still in the buffer. As others said, you still need to flush the invalid character out of the buffer before you proceed. This is a pretty dirty fix, but it will remove the offending characters from the output.

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
}

edit: fixed the code to remove all non-numeric chars in one go. Won't print out multiple "Errs" for each non-numeric char anymore.

Here is a pretty good overview of scanf.

Solution 2

scanf() leaves the "a" still in the input buffer for next time. You should probably use getline() to read a line no matter what and then parse it with strtol() or similar instead.

(Yes, getline() is GNU-specific, not POSIX. So what? The question is tagged "gcc" and "linux". getline() is also the only sensible option to read a line of text unless you want to do it all by hand.)

Solution 3

I think you just have to flush the buffer before you continue with the loop. Something like that would probably do the job, although I can't test what I am writing from here:

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

Solution 4

Rather than using scanf() and have to deal with the buffer having invalid character, use fgets() and sscanf().

/* ... */
    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);
    }
/* ... */

Solution 5

Due to the problems with scanf pointed out by the other answers, you should really consider using another approach. I've always found scanf way too limited for any serious input reading and processing. It's a better idea to just read whole lines in with fgets and then working on them with functions like strtok and strtol (which BTW will correctly parse integers and tell you exactly where the invalid characters begin).

Share:
65,312
Admin
Author by

Admin

Updated on July 17, 2022

Comments

  • Admin
    Admin almost 2 years

    I've a small C-program which just reads numbers from stdin, one at each loop cycle. If the user inputs some NaN, an error should be printed to the console and the input prompt should return again. On input of "0", the loop should end and the number of given positive/negative values should be printed to the console. Here's the program:

    #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;
    }
    

    My problem is, that on entering some non-number (like "a"), this results in an infinite loop writing "-> Err..." over and over. I guess it's a scanf() issue and I know this function could be replace by a safer one, but this example is for beginners, knowing just about printf/scanf, if-else and loops.

    I've already read the answers to the questionscanf() skips every other while loop in C and skimmed through other questions, but nothing really answer this specific problem.

  • pmg
    pmg over 14 years
    "premature optimization is the root of all evil" ... but switch the constants: '\n' is much more likely to show up than EOF :)
  • Teddy
    Teddy over 14 years
    If the input is "abc", that code will print "Err. . ." three times.
  • Tim Post
    Tim Post over 14 years
    You can not rely on non standard extensions for something as crucial as user input without providing them in your own tree in case they do not exist. If you edit your answer to reflect this, I will withdraw my down vote.
  • Andomar
    Andomar over 14 years
    @tinkertim: the question specifies gcc on Linux, guaranteeing that strtol is available
  • Tim Post
    Tim Post over 14 years
    Also, at least hinting how to turn such extension ON may help :)
  • Tim Post
    Tim Post over 14 years
    @Andomar: It was getline() that I was taking issue with ;)
  • Andomar
    Andomar over 14 years
    You'd hope EOF is 100% guaranteed to show up; otherwise, you either have a really fast keyboard or a really slow CPU
  • Admin
    Admin over 14 years
    Adding this while-loop before the if-statement does result in a wrong program behaviour. To be exact, the "->" prompt isn't shown after the first input, may it either be right or wrong.
  • pmg
    pmg over 14 years
    Your while loop will consume everything, '\n' included.
  • Admin
    Admin over 14 years
    Afaik fflush() doesn't work the same way on each system. At least on my Linux box, the fflush(stdout) doesn't help to show up the "->" prompt. Also, a call to setvbuf() doesn't help here, too.
  • Jamison Dance
    Jamison Dance over 14 years
    Yeah, it is pretty ghetto. I will tweak it a bit.
  • Roman Nikitchenko
    Roman Nikitchenko over 14 years
    fgets() read some buffer and if it doesn't contain format right from the beginning the whole line is thrown away. This could be not acceptable (but could be desired, it depends on requirements).
  • caf
    caf over 14 years
    Now if the input is "ab-10", it will incorrectly remove the minus sign from the input and read "10" as the next number.
  • Admin
    Admin over 10 years
    I see that you are using goto, don't!
  • alexia
    alexia over 9 years
    @TimPost Both getline() and getdelim() were originally GNU extensions. They were standardized in POSIX.1-2008.
  • Jonathan Leffler
    Jonathan Leffler over 9 years
    Please read Using fflush(stdin) — especially the comments to the question — for information about this. It works on Windows because Microsoft documents that it does; it doesn't work anywhere else (that I know of) in practice, notwithstanding some documentation suggesting the contrary.
  • Michael Armbruster
    Michael Armbruster almost 9 years
    I know it's old, but just change it to while (!isdigit(c) && c != '-');, that should also help with minus signs.
  • ilgaar
    ilgaar over 8 years
    This still doesn't solve the problem entirely, since if you enter a combination of alphanumeric characters, like 6y: Input Number: 6y will result in : The number is: 6 Input Number: That wasn't a number: y the program reads the input character by character, when it finds a number character in the input, it thinks the input is a number, and when it finds a non numeric one it thinks it is not a number, but can not decide that 6y is not a number altogether, and of course in the process due to the [Enter] key being still present in the buffer, the same problem arises.
  • ilgaar
    ilgaar over 8 years
    This still results in multiple input lines, try 4t and t4, 4t will give you -> Err. . . and t4 will not even give you any errors, but still multiple input lines: -> ->
  • M.M
    M.M over 8 years
    "scanf is considered a broken function" - well, it is difficult to use, but sscanf also shares most of the same difficulties. In both cases, use very carefully.
  • KriptSkitty
    KriptSkitty over 7 years
    It works on Linux now (or I should say glibc). It didn't before, and I don't know when they changed it. But the last time I tried it on a mac it crashed, and it is not in the standard, so I've added a warning about portability to this answer.
  • DrBeco
    DrBeco almost 7 years
    Not working for me here with my current version. $ ldd --version gives ldd (Debian GLIBC 2.19-18+deb8u9) 2.19. That should give all info needed. Anyone has a clue why?
  • David C. Rankin
    David C. Rankin over 6 years
    fflush input stream is only defined for input streams associated with seekable files (e.g., disk files, but not pipes or terminals). POSIX.1-2001 did not specify the behavior for flushing of input streams, POSIX.1-2008 does, but only in the limited way described.
  • ilgaar
    ilgaar over 5 years
    using fflush(stdin) will cause undefined behavior and is not guaranteed to work portably.
  • ilgaar
    ilgaar over 5 years
    The above complication in the while statement's condition is unnecessary.
  • ilgaar
    ilgaar over 5 years
    suggesting to use getline() will break portability of the code.
  • melpomene
    melpomene over 5 years
    scanf("%s", &c) is a type error. %s takes a char *, not a char (*)[5]. Also, since you're not limiting the number of characters read, this is a buffer overflow waiting to happen. Simply discarding the input would be a much better idea (%*s).
  • melpomene
    melpomene over 5 years
    gets is horribly unsafe and should never be used (it has been removed from standard C for that reason).
  • midnightCoder
    midnightCoder over 5 years
    i agree it is dangerous friend and i only used it here for a small application (hence the subjective 40 char array). if the problem at hand were more objective in its requirements then ya ^^.
  • ilgaar
    ilgaar over 5 years
    @M.M scanf() is not a broken function, if an op doesn't know how scanf() works and how to use it, then op probably hasn't read the manual for scans() and scanf() cannot be blamed for that.
  • M.M
    M.M over 5 years
    my text in quotes is quoted text from another comment which has since been removed
  • ilgaar
    ilgaar over 5 years
    You better put the getchar() just before the printf("Err. . .\n");
  • Filippo Costa
    Filippo Costa over 4 years
    @ilgaar What do you mean? It looks fine to me.
  • Steve Summit
    Steve Summit almost 3 years
    returning the number of characters consumed -- Actually, scanf returns the number of fields successfully read. So scanf("%d") might return 1, 0, or EOF.
  • Jonathan Leffler
    Jonathan Leffler over 2 years
    If you use getline() to read a line, you must remember to free() the space (or, at least, consider whether it needs freeing, and the answer will usually be "yes" so you end up freeing it). Note that getline() typically allocates space even if the first operation returns -1 (indicating EOF; note that it explicitly returns -1 and not EOF, even though EOF is usually -1).