Validar el tipo de entrada en un bucle do- while

Resuelto Juen Khaw asked hace 9 años • 5 respuestas

Básicamente, necesito asegurarme de que la entrada sea un número entero , así:

do {
    printf("Enter > ");
    scanf("%d", &integer);
} while (/* user entered a char instead of an int */);

He probado varios métodos, pero siempre termina con un error de tiempo de ejecución o un bucle infinito cuando intento ingresar un archivo char. Sabía que fflush(stdin)es un comportamiento indefinido, por lo que es mejor no incluirlo en mi código para evitar cualquier error, además ya no funciona en VS2015 por algunas razones.

Los códigos a continuación son el método que he probado:

typedef enum {false, true} bool;
int ipt;
char c;
bool wrong_ipt;

do {
    c = '\0';
    printf("Enter > ");
    scanf("%d%c", &ipt, &c); //infinite loop occurs while a char has been entered
} while (c != '\n');

do {
    c = '\0';
    printf("Enter > ");
} while (scanf("%d", &ipt) != EOF);

do {
    wrong_ipt = false;
    do {
        ipt = NULL;
        printf("Enter > ");
        scanf("%d", &ipt);
        if (ipt == NULL) {
            wrong_ipt = true;
            break;
        }
    } while (ipt == NULL);
} while (wrong_ipt);

¿Hay alguna otra forma además de fflush(stdin)cuál pueda usarse para evitar el bucle infinito cuando el usuario ingresa a charen C ?

Gracias

Juen Khaw avatar Jul 26 '15 09:07 Juen Khaw
Aceptado

El problema es que "scanf()" puede dejar datos no leídos en su búfer de entrada. De ahí el "bucle infinito".

Otro problema es que debes validar el valor de retorno de scanf(). Si espera un valor entero... y scanf devuelve "0" elementos leídos... entonces sabe que algo salió mal.

Aquí hay un ejemplo:

#include <stdio.h>

void discard_junk () 
{
  int c;
  while((c = getchar()) != '\n' && c != EOF)
    ;
}

int main (int argc, char *argv[])
{
  int integer, i;
  do {
      printf("Enter > ");
      i = scanf("%d", &integer);
      if (i == 1) {
        printf ("Good value: %d\n", integer);
      }
      else {
        printf ("BAD VALUE, i=%i!\n", i);
        discard_junk ();
      }
   } while (i != 1);

  return 0;
}

Salida de muestra:

Enter > A
BAD VALUE, i=0!
Enter > B
BAD VALUE, i=0!
Enter > 1
Good value: 1

'¡Espero que ayude!

paulsm4 avatar Jul 26 '2015 02:07 paulsm4

Este es un muy buen ejemplo de por qué scanfgeneralmente no debería usarse para la entrada del usuario.

Dado que la entrada del usuario se basa en líneas, uno esperaría que una función de entrada siempre leyera una línea de entrada a la vez. Sin embargo, esa no es la forma en que scanfse comporta la función. En cambio, consume sólo tantos caracteres como sean necesarios para coincidir con el %despecificador de formato de conversión. Si scanfno puede coincidir con nada, entonces no consume ningún carácter, por lo que la siguiente llamada scanffallará exactamente por el mismo motivo (suponiendo que se utilice el mismo especificador de conversión y que usted no descarte explícitamente la entrada no válida). ). Esto es lo que está sucediendo en tu código.

Al momento de escribir este artículo, tres de las otras respuestas resuelven este problema verificando el valor de retorno scanfy descartando explícitamente la entrada no válida. Sin embargo, las tres (!) respuestas tienen el problema de que, por ejemplo, aceptan "6sdfj23jlj"como entrada válida el número 6, aunque obviamente toda la línea de entrada debería rechazarse en este caso. Esto se debe a que scanf, como se mencionó anteriormente, no lee una línea de entrada a la vez.

