1

I am writing an application with a menu and I am asking the user to provide an integer representing an option from the menu

1. Option 1
2. Option 2
3. Option 3
...

This option is stored in a variable called

 option

I want to avoid wrong input such as "12a", "1a2", "anyString" and I've read that this can be achieved by storing return value of scanf_s() function.

So I stored it in a variable called

ret

and now I want that every time user provides wrong input to prompt them to enter a new value.

So I wrote something like this:

int ret = scanf_s("%d", &option);
while (!ret)
{
    cout << "Provide an integer, not a string! :)\n";
    ret = scanf_s("%d", &option);
}

The problem is when it enters the while it is not allowing user to enter a new value and hence the value of ret never changes, making it run endlessly.

How can I achieve what I am looking for?

Zeek Liviu
  • 15
  • 1
  • 6
  • Once `scanf` fails to match the next input character with your expected format, it will leave that character in the input stream. Consequently, doing the same `scanf` again will also fail. So after a failing match you must remove at least one character from the stream before trying the same `scanf` again. Look up `getchar` – Support Ukraine Dec 05 '22 at 08:53
  • If a `scanf` function fails to parse the input as requested (for example when you give some letter instead of a digit for the `%d` format), then `scanf` will stop immediately and leave the input in the buffer to be read again next time you read input. This is *one* of the reasons it's highly recommended to use `fgets` to read a whole line, and then attempt to parse that string instead. Basically, forget that `scanf` (and the MS-special `scanf_s`) function exists. – Some programmer dude Dec 05 '22 at 08:54
  • When `scanf()`-related functions are reading an integral value (e.g. due to `%d` format) and encounter other characters they (1) stop reading and (2) leave the extraneous characters in the stream buffer. So a second call to read an integer again encounters the same character (and stops again, leaving it in the buffer). Instead, try reading a line of input (e.g. read using `fgets()`) and parse (e.g. using `sscanf()`). That allows your code to skip over or drop extraneous characters. – Peter Dec 05 '22 at 08:56
  • @SupportUkraine Using `scanf()` and `getchar()` on the same stream is not a good idea. Since they do things differently (e.g. skip input differently) using them both on one stream is a good opportunity to confuse the user, since some characters will be dropped unexpectedly and some will not. – Peter Dec 05 '22 at 08:58
  • @Peter Using `scanf` is in general not a good idea. However, if one decides to use `scanf` anyway, it's not a problem to read a single character from the stream using `getchar` – Support Ukraine Dec 05 '22 at 09:02
  • So can't I clear the buffer somehow before calling the scanf in the while? – Zeek Liviu Dec 05 '22 at 09:10
  • @ZeekLiviu There is no standard way of "clearing the buffer" in a single function call. A typical way of "clearing the buffer" is a loop where you keep reading a single character until you get a newline. – Support Ukraine Dec 05 '22 at 09:13
  • @ZeekLiviu It may look like https://ideone.com/BdBQEo – Support Ukraine Dec 05 '22 at 09:25
  • @SupportUkraine The problem with using `getchar()` [or similar] after `scanf()` is deciding when to call `getchar()`. The real problem, with using both on the same stream, is that you're assuming the user will behave in a particular way, and cause unexpected behaviour (e.g. dropping characters that should not be dropped, or acting on input that should be dropped) if the user doesn't behave in the way you've assumed. – Peter Dec 05 '22 at 09:29
  • @SupportUkraine It's almost perfect, but what about inputs like ```12a``` or ```1s2```? They somehow seem to pass. – Zeek Liviu Dec 05 '22 at 09:33
  • @ZeekLiviu That's one of many problems with `scanf`... If you use `%d` it will read and match the digits in the start and stop at the first non-digit and "be happy". That's just how `scanf` works. If you don't like that you need more code... You could do https://ideone.com/pbGQoy This code will not accept "12a" but notice that the code will also not accept "12 " (a number followed by a space). I'm not sure if that's what you want. – Support Ukraine Dec 05 '22 at 09:45
  • @Peter Please provide an input example showing that – Support Ukraine Dec 05 '22 at 09:46
  • @SupportUkraine I just want to be sure that in the end in ```option``` I have a proper integer, regardless of whitespaces. I got an answer that seems to be enough. Though I wonder if it is possible to have just integers, without whitespaces. But I now understand that scanf has its limitations. Thank you for your explanations and time! – Zeek Liviu Dec 05 '22 at 09:54
  • @ZeekLiviu When parsing/validating user input the programmer must start by setting the rules for what is valid and invalid. The designers of `scanf` decided that `%d` shall ignore all initial whitespaces and stop at the first non-digit character. If that doesn't fit your needs, you must use something different than `scanf` and `%d` – Support Ukraine Dec 05 '22 at 10:04

1 Answers1

2

When scanf_s fails to convert an integer, the offending input stays in the input stream. Calling scanf_s("%d", &option) again will produce the same result until some characters are removed from the input stream. Note also that using scanf_s or scanf directly from stdin is error prone: the newline entered by the user stays in the input stream and will be read by a subsequent call to getchar() or fgets(), potentially causing unexpected behavior.

To avoid these pitfalls, it is recommended to read one line at a time with fgets() and convert it with sscanf() this way:

#ifdef _MSC_VER
#define _CRT_SECURE_NO_WARNINGS
#endif

#include <stdio.h>

int main() {
    char buf[80];
    int option;
    char cc;

    for (;;) {
        print_menu();  // output the menu options
        if (!fgets(buf, sizeof buf, stdin)) {
            /* end of file reached: break from the loop */
            break;
        }
        /* parse exactly one integer with optional leading and trailing whitespace */
        if (sscanf(buf, "%d %c", &option, &cc) != 1) {
            printf("invalid input: %s", buf);
            printf("enter one integer exactly\n");
            continue;
        }
        printf("option is %d\n", option);
        // handle option
    }
    return 0;
}
chqrlie
  • 131,814
  • 10
  • 121
  • 189
  • Is there any way that I can adapt this to an overload of ```operator>>``` in a class? The problem I've encountered is that ```fgets``` doesn't accept the ```in``` parameter from ```istream&```. – Zeek Liviu Dec 05 '22 at 18:53
  • @ZeekLiviu: I'm not sure how to transpose this into C++... [this answer](https://stackoverflow.com/a/51572325/4593267) suggests you could use `std::getline()` to read a line from a stream as a replacement for `fgets()`. – chqrlie Dec 05 '22 at 22:26
  • 1
    I expressed myself wrong, sorry! I wasn't looking to transpose your solution to C++. I just wanted to use a different stream than ```stdin``` because I thought it wouldn't work when I am reading my class attributes in the ```istream& in``` object. I tested, and turns out that it's working perfectly (and I think that's normal because, after all, I get characters from keyboard. Thanks again! – Zeek Liviu Dec 06 '22 at 15:52