¿Cómo leer/analizar entradas en C? Las preguntas frecuentes
Tengo problemas con mi programa C cuando intento leer/analizar entradas.
¿Ayuda?
Esta es una entrada de preguntas frecuentes.
StackOverflow tiene muchas preguntas relacionadas con la lectura de entradas en C, con respuestas generalmente centradas en el problema específico de ese usuario en particular sin realmente describir el panorama completo.
Este es un intento de cubrir una serie de errores comunes de manera integral, por lo que esta familia específica de preguntas se puede responder simplemente marcándolas como duplicados de ésta:
- ¿Por qué la última línea se imprime dos veces?
- ¿Por qué falla mi
scanf("%d", ...)
/scanf("%c", ...)
? - ¿Por qué
gets()
falla? - ...
La respuesta está marcada como wiki comunitaria. Siéntase libre de mejorar y (con cautela) ampliar.
Introducción a la entrada C para principiantes
- Modo texto versus modo binario
- Verifique fopen() en busca de fallas
- Escollos
- Verifique cualquier función que solicite para tener éxito
- EOF, o "¿por qué la última línea se imprime dos veces?"
- No utilices gets() , nunca
- Nunca utilices fflush() ni
stdin
ninguna otra transmisión abierta para lectura. - No utilice *scanf() para entradas potencialmente mal formadas
- Cuando *scanf() no funciona como se esperaba
- Leer y luego analizar
- Leer (parte de) una línea de entrada mediante fgets()
- Analizar la línea en memoria
- Limpiar
Modo texto versus modo binario
Una secuencia en "modo binario" se lee exactamente como se escribió. Sin embargo, puede haber (o no) un número definido por la implementación de caracteres nulos (' \0
') añadidos al final de la secuencia.
Una secuencia en "modo texto" puede realizar varias transformaciones, que incluyen (entre otras):
- eliminación de espacios inmediatamente antes del final de una línea;
- cambiar nuevas líneas (
'\n'
) a otra cosa en la salida (por ejemplo,"\r\n"
en Windows) y volver a'\n'
la entrada; - agregar, modificar o eliminar caracteres que no sean caracteres de impresión (
isprint(c)
es verdadero), tabulaciones horizontales o líneas nuevas.
Debería ser obvio que el modo texto y binario no se mezclan. Abra archivos de texto en modo texto y archivos binarios en modo binario.
Verifique fopen() en busca de fallas
El intento de abrir un archivo puede fallar por varias razones: la falta de permisos o el archivo no encontrado son las más comunes. En este caso, fopen() devolverá un NULL
puntero. Siempre verifique si fopen
devolvió un NULL
puntero antes de intentar leer o escribir en el archivo.
Cuando fopen
falla, generalmente establece la variable errno global para indicar por qué falló. (Esto técnicamente no es un requisito del lenguaje C, pero tanto POSIX como Windows garantizan hacerlo). errno
es un número de código que se puede comparar con constantes en errno.h
, pero en programas simples, generalmente todo lo que necesita hacer es convertirlo en un mensaje de error e imprimirlo, usando perror()
o strerror()
. El mensaje de error también debe incluir el nombre del archivo que pasó fopen
; Si no lo hace, se sentirá muy confundido cuando el problema sea que el nombre del archivo no sea el que pensaba.
#include <stdio.h>
#include <string.h>
#include <errno.h>
int main(int argc, char **argv)
{
if (argc < 2) {
fprintf(stderr, "usage: %s file\n", argv[0]);
return 1;
}
FILE *fp = fopen(argv[1], "r");
if (!fp) {
// alternatively, just `perror(argv[1])`
fprintf(stderr, "cannot open %s: %s\n", argv[1], strerror(errno));
return 1;
}
// read from fp here
fclose(fp);
return 0;
}
Escollos
Verifique cualquier función que solicite para tener éxito
Esto debería ser obvio. Pero consulte la documentación de cualquier función que llame para conocer su valor de retorno y manejo de errores, y verifique esas condiciones.
Estos son errores que son fáciles cuando se detecta la afección a tiempo, pero que provocan muchos dolores de cabeza si no se detecta.
EOF, o "¿por qué la última línea se imprime dos veces?"
La función feof() regresa true
si se ha alcanzado el EOF. Un malentendido de lo que realmente significa "alcanzar" el EOF hace que muchos principiantes escriban algo como esto:
// BROKEN CODE
while (!feof(fp)) {
fgets(buffer, BUFFER_SIZE, fp);
printf("%s", buffer);
}
Esto hace que la última línea de la entrada se imprima dos veces , porque cuando se lee la última línea (hasta la nueva línea final, el último carácter en el flujo de entrada), EOF no se establece.
¡EOF solo se configura cuando intentas leer más allá del último carácter!
Entonces, el código anterior se repite una vez más, fgets() no puede leer otra línea, establece EOF y deja el contenido buffer
intacto , que luego se imprime nuevamente.
En su lugar, verifique si fgets
falló directamente:
// GOOD CODE
while (fgets(buffer, BUFFER_SIZE, fp)) {
printf("%s", buffer);
}
No utilices gets() , nunca
No hay forma de utilizar esta función de forma segura. Debido a esto, se eliminó del lenguaje con la llegada del C11.
Nunca utilices fflush() ni stdin
ninguna otra transmisión abierta para lectura.
Mucha gente espera fflush(stdin)
descartar la entrada del usuario que aún no ha sido leída. No hace eso. En ISO C simple, llamar a fflush() en un flujo de entrada tiene un comportamiento indefinido . Tiene un comportamiento bien definido en POSIX y MSVC, pero ninguno de ellos descarta la entrada del usuario que aún no se ha leído.
Por lo general, la forma correcta de borrar la entrada pendiente es leer y descartar caracteres hasta una nueva línea incluida, pero no más allá:
int c;
do c = getchar(); while (c != EOF && c != '\n');
No utilice *scanf() para entradas potencialmente mal formadas
Muchos tutoriales te enseñan a usar *scanf() para leer cualquier tipo de entrada, porque es muy versátil.
Pero el propósito de *scanf() es realmente leer datos masivos en los que se puede confiar en que estén en un formato predefinido. (Como estar escrito por otro programa).
Incluso entonces *scanf() puede hacer tropezar a los inobservadores:
- El uso de una cadena de formato que de alguna manera puede ser influenciada por el usuario es un enorme agujero de seguridad.
- Si la entrada no coincide con el formato esperado, *scanf() inmediatamente detiene el análisis, dejando los argumentos restantes sin inicializar.
- Le dirá cuántas asignaciones ha realizado con éxito, razón por la cual debe verificar su código de retorno (ver arriba), pero no exactamente dónde dejó de analizar la entrada, lo que dificulta la recuperación elegante de errores.
- Omite los espacios en blanco iniciales en la entrada, excepto cuando no lo hace (
[
,c
yn
conversiones). (Ver el siguiente párrafo.) - Tiene un comportamiento algo peculiar en algunos casos extremos.
Cuando *scanf() no funciona como se esperaba
Un problema frecuente con *scanf() es cuando hay un espacio en blanco no leído ( ' '
, '\n'
, ...) en el flujo de entrada que el usuario no tuvo en cuenta.
La lectura de un número ( "%d"
et al.), o una cadena ( "%s"
), se detiene en cualquier espacio en blanco. Y aunque la mayoría de *scanf()
los especificadores de conversión omiten los espacios en blanco iniciales en la entrada, [
y c
no n
lo hacen. Por lo tanto, la nueva línea sigue siendo el primer carácter de entrada pendiente, lo que hace que ambos %c
no %[
coincidan.
Puede omitir la nueva línea en la entrada, leyéndola explícitamente, por ejemplo, mediante fgetc() o agregando un espacio en blanco a su cadena de formato *scanf() . (Un único espacio en blanco en la cadena de formato coincide con cualquier número de espacios en blanco en la entrada).
Leer y luego analizar
Simplemente desaconsejamos el uso de *scanf() excepto cuando usted realmente y positivamente sabe lo que está haciendo. Entonces, ¿qué utilizar como sustituto?
En lugar de leer y analizar la entrada de una sola vez, como intenta hacer *scanf() , separe los pasos.
Leer (parte de) una línea de entrada mediante fgets()
fgets() tiene un parámetro para limitar su entrada a esa cantidad de bytes como máximo, evitando el desbordamiento de su búfer. Si la línea de entrada cabe completamente en su búfer, el último carácter en su búfer será la nueva línea ( '\n'
). Si no todo encaja, estás viendo una línea parcialmente leída.
Analizar la línea en memoria
Especialmente útiles para el análisis en memoria son las familias de funciones strtol() y strtod() , que proporcionan una funcionalidad similar a los especificadores de conversión *scanf()d
, i
, u
, o
, x
, a
, e
, f
y g
.
Pero también le dicen exactamente dónde dejaron de analizar y tienen un manejo significativo de números demasiado grandes para el tipo de destino.
Más allá de eso, C ofrece una amplia gama de funciones de procesamiento de cadenas . Dado que tiene la entrada en la memoria y siempre sabe exactamente hasta qué punto la ha analizado, puede retroceder tantas veces como desee tratando de darle sentido a la entrada.
Y si todo lo demás falla, tiene toda la línea disponible para imprimir un mensaje de error útil para el usuario.
Limpiar
Asegúrese de cerrar explícitamente cualquier transmisión que haya abierto (con éxito). Esto vacía los buffers aún no escritos y evita fugas de recursos.
fclose(fp);