Por lo tanto, la mejor solución a su problema probablemente sería utilizar la entrada basada en líneas fgetsen su lugar. De esa manera, siempre leerá exactamente una línea de entrada a la vez (suponiendo que el búfer de entrada sea lo suficientemente grande como para almacenar una línea completa de entrada). Después de leer la línea, puedes intentar convertirla en un número usando strtol. Incluso si la conversión falla, la línea de entrada se habrá consumido del flujo de entrada, por lo que no tendrá la mayoría de los problemas descritos anteriormente.

Una solución simple fgetspodría verse así:

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

int main( void )
{
    char line[100];
    long number;

    //retry until user enters valid input
    for (;;) //infinite loop, equivalent to while(1)
    {
        char *p;

        //prompt user for input
        printf( "Please enter a number: " );

        //attempt to read one line of input
        if ( fgets( line, sizeof line, stdin ) == NULL )
        {
            fprintf( stderr, "Unrecoverable input error!\n" );
            exit( EXIT_FAILURE );
        }

        //attempt to convert input to number
        number = strtol( line, &p, 10 );

        //verify that conversion was successful
        if ( p == line )
        {
            printf( "Invalid input!\n" );
            continue;
        }

        //verify that remainder of line only contains
        //whitespace, so that input such as "6sdfj23jlj"
        //gets rejected
        for ( ; *p != '\0'; p++ )
        {
            if ( !isspace( (unsigned char)*p ) )
            {
                printf( "Encountered invalid character!\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;
            }
        }

        //input was valid, so break out of infinite loop
        break;

    //label for breaking out of nested loop
    continue_outer_loop:
        continue;
    }

    printf( "Input was valid.\n" );
    printf( "The number is: %ld\n", number );

    return 0;
}

Tenga en cuenta que la gotodeclaración generalmente no debe usarse. Sin embargo, en este caso, es necesario para salir de un bucle anidado.

Este programa tiene el siguiente resultado:

Please enter a number: 94hjj
Encountered invalid character!
Please enter a number: 5455g
Encountered invalid character!
Please enter a number: hkh7
Invalid input!
Please enter a number: 6sdfj23jlj
Encountered invalid character!
Please enter a number: 67
Input was valid.
The number is: 67

Sin embargo, este código aún no es perfecto. Todavía tiene los siguientes problemas:

  1. Si el usuario ingresa 100 caracteres en una sola línea, entonces toda la línea no cabe en el búfer de entrada. En ese caso, se necesitarán dos llamadas fgetspara leer la línea completa y el programa tratará incorrectamente esa línea como dos líneas de entrada separadas.

  2. El código no verifica si el número que el usuario ingresó se puede representar como long(por ejemplo, si el número es excesivamente grande). La función strtolinforma esto configurándose errnoen consecuencia (que es una característica de la que scanfcarece).

Estos dos problemas también se pueden solucionar realizando comprobaciones adicionales y manejo de errores. Sin embargo, el código se está volviendo tan complejo que tiene sentido ponerlo todo en su propia función:

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

int get_int_from_user( const char *prompt )
{
    //loop forever until user enters a valid number
    for (;;)
    {
        char buffer[1024], *p;
        long l;

        //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;
        l = strtol( buffer, &p, 10 );
        if ( p == buffer )
        {
            printf( "Error converting string to number!\n" );
            continue;
        }

        //make sure that number is representable as an "int"
        if ( errno == ERANGE || l < INT_MIN || l > INT_MAX )
        {
            printf( "Number out of range error!\n" );
            continue;
        }

        //make sure that remainder of line contains only whitespace,
        //so that input such as "6sdfj23jlj" 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 l;

    continue_outer_loop:
        continue;
    }
}

int main( void )
{
    int number;

    number = get_int_from_user( "Please enter a number: " );

    printf( "Input was valid.\n" );
    printf( "The number is: %d\n", number );

    return 0;
}
Andreas Wenzel avatar Oct 19 '2021 19:10 Andreas Wenzel