1

The while loop below breaks when a letter is added. I would like it to continue to loop even if a letter is added, as well as if a negative number is added. Here's my current code:

float monthpay;

printf("Please enter your monthly wage:\t");
    scanf("%f", &monthpay);
    while (monthpay <= 0)
    {
        printf("\nThat was invalid");
        printf("\n Please try again: ");
        scanf("%f", &monthpay);

    }
printf("%.2f", monthpay);

I was trying to make a loop that would loop if a letter was added by mistake.

Ken White
  • 123,280
  • 14
  • 225
  • 444
tyna 007
  • 11
  • 1
  • 3
    Check the return value of `scanf`. Check its [documentation](https://man7.org/linux/man-pages/man3/scanf.3.html) to see what it is returning. – Eugene Sh. Jan 09 '23 at 21:59
  • 4
    If you enter `23x`, `scanf` will return 1 with `monthpay` set to 23. However, if you just enter `x`, the return is 0, `monthpay` is unchanged and `x` will "stay stuck" in the stream. So, all subsequent calls will repeat the same behavior. (i.e.) your loop will become infinite. If you want real error checking, use `fgets` and then `strtod` instead of `scanf` – Craig Estey Jan 09 '23 at 22:06
  • 1
    For an example usage of `fgets` and `strtol` [can be adapted to `strtod`], see my answer: [Check if all values entered into char array are numerical](https://stackoverflow.com/a/65013419/5382650) – Craig Estey Jan 09 '23 at 22:13
  • `monthpay` is not initialized. `scanf` may not assign a value to `monthpay`, in which case `while (monthpay <= 0)` is undefined behavior. Resolve all potential UB before proceeding any further. 2 general rules: 1) don't use scanf, for anything, ever. 2) If you ignore rule 1, make sure you use scanf correctly. It is easier for a camel to go through the eye of a needle than it is for a developer to use scanf correctly. – William Pursell Jan 09 '23 at 22:26
  • 1
    @WilliamPursell: I disagree with your statement that the behavior is undefined. As far as I can tell, this would only be the case if `monthpay` happens to contain a trap representation. [§6.3.2.1 ¶2 of the ISO C11 standard](http://port70.net/~nsz/c/c11/n1570.html#6.3.2.1p2) does not apply, because the address of `monthpay` was taken. – Andreas Wenzel Jan 09 '23 at 23:15
  • But I have to agree with the sentiment that "It is easier for a camel to go through the eye of a needle than it is for a developer to use `scanf` correctly." :-) – Steve Summit Jan 10 '23 at 14:09
  • See [What can I use for input conversion instead of `scanf`?](https://stackoverflow.com/questions/58403537) – Steve Summit Jan 10 '23 at 14:10

3 Answers3

1

scanf() returns number of input items that were successfully matched and assigned. If you don't check the return value (rv) your variable (monthpay) may be uninitialized, and it's undefined behavior to use it.

void flush() {
   for(;;) {
     char ch;
     int rv = scanf("%c", &ch);
     if(rv == EOF || rv == 1 && ch == '\n') return;
   }
}

printf("Please enter your monthly wage:\t");
float monthpay;
for(;;) {
    int rv = scanf("%f", &monthpay);
    if(rv == EOF)
        return;
    if(rv == 1)
        break;
    printf("\nThat was invalid");
    printf("\n Please try again: ");
    flush();
}
printf("%.2f", monthpay);
Allan Wind
  • 23,068
  • 5
  • 28
  • 38
  • 2
    Note this will infinite loop if the user enters an invalid input like `ASDF` as `scanf()` will leave that in the input buffer. – Andrew Henle Jan 09 '23 at 22:46
  • This solution will correctly reject `abc`, but will accept `6abc` as valid input for the number `6`, although it should probably be rejected. – Andreas Wenzel Jan 09 '23 at 22:48
  • @AndrewHenle Good point. Added a flush. – Allan Wind Jan 09 '23 at 23:49
  • @AndreasWenzel op doesn't specify and I am not sure. For instance, if op wants to read a string next should it flush it or leave in the buffer? Both could be right. If you want to validate a line of input you would use `fgets()` then parse line of text. – Allan Wind Jan 09 '23 at 23:50
1

You should always check the return value of scanf to determine whether the input was successfully converted, before you attempt to use the result.

However, for line-based user input, using the function scanf is generally not advisable, because its behavior is not intuitive. It usually does not consume an entire line of input, which can cause all kinds of trouble. For example, scanf leaving the newline character on the input stream can cause this problem. If the user enters 6abc\n, then the situation is even worse: In that case, scanf will match the 6 as valid input, but leave abc\n on the input stream, which will most likely cause the next input operation to not behave as intended, unless you explicitly discard the remainder of the line from the input stream beforehand.

For the reasons stated above, it is generally better to always read an entire line of input at once as a string, for example using the function fgets. You can then use the function strtof to attempt to convert the string to a number.

Here is an example, which performs extensive input validation:

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

float get_float_from_user( const char *prompt );

int main( void )
{
    float monthpay;

    //loop forever until user enters a valid number
    for (;;) //infinite loop, equivalent to while(1)
    {
        monthpay = get_float_from_user( "Please enter your monthly wage: ");
        if ( monthpay > 0 )
        {
            //input is ok, so we can break out of the loop
            break;
        }

        printf( "Please enter a positive number!\n" );
    }

    printf( "Input was valid. You entered: %.2f\n", monthpay );

    return 0;
}

float get_float_from_user( const char *prompt )
{
    //loop forever until user enters a valid number
    for (;;)
    {
        char buffer[1024], *p;
        float f;

        //prompt user for input
        fputs( prompt, stdout );

        //get one line of input from input stream
        if ( fgets( buffer, sizeof buffer, stdin ) == NULL )
        {
            fprintf( stderr, "Unrecoverable input error!\n" );
            exit( EXIT_FAILURE );
        }

        //make sure that entire line was read in (i.e. that
        //the buffer was not too small)
        if ( strchr( buffer, '\n' ) == NULL && !feof( stdin ) )
        {
            int c;

            printf( "Line input was too long!\n" );

            //discard remainder of line
            do
            {
                c = getchar();

                if ( c == EOF )
                {
                    fprintf( stderr, "Unrecoverable error reading from input!\n" );
                    exit( EXIT_FAILURE );
                }

            } while ( c != '\n' );

            continue;
        }

        //attempt to convert string to number
        errno = 0;
        f = strtof( buffer, &p );
        if ( p == buffer )
        {
            printf( "Error converting string to number!\n" );
            continue;
        }

        //make sure that number is representable as a "float"
        if ( errno == ERANGE )
        {
            printf( "Number out of range error!\n" );
            continue;
        }

        //make sure that remainder of line contains only whitespace,
        //so that input such as "6abc" gets rejected
        for ( ; *p != '\0'; p++ )
        {
            if ( !isspace( (unsigned char)*p ) )
            {
                printf( "Unexpected input encountered!\n" );

                //cannot use `continue` here, because that would go to
                //the next iteration of the innermost loop, but we
                //want to go to the next iteration of the outer loop
                goto continue_outer_loop;
            }
        }

        return f;

    continue_outer_loop:
        continue;
    }
}

This program has the following behavior:

Please enter your monthly wage: abc
Error converting string to number!
Please enter your monthly wage: 6abc
Unexpected input encountered!
Please enter your monthly wage: 6e+2000
Number out of range error!
Please enter your monthly wage: -5
Please enter a positive number!
Please enter your monthly wage: 0
Please enter a positive number!
Please enter your monthly wage: 6.3
Input was valid. You entered: 6.30

The function get_float_from_user that I am using in the code above is based on the function get_int_from_user from this answer of mine to another question. See that answer for a further explanation on how that function works.

Andreas Wenzel
  • 22,760
  • 4
  • 24
  • 39
1

After trying to scan a float, scan and discard whitespace except for newline, scan discard and count any remaining characters that are not a newline, scan a newline.
The %n specifier will provide a count of the characters processed by the scan up to the specifier.
The asterisk in the scanset %*[ \t\f\v] tells scanf to discard the matching characters. The carat in the scanset %*[^\n] tells scanf to process the non-matching characters so everything that is not a newline will be discarded. %1[\n] tells scanf to scan one character and it must be a newline. If a float is entered followed only by whitespace, the loop will exit as scanned will be 2 and extra will be 0.

#include <stdio.h>

int main ( void) {
    char newline[2] = "";
    int extra = 0;
    int scanned = 0;
    float monthpay = 0.0;

    do {
        printf ( "enter monthly pay\n");
        scanned = scanf ( "%f", &monthpay);
        if ( scanned == EOF) {
            return 1;
        }
        extra = 0;
        scanf ( "%*[ \f\t\v]"); // scan and discard whitespace except newline
        scanf ( "%*[^\n]%n", &extra); // scan discard and count non-whitespace
        scanned += scanf ( "%1[\n]", newline);
    } while ( monthpay < 0 || scanned != 2 || extra != 0);

    return 0;
}
user3121023
  • 8,181
  • 5
  • 18
  • 16