Desventajas de scanf

12 minutos de lectura

avatar de usuario
karthi_ms

Quiero saber las desventajas de scanf().

En muchos sitios, he leído que usando scanf podría causar desbordamientos de búfer. ¿Cuál es la razón para esto? ¿Hay otros inconvenientes con scanf?

avatar de usuario
autista

La ventaja de scanf es que una vez que aprende a usar la herramienta, como siempre debe hacer en C, tiene casos de uso inmensamente útiles. Puedes aprender a usar scanf y amigos leyendo y comprendiendo el manual. Si no puede leer ese manual sin serios problemas de comprensión, esto probablemente indica que no conoce muy bien C.


scanf y amigos sufrieron decisiones de diseño desafortunadas eso hizo que fuera difícil (y en ocasiones imposible) usarlo correctamente sin leer la documentación, como han demostrado otras respuestas. Desafortunadamente, esto ocurre en todo C, por lo que si tuviera que desaconsejar el uso de scanf entonces probablemente recomendaría no usar C.

Una de las mayores desventajas parece ser puramente la reputación que se ganó entre los no iniciados.; como con muchas características útiles de C, debemos estar bien informados antes de usarlo. La clave es darse cuenta de que, al igual que con el resto de C, parece sucinto e idiomático, pero eso puede ser sutilmente engañoso. Esto es omnipresente en C; Es fácil para los principiantes escribir código que creen que tiene sentido y que incluso podría funcionar para ellos inicialmente, pero que no tiene sentido y puede fallar catastróficamente.

Por ejemplo, los no iniciados normalmente esperan que el %s delegado causaría una linea para ser leído, y si bien eso puede parecer intuitivo, no es necesariamente cierto. Es más apropiado describir el campo leído como una palabra. Se recomienda encarecidamente leer el manual para cada función.

¿Cuál sería cualquier respuesta a esta pregunta sin mencionar su falta de seguridad y el riesgo de desbordamiento del búfer? Como ya hemos cubierto, C no es un lenguaje seguro y nos permitirá tomar atajos, posiblemente para aplicar una optimización a expensas de la corrección o más probablemente porque somos programadores perezosos. Por lo tanto, cuando sabemos que el sistema nunca recibirá una cadena mayor que un número fijo de bytes, tenemos la capacidad de declarar una matriz de ese tamaño y renunciar a la verificación de límites. Realmente no veo esto como una caída; es una opción Una vez más, se recomienda encarecidamente leer el manual, ya que nos revelaría esta opción.

Los programadores perezosos no son los únicos a los que les molesta scanf. No es raro ver a personas tratando de leer float o double valores usando %d, por ejemplo. Por lo general, se equivocan al creer que la implementación realizará algún tipo de conversión en segundo plano, lo que tendría sentido porque se producen conversiones similares en el resto del lenguaje, pero ese no es el caso aquí. Como dije anteriormente, scanf y los amigos (y de hecho el resto de C) son engañosos; parecen sucintas e idiomáticas, pero no lo son.

Los programadores sin experiencia no están obligados a considerar el éxito de la operación.. Supongamos que el usuario ingresa algo completamente no numérico cuando le hemos dicho scanf para leer y convertir una secuencia de dígitos decimales usando %d. La única forma en que podemos interceptar tales datos erróneos es verificar el valor devuelto, y ¿con qué frecuencia nos molestamos en verificar el valor devuelto?

Muy parecido fgetscuando scanf y los amigos no leen lo que se les dice que lean, la transmisión quedará en un estado inusual;
– En el caso de fgets, si no hay suficiente espacio para almacenar una línea completa, entonces el resto de la línea que queda sin leer podría tratarse erróneamente como si fuera una línea nueva cuando no lo es. – En el caso de scanf y amigos, una conversión falló como se documentó anteriormente, los datos erróneos no se leen en la transmisión y pueden tratarse erróneamente como si fueran parte de un campo diferente.

No es más fácil de usar scanf y amigos que usar fgets. Si verificamos el éxito buscando un '\n' cuando estamos usando fgets o inspeccionando el valor de retorno cuando usamos scanf y amigos, y encontramos que hemos leído una línea incompleta usando fgets o no pudo leer un campo usando scanfentonces nos enfrentamos a la misma realidad: es probable que descartar entrada (generalmente hasta e incluyendo la siguiente nueva línea)! ¡Yuuuuuuck!

Desafortunadamente, scanf ambos simultáneamente hacen que sea difícil (no intuitivo) y fácil (menos pulsaciones de teclas) descartar la entrada de esta manera. Ante esta realidad de descartar la entrada del usuario, algunos han intentado scanf("%*[^\n]%*c");sin darse cuenta de que el %*[^\n] delegar fallará cuando no encuentre nada más que una nueva línea y, por lo tanto, la nueva línea seguirá estando en la transmisión.

