Varias preguntas se han ocupado de lo que has hecho mal y cómo solucionarlo, pero también se ha dicho (el énfasis es mío):
alguien puede explicar por qué, y por qué este estilo de la codificación es mala
Creo scanf
es una terrible manera de leer la entrada. Es inconsistente con printf
, hace que sea fácil olvidarse de buscar errores, hace que sea difícil recuperarse de errores, y es incompatible con operaciones de lectura ordinarias (y más fáciles de hacer correctamente) (como fgets
y compañía).
Primero, tenga en cuenta que el formato "%s"
solo se leerá hasta que vea espacios en blanco. ¿Por qué espacios en blanco? ¿Por qué "%s"
imprime una cadena completa, pero lee en cadenas con una capacidad limitada?
Si desea leer en una línea completa, como a menudo lo hará, scanf
proporciona ... con "%[^\n]"
. ¿Qué? ¿Que es eso? ¿Cuándo se convirtió esto en Perl?
Pero el problema real es que ninguna de ellas es segura. Ambos se desbordan sin límites y sin límites. ¿Desea verificar los límites? De acuerdo, lo tienes: "%10s"
(y "%10[^\n]"
está empezando a verse peor). Eso solo leerá 9 caracteres, y agregará un nul-character de terminación automáticamente. Así que eso es bueno ... cuando nuestra matriz de tamaño nunca necesite cambiar.
¿Qué sucede si queremos pasar el tamaño de nuestra matriz como argumento al scanf
? printf
puede hacer esto:
char string[] = "Hello, world!";
printf("%.*s\n", sizeof string, string); // prints whole message;
printf("%.*s\n", 6, string); // prints just "Hello,"
quieren hacer lo mismo con scanf
? Así es como:
static char tmp[/*bit twiddling to get the log10 of SIZE_MAX plus a few*/];
// if we did the math right we shouldn't need to use snprintf
snprintf(tmp, sizeof tmp, "%%%us", bufsize);
scanf(tmp, buffer);
Así es - scanf
no es compatible con la precisión "%.*s"
variable de printf
hace, por lo que hacer con los límites dinámicos de cheques scanf
tenemos que construir nuestra propia cadena de formato en un buffer temporal. Esto es todo tipo de problemas, y aunque en realidad es seguro aquí, parecerá una muy mala idea para cualquiera que esté de visita.
Mientras tanto, echemos un vistazo a otro mundo. Veamos el mundo de fgets
. Así es como se lee en una línea de datos con fgets
:
fgets(buffer, bufsize, stdin);
infinitamente menos dolor de cabeza, sin tiempo de procesador desperdiciado convertir una precisión de número entero en una cadena que sólo será reparsed por la biblioteca de nuevo en un entero, y todo el los elementos relevantes están sentados allí en una línea para que podamos ver cómo funcionan juntos.
concedido, esto puede no leer una línea entera. Solo leerá una línea completa si la línea es más corta que bufsize - 1
caracteres. Así es como podemos leer una línea completa:
char *readline(FILE *file)
{
size_t size = 80; // start off small
size_t curr = 0;
char *buffer = malloc(size);
while(fgets(buffer + curr, size - curr, file))
{
if(strchr(buffer + curr, '\n')) return buffer; // success
curr = size - 1;
size *= 2;
char *tmp = realloc(buffer, size);
if(tmp == NULL) /* handle error */;
buffer = tmp;
}
/* handle error */;
}
La variable curr
es una optimización que nos impida volver a comprobar los datos que ya hemos leído, y no es necesario (aunque útiles como leemos más datos). Incluso podríamos usar el valor de retorno de strchr
para quitar el carácter "\n"
final si lo prefiere.
Observe también que size_t size = 80;
como punto de partida es completamente arbitrario. Podríamos usar 81, o 79, o 100, o agregarlo como un argumento proporcionado por el usuario a la función. Incluso podríamos agregar un argumento int (*inc)(int)
, y cambiar size *= 2;
a size = inc(size);
, lo que permite al usuario controlar qué tan rápido crece la matriz. Estos pueden ser útiles para la eficiencia, cuando las reasignaciones son costosas y es necesario leer y procesar gran cantidad de líneas de datos.
Podríamos escribir lo mismo con scanf
, pero piense en cuántas veces tendríamos que volver a escribir la cadena de formato.Podríamos limitarlo a un incremento constante, en lugar del doble (fácil) implementado anteriormente, y nunca tener que ajustar la cadena de formato; Podríamos ceder y almacenar el número, hacer los cálculos con los datos anteriores y usar snprintf
para convertirlo a un formato de cadena cada vez que reasignamos para que scanf
pueda convertirlo de nuevo al mismo número; podríamos limitar nuestro crecimiento y posición inicial de tal forma que podamos ajustar manualmente la cadena de formato (digamos, simplemente incrementar los dígitos), pero esto podría volverse peludo después de un tiempo y puede requerir recurrencia (!) para funcionar limpiamente.
Además, es difícil mezclar la lectura con scanf
con la lectura con otras funciones. ¿Por qué? Supongamos que quiere leer un número entero de una línea, luego lea una cadena de la siguiente línea. Intenta esto:
int i;
char buf[BUSIZE];
scanf("%i", &i);
fgets(buf, BUFSIZE, stdin);
que leerá el "2", pero luego fgets
leerá una línea vacía porque scanf
no leyó el salto de línea! Está bien, tomar dos:
...
scanf("%i\n", &i);
...
Se piensa que esto se come el salto de línea, y lo hace - pero también se come conduce un espacio en blanco en la línea siguiente, porque scanf
no puede decir la diferencia entre los saltos de línea y otras formas de espacio en blanco. (. Además, resulta que usted está escribiendo un analizador de Python, y llevando los espacios en blanco en las líneas es importante) Para que esto funcione, usted tiene que llamar getchar
o algo para leer en la nueva línea y tirar a la basura que:
...
scanf("%i", &i);
getchar();
...
¿No es tonto? ¿Qué sucede si usa scanf
en una función, pero no llama al getchar
porque no sabe si la próxima lectura va a ser scanf
o algo más correcto (o si el próximo carácter será incluso una nueva línea) ? De repente, la mejor manera de manejar la situación parece ser escoger uno u otro: ¿usamos scanf
exclusivamente y nunca tenemos acceso a fgets
-estilo de entrada de control total, o usamos fgets
exclusivamente y hacemos más difícil realizar un análisis complejo ?
En realidad, la respuesta es no lo hacemos. Usamos fgets
(o funciones que no son scanf
) exclusivamente, y cuando necesitamos scanf
, como funcionalidad, , ¡simplemente llamamos al sscanf
en las cadenas! No necesitamos tener scanf
¡vaciando nuestros archivos innecesariamente! Podemos tener todo el control preciso sobre nuestra entrada que queremos y todavía obtener toda la funcionalidad del formato scanf
. Y aunque no pudiéramos, muchas opciones de formato scanf
tienen funciones casi directas correspondientes en la biblioteca estándar, como las funciones strtol
y strtod
infinitamente más flexibles (y amigos). Además, i = strtoumax(str, NULL)
para tipos enteros de tamaño C99 es mucho más limpia que scanf("%" SCNuMAX, &i);
, y mucho más segura (podemos usar esa línea strtoumax
sin cambios para tipos más pequeños y dejar que la conversión implícita maneje los bits adicionales, pero con scanf
tenemos que hacer una uintmax_t
para leer en).
La moraleja de esta historia: evitar scanf
. Si necesita el formato que proporciona, y no desea (o no puede) hacerlo (más eficientemente) usted mismo, use fgets
/sscanf
.
también necesitan stdlib para malloc! –
@fehergeri: sí, y stdio para usar 'scanf'. Supongo que los encabezados han sido omitidos por propósito. – peoro
Preferiría hacer 'receiveInput (char * s, size_t len)' pero luego necesitaría pasar por algunos aros construyendo la cadena de formato. –