Desventajas de scanf

Quiero saber las desventajas de scanf() .

En muchos sitios, he leído que usar scanf puede causar desbordamientos de buffer. ¿Cuál es la razón para esto? ¿Hay algún otro inconveniente con scanf ?

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 y cause un desbordamiento.
  • la posibilidad de un análisis fallido que deja su puntero de archivo en una ubicación indeterminada.

Prefiero usar fgets para leer líneas enteras para que pueda limitar la cantidad de datos leídos. Si tienes un buffer 1K, y lees una línea con fgets puedes decir si la línea fue demasiado larga por el hecho de que no hay un carácter de nueva línea de finalización (a pesar de la última línea de un archivo sin una nueva línea).

Luego puede presentar una queja al usuario o asignar más espacio para el rest 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, sabrá que se encuentra en la siguiente línea, por lo que no hay problema. A continuación, puede sscanf la 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 frecuentemente para asegurar que no se desborde el búfer cuando le pido información al usuario.

Se podría ajustar fácilmente para usar un archivo que no sea la entrada estándar si es necesario y también podría hacer que asignara su propio búfer (y seguir aumentando hasta que sea lo suficientemente grande) antes de devolverlo a la persona que llama (aunque la persona que llama sería responsable) para liberarlo, por supuesto).

 #include  #include  #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; // 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. size_t lastPos = strlen(buff) - 1; if (buff[lastPos] != '\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[lastPos] = '\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 ejecución de prueba para mostrarlo en acción:

 $ ./tstprg Enter string>[CTRL-D] No input $ ./tstprg Enter string> a OK [a] $ ./tstprg Enter string> hello OK [hello] $ ./tstprg Enter string> hello there Input too long [hello the] $ ./tstprg Enter string> i am pax OK [i am pax] 

La mayoría de las respuestas hasta ahora parecen centrarse en el problema del desbordamiento del búfer de cadena. En realidad, los especificadores de formato que se pueden usar con las funciones de scanf admiten la configuración de ancho de campo explícito, que limita el tamaño máximo de la entrada y evita el desbordamiento del búfer. Esto hace que las acusaciones populares de los peligros de desbordamiento del buffer de cadena presentes en scanf virtualmente infundadas. Afirmar que scanf es de alguna manera análogo a gets en el respeto es completamente incorrecto. Hay una gran diferencia cualitativa entre scanf y gets : scanf proporciona al usuario funciones de prevención de desbordamiento de búfer de cadena, mientras que gets no lo hace.

Se puede argumentar que estas características scanf son difíciles de usar, ya que el ancho del campo debe estar incrustado en la cadena de formato (no hay forma de pasarlo a través de un argumento variódico, como se puede hacer en printf ). Eso es realmente cierto. scanf está bastante mal diseñado en ese sentido. Pero, no obstante, cualquier afirmación de que scanf esté irremediablemente roto con respecto a la seguridad del desbordamiento del buffer de cadena es completamente falsa y generalmente es hecha por progtwigdores perezosos.

El verdadero problema con scanf tiene una naturaleza completamente diferente, aunque también se trata de desbordamiento . Cuando la función scanf se utiliza para convertir representaciones decimales de números en valores de tipos aritméticos, no proporciona protección contra el desbordamiento aritmético. Si ocurre un desbordamiento, el scanf produce un comportamiento indefinido. Por este motivo, la única forma adecuada de realizar la conversión en la biblioteca estándar C es desde la familia strto...

Entonces, para resumir lo anterior, el problema con scanf es que es difícil (aunque posible) usar de manera adecuada y segura con almacenamientos intermedios de cadenas. Y es imposible de usar de forma segura para la entrada aritmética. Este último es el problema real. El primero es solo un inconveniente.

PD Lo anterior en la intención de ser sobre toda la familia de funciones scanf (incluyendo también fscanf y sscanf ). Con scanf específicamente, el problema obvio es que la sola idea de usar una función con formato estricto para leer datos potencialmente interactivos es bastante cuestionable.

De las preguntas frecuentes de comp.lang.c: ¿Por qué todos dicen no usar scanf? ¿Qué debería usar en su lugar?

scanf tiene una serie de problemas: vea las preguntas 12.17 , 12.18a y 12.19 . Además, su formato %s tiene el mismo problema que gets() tiene (vea la pregunta 12.23 ): es difícil garantizar que el búfer de recepción no se desborde. [nota]

En términos más generales, scanf está diseñado para una entrada formateada relativamente estructurada (su nombre se deriva de “escaneado formateado”). Si presta atención, le dirá si tuvo éxito o no, pero puede decirle solo aproximadamente dónde falló, y en absoluto cómo o por qué. Tiene muy pocas oportunidades de realizar una recuperación de errores.

