Desventajas de scanf
Quiero saber las desventajas de scanf()
.
En muchos sitios, he leído que el uso scanf
puede provocar desbordamientos del búfer. ¿Cuál es la razón para esto? ¿ Existen otros inconvenientes con scanf
?
La mayoría de las respuestas hasta ahora parecen centrarse en el problema del desbordamiento del búfer de cadenas. En realidad, los especificadores de formato que se pueden usar con scanf
funciones admiten la configuración explícita del ancho de campo , que limita el tamaño máximo de la entrada y evita el desbordamiento del búfer. Esto hace que las acusaciones populares sobre los peligros de desbordamiento del buffer de cadena presentes sean scanf
prácticamente infundadas. Afirmar que scanf
de alguna manera es análogo a gets
in the respect es completamente incorrecto. Existe una diferencia cualitativa importante entre scanf
y gets
: scanf
proporciona al usuario funciones para prevenir el desbordamiento del búfer de cadena, mientras que gets
no.
Se puede argumentar que estas scanf
características son difíciles de usar, ya que el ancho del campo debe estar incrustado en la cadena de formato (no hay manera de pasarlo a través de un argumento variado, como se puede hacer en printf
). Eso es realmente cierto. scanf
de hecho está bastante mal diseñado en ese sentido. Sin embargo, cualquier afirmación que scanf
de alguna manera esté irremediablemente incumplida con respecto a la seguridad del desbordamiento del búfer de cadenas es completamente falsa y generalmente la hacen programadores perezosos.
El verdadero problema scanf
tiene una naturaleza completamente diferente, aunque también se trata de desbordamiento . Cuando scanf
la función 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, scanf
produce un comportamiento indefinido. Por esta razón, la única forma adecuada de realizar la conversión en la biblioteca estándar de C son las funciones de strto...
la familia.
Entonces, para resumir lo anterior, el problema scanf
es que es difícil (aunque posible) usarlo de manera adecuada y segura con buffers de cadena. Y es imposible utilizarlo de forma segura para entradas aritméticas. Este último es el verdadero problema. Lo primero es sólo un inconveniente.
PD: Lo anterior pretende referirse a toda la familia de scanf
funciones (incluidas también fscanf
y sscanf
). Específicamente scanf
, el problema obvio es que la idea misma de usar una función con formato estricto para leer entradas potencialmente interactivas es bastante cuestionable.
Los problemas con scanf son (como mínimo):
- usando
%s
para obtener una cadena del usuario, lo que genera la posibilidad de que la cadena sea más larga que su búfer, provocando un desbordamiento. - la posibilidad de que un análisis fallido deje el puntero del archivo en una ubicación indeterminada.
Prefiero usar fgets
para leer líneas completas para poder limitar la cantidad de datos leídos. Si tiene un búfer de 1K y lee una línea, fgets
puede saber si la línea era demasiado larga por el hecho de que no hay un carácter de nueva línea final (a pesar de la última línea de un archivo sin una nueva línea).
Luego puedes quejarte con el usuario o asignar más espacio para el resto de la línea (de forma continua si es necesario hasta que tengas suficiente espacio). En cualquier caso, no hay riesgo de desbordamiento del búfer.
Una vez que hayas leído la línea, sabrás que estás ubicado en la siguiente línea, por lo que no hay ningún problema allí. Luego puede configurar sscanf
su cadena al contenido de su corazón sin tener que guardar y restaurar el puntero del archivo para volver a leerlo.
Aquí hay un fragmento de código que uso con frecuencia para garantizar 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 asigne su propio búfer (y seguir incrementándolo hasta que sea lo suficientemente grande) antes de devolvérselo a la persona que llama (aunque la persona que llama entonces sería responsable). por liberarlo, por supuesto).
#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 piloto de pruebas 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]
De las preguntas frecuentes de comp.lang.c: ¿ Por qué todo el mundo dice que no se use scanf? ¿Qué debería usar en su lugar?
scanf
tiene varios problemas; consulte las preguntas 12.17 , 12.18a y 12.19 . Además, su%s
formato tiene el mismo problema quegets()
tiene (consulte la pregunta 12.23 ): es difícil garantizar que el búfer de recepción no se desborde. [nota]De manera más general,
scanf
está diseñado para entradas formateadas y relativamente estructuradas (de hecho, su nombre deriva de “escaneo formateado”). Si prestas atención, te dirá si tuvo éxito o fracasó, pero sólo puede decirte aproximadamente dónde falló, y no cómo ni por qué. Tiene muy pocas oportunidades de realizar alguna 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á al usuario escribir casi cualquier cosa, no sólo letras o signos de puntuación cuando se esperaban dígitos, sino también más o menos caracteres de los esperados, o ningún carácter (es decir, solo el botón RETURN ). clave), o EOF prematuro, o cualquier cosa. Es casi imposible lidiar con todos estos problemas potenciales al usar
scanf
; Es mucho más fácil leer líneas completas (confgets
o similares) y luego interpretarlas, ya sea usandosscanf
otras técnicas. (Funciones comostrtol
,strtok
yatoi
suelen ser útiles; consulte también las preguntas 12.16 y 13.6 .) Si utiliza algunascanf
variante, asegúrese de verificar el valor de retorno para asegurarse de que se encontró la cantidad esperada de elementos. Además, si usa%s
, asegúrese de protegerse contra el desbordamiento del búfer.Tenga en cuenta, por cierto, que las críticas a
scanf
no son necesariamente acusaciones afscanf
ysscanf
.scanf
lee desdestdin
, que suele ser un teclado interactivo y, por lo tanto, es el menos restringido y genera la mayor cantidad de problemas. Por otro lado, cuando un archivo de datos tiene un formato conocido, puede ser apropiado leerlo confscanf
. Es perfectamente apropiado analizar cadenassscanf
(siempre que se verifique el valor de retorno), porque es muy fácil recuperar el control, reiniciar el análisis, descartar la entrada si no coincide, etc.Enlaces adicionales:
- explicación más larga de Chris Torek
- explicación más larga por parte de un servidor
Referencias: K&R2 Sec. 7,4p. 159
Es muy difícil llegar scanf
a hacer lo que quieres. Claro que puedes, pero cosas como scanf("%s", buf);
son tan peligrosas gets(buf);
como todo el mundo ha dicho.
Como ejemplo, lo que está haciendo paxdiablo en su función de leer se puede hacer con algo como:
scanf("%10[^\n]%*[^\n]", buf));
getchar();
Lo anterior leerá una línea, almacenará los primeros 10 caracteres que no sean de nueva línea buf
y luego descartará todo hasta (incluido) una nueva línea. Entonces, la función de paxdiablo podría escribirse de scanf
la siguiente manera:
#include <stdio.h>
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 scanf
es su comportamiento en caso de desbordamiento. Por ejemplo, al leer un int
:
int i;
scanf("%d", &i);
lo anterior no se puede utilizar de forma segura en caso de desbordamiento. Incluso en el primer caso, leer una cadena es mucho más sencillo que hacerlo fgets
con scanf
.
Sí, tiene usted razón. Hay una falla de seguridad importante en scanf
family( scanf
, sscanf
, fscanf
..etc), especialmente cuando se lee una cadena, porque no tienen en cuenta la longitud del buffer (en el que están leyendo).
Ejemplo:
char buf[3];
sscanf("abcdef","%s",buf);
Claramente, el búfer buf
puede contener 3
caracteres MAX. Pero sscanf
intentará ingresarlo "abcdef"
provocando un desbordamiento del búfer.