Una ligera adaptación, al separar los dos delegados de formato y vemos cierto éxito aquí: scanf("%*[^\n]"); getchar();. Intente hacer eso con tan pocas pulsaciones de teclas usando alguna otra herramienta;)

avatar de usuario
paxdiablo

Los problemas con scanf son (como mínimo):

  • utilizando %s para obtener una cadena del usuario, lo que lleva a la posibilidad de que la cadena sea más larga que su búfer, causando un desbordamiento.
  • la posibilidad de que un escaneo fallido deje el puntero de su archivo en una ubicación indeterminada.

Prefiero mucho usar fgets para leer líneas completas para que pueda limitar la cantidad de datos leídos. Si tiene un búfer de 1K y lee una línea en él con fgets puede saber si la línea era demasiado larga por el hecho de que no hay un carácter de nueva línea de terminación (a pesar de la última línea de un archivo sin una nueva línea).

Luego puede quejarse al usuario o asignar más espacio para el resto de la línea (continuamente si es necesario hasta que tenga suficiente espacio). En cualquier caso, no hay riesgo de desbordamiento del búfer.

Una vez que haya leído la línea, usted saber que estás posicionado en la siguiente línea, así que no hay problema allí. entonces puedes sscanf su cadena al contenido de su corazón sin tener que guardar y restaurar el puntero del archivo para volver a leer.

Aquí hay un fragmento de código que uso con frecuencia para asegurar que no se desborde el búfer cuando le pido información al usuario.

Podría ajustarse fácilmente para usar un archivo que no sea la entrada estándar si es necesario y también podría hacer que asigne su propio búfer (y seguir aumentándolo hasta que sea lo suficientemente grande) antes de devolvérselo a la persona que llama (aunque la persona que llama sería entonces responsable por liberarlo, claro).

#include <stdio.h>
#include <string.h>

#define OK         0
#define NO_INPUT   1
#define TOO_LONG   2
#define SMALL_BUFF 3
static int getLine (char *prmpt, char *buff, size_t sz) {
    int ch, extra;

    // Size zero or one cannot store enough, so don't even
    // try - we need space for at least newline and terminator.

    if (sz < 2)
        return SMALL_BUFF;

    // Output prompt.

    if (prmpt != NULL) {
        printf ("%s", prmpt);
        fflush (stdout);
    }

    // Get line with buffer overrun protection.

    if (fgets (buff, sz, stdin) == NULL)
        return NO_INPUT;

    // Catch possibility of `\0` in the input stream.

    size_t len = strlen(buff);
    if (len < 1)
        return NO_INPUT;

    // If it was too long, there'll be no newline. In that case, we flush
    // to end of line so that excess doesn't affect the next call.

    if (buff[len - 1] != '\n') {
        extra = 0;
        while (((ch = getchar()) != '\n') && (ch != EOF))
            extra = 1;
        return (extra == 1) ? TOO_LONG : OK;
    }

    // Otherwise remove newline and give string back to caller.
    buff[len - 1] = '\0';
    return OK;
}

Y, un controlador de prueba para ello:

// Test program for getLine().

int main (void) {
    int rc;
    char buff[10];

    rc = getLine ("Enter string> ", buff, sizeof(buff));
    if (rc == NO_INPUT) {
        // Extra NL since my system doesn't output that on EOF.
        printf ("\nNo input\n");
        return 1;
    }

    if (rc == TOO_LONG) {
        printf ("Input too long [%s]\n", buff);
        return 1;
    }

    printf ("OK [%s]\n", buff);

    return 0;
}

Finalmente, una prueba para mostrarlo en acción:

$ printf "\0" | ./tstprg     # Singular NUL in input stream.
Enter string>
No input

$ ./tstprg < /dev/null       # EOF in input stream.
Enter string>
No input

$ ./tstprg                   # A one-character string.
Enter string> a
OK [a]

$ ./tstprg                   # Longer string but still able to fit.
Enter string> hello
OK [hello]

$ ./tstprg                   # Too long for buffer.
Enter string> hello there
Input too long [hello the]

$ ./tstprg                   # Test limit of buffer.
Enter string> 123456789
OK [123456789]

