Why does scanf appear to skip input?

2019-01-26 14:53发布

问题:

I am confused about scanf's behaviour in the following program. scanf appears to input once, and then not input again, until a stream of characters is printed.

Below in a C program

#include<stdio.h>
int main()
{
    int i, j=0;

    do
    {
        ++j;
        scanf("%d", &i);
        printf("\n\n%d %d\n\n", i, j);
    }
    while((i!=8) && (j<10));  

    printf("\nJ = %d\n", j);
    return 0;
}

here, Till i am inputting any integer program works perfectly fine, but when a character is inputted it goes on printing the last inputed value of i and never stops(untill j is 10 when loop exits) for scanf to take next input.

output::  
1    <-----1st input
1 1
2    <---- 2nd input
2 2
a    <---- character input
2 3  
2 4
2 5
2 6
2 7
2 8
2 9
2 10

J = 10  

same thing is happening in c++ also.

#include<iostream>
using namespace std;
int main()
{
    int i, j=0;

    do
    {
        ++j;
        cin>>i;
        cout<<i<<" "<<j<<"\n";
    }
    while((i!=8) && (j<10));

    cout<<"\nj = "<<j<<"\n";
}   


output of c++ program ::  
1     <-----1st input
1 1
2     <-----2nd input
2 2
a    <------ character input
0 3
0 4
0 5
0 6
0 7
0 8
0 9
0 10

j = 10  

only change in c++ is that 0 is being printed instead of last value.

I know here integer values are expected by the program, but i want to know what happens when character is inputted in place of an integer? what is the reason of all happening above?

回答1:

When you enter a, then cin >> i fails to read it because the type of i is int to which a character cannot be read. That means, a remains in the stream forever.

Now why i prints 0 is a different story. Actually it can print anything. The content of i is not defined once the attempt to read fails. Similar thing happens with scanf as well.

The proper way to write it this:

do
{
    ++j;
    if (!(cin>>i)) 
    {
       //handle error, maybe you want to break the loop here?
    }
    cout<<i<<" "<<j<<"\n";
}
while((i!=8) && (j<10));

Or simply this (if you want to exit loop if error occurs):

int i = 0, j = 0;
while((i!=8) && (j<10) && ( cin >> i) )
{
    ++j;
    cout<<i<<" "<<j<<"\n";
}


回答2:

If scanf sees a character in the input stream that doesn't match the conversion specifier, it stops the conversion and leaves the offending character in the input stream.

There are a couple of ways to deal with this. One is to read everything as text (using scanf with a %s or %[ conversion specifier or fgets) and then use atoi or strtol to do the conversion (my preferred method).

Alternately, you can check the return value of scanf; it will indicate the number of successful conversions. So, if scanf("%d", &i); equals 0, then you know there's a bad character in the input stream. You can consume it with getchar() and try again.



回答3:

You can never expect your users to enter valid things. The best practice is to read the input into a string and try to convert it to integer. If the input is not an integer, you can give an error message to the user.



回答4:

The problem is that when you enter an input that is not of the expected type (specified by %d for scanf, and the int type for cin>>i;, the inputstream is not advanced, which results in both operations trying to extract the same type of data from the exact same incorrect input (and failing just as well this time around too), thus you will never asked for another input.

To ensure this does not happen you will need to check the return value of both operations (read the manual for how each reports errors). If an error does happen (as when you enter a character), you will need to clear the error, consume the invalid input and try again. I find it better in C++ to read a whole line using std::gtline() instead of int or even std::string when geting input from ther user interactively, so you get into this "infinite" loop you experienced.



回答5:

You are ignoring the return value. See what the manual says about scanf(3):

RETURN VALUE
These functions return the number of input items successfully matched and assigned, which can be fewer than provided for, or even zero in the event of an early matching failure.

It fails matching an integer.



回答6:

You could check the return value of scanf to determine if an integer has been parsed correctly (return should =1). On failure, you have choices: either notify the user of the error and terminate, or recover by reading the next token with a scanf("%s" ...) perhaps with a warning.



回答7:

For scanf, you need to check its return value to see if the conversion on the input worked. scanf will return the number of elements successfully scanned. If the conversion did not work, it will leave the input alone, and you can try to scan it differently, or just report an error. For example:

if (scanf("%d", &i) != 1) {
    char buf[512];
    fgets(buf, sizeof(buf), stdin);
    printf("error in line %d: got %s", j, buf);
    return 0;
}

In your program, since the input is left alone, your loop repeats trying to read the same input.

In C++, you check for failure using the fail method, but the input stream failure state is sticky. So it won't let you scan further without clearing the error state.

    std::cin >> i;
    if (std::cin.fail()) {
        std::string buf;
        std::cin.clear();
        std::getline(cin, buf);
        std::cout
             << "error in line " << j
             << ": got " << buf
             << std::endl;
        return 0;
    }

In your program, since you never clear the failure state, the loop repeats using cin in a failure state, so it just reports failure without doing anything.

In both cases, you might find it easier or more reliable to work with the input if you would read in the input line first, and then attempt to parse the input line. In pseudocode:

while read_a_line succeeds
    parse_a_line

In C, the catch to reading a line is that if it is longer than your buffer, you will need to check for that and concatenate multiple fgets call results together to form the line. And, to parse a line, you can use sscanf, which is similar to scanf but works on strings.

    if (sscanf(buf, "%d", &i) != 1) {
        printf("error in line %d: got %s", j, buf);
        return 0;
    }

In C++, for quick low level parsing of formatted input, I also prefer sscanf. But, if you want to use the stream approach, you can convert the string buffer into a istringstream to scan the input.

    std::getline(cin, buf);
    if (std::cin.fail()) {
        break;
    }
    std::istringstream buf_in(buf);
    buf_in >> i;
    if (buf_in.fail()) {
        std::cout << "error in line " << j
             << ": got " << buf
             << std::endl;
        return 0;
    }


标签: c++ c io scanf cout