Sin embargo, la entrada interactiva del usuario es la entrada menos estructurada que existe. Una interfaz de usuario bien diseñada permitirá la posibilidad de que el usuario escriba casi cualquier cosa, no solo letras o signos de puntuación cuando se esperaban los dígitos, sino también más o menos caracteres de los que se esperaban, o ningún carácter ( es decir , solo el RETORNO). clave), o EOF prematuro, o cualquier cosa. Es casi imposible tratar con gracia todos estos posibles problemas cuando se usa scanf ; es mucho más fácil leer líneas enteras (con fgets o similares), y luego interpretarlas, ya sea utilizando sscanf o algunas otras técnicas. (Las funciones como strtol , atoi y atoi son a menudo útiles, consulte también las preguntas 12.16 y 13.6 ). Si utiliza cualquier variante de scanf , asegúrese de verificar el valor de retorno para asegurarse de que se encontró el número esperado de elementos. Además, si usa %s , asegúrese de evitar el desbordamiento del búfer.

Tenga en cuenta, por cierto, que las críticas de scanf no son necesariamente acusaciones de fscanf y sscanf . scanf lee de stdin , que generalmente es un teclado interactivo y, por lo tanto, es el menos restringido, lo que genera la mayoría de los problemas. Cuando un archivo de datos tiene un formato conocido, por otro lado, puede ser apropiado leerlo con fscanf . Es perfectamente apropiado analizar cadenas con sscanf (siempre que se sscanf el valor de retorno), porque es muy fácil recuperar el control, reiniciar el escaneo, descartar la entrada si no coincide, etc.

Enlaces adicionales:

  • explicación más larga por Chris Torek
  • explicación más larga por los tuyos verdaderamente

Referencias: K & R2 Sec. 7.4 p. 159

Sí, tiene usted razón. Hay una falla importante de seguridad en la familia scanf ( scanf , sscanf , fscanf ..etc) esp al leer una cadena, porque no toman en cuenta la longitud del búfer (en el que están leyendo).

Ejemplo:

 char buf[3]; sscanf("abcdef","%s",buf); 

claramente el tampón buf puede contener MAX 3 char. Pero el sscanf intentará poner "abcdef" en él causando un desbordamiento del buffer.

Es muy difícil obtener scanf para hacer lo que quieras. Claro, puedes, pero cosas como scanf("%s", buf); son tan peligrosos como gets(buf); como todos han dicho

Como ejemplo, lo que paxdiablo está haciendo en su función para leer se puede hacer con algo como:

 scanf("%10[^\n]%*[^\n]", buf)); getchar(); 

Lo anterior leerá una línea, almacenará los primeros 10 caracteres no nuevos en buf , y luego descartará todo hasta (e incluyendo) una nueva línea. Entonces, la función de paxdiablo podría escribirse usando scanf la siguiente manera:

 #include  enum read_status { OK, NO_INPUT, TOO_LONG }; static int get_line(const char *prompt, char *buf, size_t sz) { char fmt[40]; int i; int nscanned; printf("%s", prompt); fflush(stdout); sprintf(fmt, "%%%zu[^\n]%%*[^\n]%%n", sz-1); /* read at most sz-1 characters on, discarding the rest */ i = scanf(fmt, buf, &nscanned); if (i > 0) { getchar(); if (nscanned >= sz) { return TOO_LONG; } else { return OK; } } else { return NO_INPUT; } } int main(void) { char buf[10+1]; int rc; while ((rc = get_line("Enter string> ", buf, sizeof buf)) != NO_INPUT) { if (rc == TOO_LONG) { printf("Input too long: "); } printf("->%s<-\n", buf); } return 0; } 

Uno de los otros problemas con scanf es su comportamiento en caso de desbordamiento. Por ejemplo, al leer un int :

 int i; scanf("%d", &i); 

lo anterior no se puede usar con seguridad en caso de un desbordamiento. Incluso para el primer caso, leer una cadena es mucho más simple de hacer con fgets lugar de con scanf .