$ ./tstprg                   # Test just over limit.
Enter string> 1234567890
Input too long [123456789]

  • if (fgets (buff, sz, stdin) == NULL) return NO_INPUT; ¿Por qué usaste NO_INPUT como valor de retorno? fgets devoluciones NULL solo por error.

    – Fabio Carello

    29 de marzo de 2013 a las 17:19


  • @Fabio, no del todo. También devuelve nulo si la secuencia se cierra antes de que se haya realizado ninguna entrada. Ese es el caso de ser atrapado aquí. No cometa el error de que NO_INPUT significa entrada vacía (presionando ENTER antes que cualquier otra cosa); este último le da una cadena vacía sin código de error NO_INPUT.

    – pax diablo

    29 de marzo de 2013 a las 23:21


  • El último estándar POSIX permite char *buf; scanf("%ms", &buf); que asignará suficiente espacio para usted con malloc (por lo que debe liberarse más tarde), lo que ayudaría a evitar el desbordamiento del búfer.

    – sueño relajado

    3 de octubre de 2014 a las 1:14

  • ¿Qué pasa si llamamos? getLine con 1 como el sz ¿parámetro? if (buff[strlen(buff)-1] != '\n') es donde ocurre el problema. Quizás if (!sz) { return TOO_LONG; } if (buff[sz = strcspn(buff, "\n")] == '\n' || getchar() == '\n') { buff[sz] = '\0'; return OK; } unsigned char c; while (fread(&c, 1, 1, stdin) == 1 && c != '\n'); return TOO_LONG; que de hecho no desborda cuando pasas sz <= 1 y tiene el beneficio adicional de eliminar el '\n' para usted sin gastos generales, aunque debe tenerse en cuenta que su código podría mejorarse mediante el uso estratégico de scanf

    – autista

    11 de julio de 2018 a las 23:21

  • Esa es una buena captura, @chux, agregué un control adicional para tratarlo como “sin entrada”. La prueba se hizo con printf "\0" | exeName para verificar el problema original y solucionarlo. Supongo que nunca verifiqué con un escenario de entrada loco como ese (pero maldita sea debería tener). Gracias por el aviso.

    – pax diablo

    1 de septiembre de 2020 a las 2:00


avatar de usuario
Juan Bode

problemas que tengo con el *scanf() familia:

  • Posibilidad de desbordamiento de búfer con %s y %[ conversion specifiers. Yes, you can specify a maximum field width, but unlike with printf(), you can’t make it an argument in the scanf() call; it must be hardcoded in the conversion specifier.
  • Potential for arithmetic overflow with %d, %i, etc.
  • Limited ability to detect and reject badly formed input. For example, “12w4” is not a valid integer, but scanf("%d", &value); will successfully convert and assign 12 to value, leaving the “w4” stuck in the input stream to foul up a future read. Ideally the entire input string should be rejected, but scanf() doesn’t give you an easy mechanism to do that.

If you know your input is always going to be well-formed with fixed-length strings and numerical values that don’t flirt with overflow, then scanf() is a great tool. If you’re dealing with interactive input or input that isn’t guaranteed to be well-formed, then use something else.

  • if (fgets (buff, sz, stdin) == NULL) return NO_INPUT; Why did you use NO_INPUT as return value? fgets returns NULL only on error.

    – Fabio Carello

    Mar 29, 2013 at 17:19


  • @Fabio, not quite. It also returns null if the stream is closed before any input has been made. That’s the case being caught here. Don’t make the mistake that NO_INPUT means empty input (pressing ENTER before anything else) – the latter gives you an empty string with no NO_INPUT error code.

    – paxdiablo

    Mar 29, 2013 at 23:21


  • The latest POSIX standard allows char *buf; scanf("%ms", &buf); which will allocate enough space for you with malloc (so it must be freed later), which would help prevent buffer overruns.

    – dreamlax

    Oct 3, 2014 at 1:14

  • What happens if we call getLine with 1 as the sz parameter? if (buff[strlen(buff)-1] != '\n') es donde ocurre el problema. Quizás if (!sz) { return TOO_LONG; } if (buff[sz = strcspn(buff, "\n")] == '\n' || getchar() == '\n') { buff[sz] = '\0'; return OK; } unsigned char c; while (fread(&c, 1, 1, stdin) == 1 && c != '\n'); return TOO_LONG; que de hecho no desborda cuando pasas sz <= 1 y tiene el beneficio adicional de eliminar el '\n' para usted sin gastos generales, aunque debe tenerse en cuenta que su código podría mejorarse mediante el uso estratégico de scanf

    – autista

    11 de julio de 2018 a las 23:21

  • Esa es una buena captura, @chux, agregué un control adicional para tratarlo como “sin entrada”. La prueba se hizo con printf "\0" | exeName para verificar el problema original y solucionarlo. Supongo que nunca verifiqué con un escenario de entrada loco como ese (pero maldita sea debería tener). Gracias por el aviso.

    – pax diablo

    1 de septiembre de 2020 a las 2:00


avatar de usuario
Sueño relajado

Muchas respuestas aquí discuten los posibles problemas de desbordamiento del uso scanf("%s", buf)pero la especificación POSIX más reciente resuelve más o menos este problema proporcionando un m carácter de asignación-asignación que se puede utilizar en especificadores de formato para c, sy [ formats. This will allow scanf to allocate as much memory as necessary with malloc (so it must be freed later with free).

An example of its use:

char *buf;
scanf("%ms", &buf); // with 'm', scanf expects a pointer to pointer to char.

// use buf

free(buf);

See here. Disadvantages to this approach is that it is a relatively recent addition to the POSIX specification and it is not specified in the C specification at all, so it remains rather unportable for now.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad