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
?
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 fgets
cuando 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 scanf
entonces 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 sin darse cuenta de que el scanf("%*[^\n]%*c");
%*[^\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;)
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é usasteNO_INPUT
como valor de retorno?fgets
devolucionesNULL
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 conmalloc
(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
con1
como elsz
¿parámetro?if (buff[strlen(buff)-1] != '\n')
es donde ocurre el problema. Quizásif (!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 pasassz <= 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 descanf
…– 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
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 thescanf()
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 tovalue
, leaving the “w4” stuck in the input stream to foul up a future read. Ideally the entire input string should be rejected, butscanf()
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 useNO_INPUT
as return value?fgets
returnsNULL
only on error.– Fabio CarelloMar 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.
– paxdiabloMar 29, 2013 at 23:21
-
The latest POSIX standard allows
char *buf; scanf("%ms", &buf);
which will allocate enough space for you withmalloc
(so it must be freed later), which would help prevent buffer overruns.– dreamlaxOct 3, 2014 at 1:14
-
What happens if we call
getLine
with1
as thesz
parameter?if (buff[strlen(buff)-1] != '\n')
es donde ocurre el problema. Quizásif (!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 pasassz <= 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 descanf
…– 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
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
, s
y [
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.
Ver también Una guía para principiantes lejos de
scanf()
.–Jonathan Leffler
19 de julio de 2017 a las 4:41