Problemas que tengo con la familia *scanf() :

  • Potencial de desbordamiento de búfer con% s y% [especificadores de conversión. Sí, puede especificar un ancho de campo máximo, pero a diferencia de printf() , no puede convertirlo en un argumento en la llamada scanf() ; debe estar codificado en el especificador de conversión.
  • Potencial de desbordamiento aritmético con% d,% i, etc.
  • Capacidad limitada para detectar y rechazar entradas mal formadas. Por ejemplo, “12w4” no es un entero válido, sino scanf("%d", &value); convertirá con éxito y asignará 12 a value , dejando el “w4” atascado en la stream de entrada para ensuciar una lectura futura. Idealmente, toda la cadena de entrada debería ser rechazada, pero scanf() no le proporciona un mecanismo fácil para hacerlo.

Si sabe que su entrada siempre estará bien formada con cadenas de longitud fija y valores numéricos que no coquetean con el desbordamiento, entonces scanf() es una gran herramienta. Si se trata de entradas o entradas interactivas que no se garantiza que estén bien formadas, entonces use algo más.

Hay un gran problema con las funciones tipo scanf , la falta de cualquier tipo de seguridad. Es decir, puedes codificar esto:

 int i; scanf("%10s", &i); 

Demonios, incluso esto está “bien”:

 scanf("%10s", i); 

Es peor que las funciones tipo printf , porque scanf espera un puntero, por lo que los lockings son más probables.

Claro, hay algunos verificadores de especificador de formato por ahí, pero esos no son perfectos y bueno, no son parte del lenguaje o de la biblioteca estándar.

La ventaja de scanf es que una vez que aprendes cómo usar la herramienta, como siempre debes hacer en C, tiene usos muy útiles. Puede aprender cómo usar scanf y sus amigos leyendo y comprendiendo el manual . Si no puede leer ese manual sin problemas serios de comprensión, esto probablemente indique que usted no conoce muy bien a C.


scanf y sus amigos sufrieron desafortunadas elecciones de diseño que hicieron difícil (y en ocasiones imposible) usar correctamente sin leer la documentación, como han demostrado otras respuestas. Esto ocurre a lo largo de C, desafortunadamente, así que si tuviera que desaconsejar el uso de scanf entonces probablemente desaconsejaría usar C.

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

Por ejemplo, los no iniciados suelen esperar que el delegado %s haga que se lea una línea , y aunque parezca intuitivo, no necesariamente es 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 la respuesta a esta pregunta sin mencionar su falta de seguridad y el riesgo de desbordamientos de buffer? Como ya hemos explicado, C no es un lenguaje seguro, y nos permitirá tomar atajos, posiblemente para aplicar una optimización a costa de la corrección o más probablemente porque somos progtwigdores perezosos. Por lo tanto, cuando sabemos que el sistema nunca recibirá una cadena más grande que un número fijo de bytes, se nos da la posibilidad de declarar una matriz de ese tamaño y evitar la verificación de límites. Realmente no veo esto como una caída hacia abajo; es una opción. Nuevamente, se recomienda encarecidamente leer el manual y revelar esta opción para nosotros.

Los progtwigdores perezosos no son los únicos afectados por scanf . No es raro ver personas tratando de leer float o valores double usando %d , por ejemplo. Por lo general, se equivocan al creer que la implementación llevará a cabo algún tipo de conversión entre bastidores, lo que tendría sentido porque las conversiones similares ocurren en el rest del idioma, pero ese no es el caso aquí. Como dije antes, scanf y amigos (y de hecho el rest de C) son engañosos; parecen sucintos e idiomáticos, pero no lo son.

Los progtwigdores inexpertos no están obligados a considerar el éxito de la operación . Supongamos que el usuario introduce algo completamente no numérico cuando le hemos dicho a scanf que lea y convierta una secuencia de dígitos decimales usando %d . La única forma en que podemos interceptar estos datos erróneos es verificar el valor de retorno, y ¿con qué frecuencia nos molestamos en verificar el valor de retorno?

Al igual que los fgets , cuando scanf y amigos no pueden leer lo que se les dice que deben leer, la transmisión se dejará en un estado inusual; – En el caso de los fgets , si no hay suficiente espacio para almacenar una línea completa, el rest de la línea que no se haya leído podría tratarse erróneamente como si fuera una nueva línea cuando no lo es. – En el caso de scanf y sus amigos, una conversión falló como se documentó anteriormente, los datos erróneos se dejan sin leer en la transmisión y podrían tratarse erróneamente como si fueran parte de un campo diferente.

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

Desafortunadamente, scanf simultáneamente hace que sea difícil (no intuitivo) y fácil (menor cantidad de teclas) descartar la entrada de esta manera. Frente a esta realidad de descartar la entrada del usuario, algunos han intentado scanf("%*[^\n]%*c"); , sin darse cuenta de que el delegado %*[^\n] fallará cuando no encuentre nada más que una línea nueva, y por lo tanto, la línea nueva seguirá quedando en la secuencia.

Una pequeña adaptación, separando a los dos delegates de formato y vemos cierto éxito aquí: scanf("%*[^\n]"); getchar(); scanf("%*[^\n]"); getchar(); . Intente hacer eso con tan pocas teclas presionando alguna otra herramienta;)

Muchas respuestas aquí discuten los posibles problemas de desbordamiento de usar scanf("%s", buf) , pero la última especificación POSIX resuelve este problema más o menos al proporcionar un m atributo de asignación de asignación que se puede usar en especificadores de formato para c , s , y [ formatos. Esto permitirá que scanf asigne tanta memoria como sea necesario con malloc (por lo que debe liberarse más tarde de forma free ).

Un ejemplo de su uso:

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

Mira aquí . Las desventajas de este enfoque es que es una adición relativamente reciente a la especificación POSIX y no está especificada en absoluto en la especificación C, por lo que sigue siendo poco práctico por ahora.