¿Cómo leer / analizar la entrada en C? Las preguntas frecuentes

Tengo problemas con mi progtwig C cuando trato de leer / analizar la entrada.

¿Ayuda?


Esta es una entrada de preguntas frecuentes.

StackOverflow tiene muchas preguntas relacionadas con la lectura de entrada en C, con respuestas generalmente enfocadas en el problema específico de ese usuario en particular sin realmente pintar la imagen completa.

Este es un bash de cubrir una serie de errores comunes de forma exhaustiva, por lo que esta familia específica de preguntas se puede responder simplemente marcándolas como duplicados de esta:

  • ¿Por qué la última línea imprime dos veces?
  • ¿Por qué mi scanf("%d", ...) / scanf("%c", ...) falla?
  • ¿Por qué gets() bloquea?

La respuesta está marcada como wiki de la comunidad. Siéntete libre de mejorar y (cautelosamente) extender.

La cartilla de entrada del principiante C

  • Modo de texto vs. modo binario
  • Compruebe fopen () para el fracaso
  • Trampas
    • Verifique cualquier función que llame para tener éxito
    • EOF, o “¿por qué la última línea imprime dos veces?”
    • No use gets () , nunca
    • No use fflush () en stdin o cualquier otra secuencia abierta para leer, nunca
    • No use * scanf () para una entrada potencialmente mal formada
    • Cuando * scanf () no funciona como se esperaba
  • Leer, luego analizar
    • Lea (parte de) una línea de entrada a través de fgets ()
    • Analizar la línea en la memoria
  • Limpiar

Modo de texto vs. modo binario

Se lee una secuencia de “modo binario” exactamente como se ha escrito. Sin embargo, puede haber (o no) una cantidad definida por la implementación de caracteres nulos (‘ \0 ‘) añadidos al final de la secuencia.

Una secuencia de “modo de texto” puede realizar una serie de transformaciones, incluidas (entre otras):

  • eliminación de espacios inmediatamente antes de un final de línea;
  • cambio de líneas nuevas ( '\n' ) a otra cosa en la salida (por ejemplo, "\r\n" en Windows) y vuelta a '\n' en la entrada;
  • agregar, alterar o eliminar caracteres que no isprint(c) caracteres ( isprint(c) es verdadero), tabs horizontales o líneas nuevas.

Debería ser obvio que el texto y el modo binario no se mezclan. Abre archivos de texto en modo texto y archivos binarios en modo binario.

Compruebe fopen () para el fracaso

El bash de abrir un archivo puede fallar por varias razones: la falta de permisos o el archivo no encontrado son los más comunes. En este caso, fopen () devolverá un puntero NULL . Siempre verifique si fopen devolvió un puntero NULL , antes de intentar leer o escribir en el archivo.

Cuando fopen falla, generalmente establece la variable global errno 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 progtwigs 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 de archivo que pasó a fopen ; si no haces eso, estarás muy confundido cuando el problema sea que el nombre del archivo no es lo que pensabas que era.

 #include  #include  #include  int main(int argc, char **argv) { if (argc < 2) { fprintf(stderr, "usage: %s file\n", argv[0]); return 1; } FILE *fp = fopen(argv[1], "rb"); 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; } 

Trampas

Verifique cualquier función que llame para tener éxito

Esto debería ser obvio. Pero verifique la documentación de cualquier función a la que llame para conocer su valor de retorno y el manejo de errores, y verifique esas condiciones.

Estos son errores que son fáciles cuando captas la condición temprano, pero provocan muchos arañazos en la cabeza si no lo haces.

EOF, o "¿por qué la última línea imprime dos veces?"

La función feof () devuelve true si se ha alcanzado EOF. Un malentendido de lo que significa "llegar a" EOF significa que muchos principiantes escriben 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 última línea final, el último carácter en la secuencia de entrada), no se establece EOF.

¡EOF solo se configura cuando intentas leer más allá del último personaje!

Entonces, el código de arriba se repite una vez más, fgets () no puede leer otra línea, establece EOF y deja intacto el contenido del buffer , que luego se vuelve a imprimir.

En cambio, verifica si los fgets fallaban directamente:

 // GOOD CODE while (fgets(buffer, BUFFER_SIZE, fp)) { printf("%s", buffer); } 

No use gets () , nunca

No hay forma de usar esta función de forma segura. Debido a esto, se ha eliminado del lenguaje con la llegada de C11.

No use fflush () en stdin o cualquier otra secuencia abierta para leer, nunca

Muchas personas esperan que fflush(stdin) descarte las entradas del usuario que aún no se han leído. No hace eso. En ISO claro C, llamar a fflush () en un flujo de entrada tiene un comportamiento indefinido . Tiene un comportamiento bien definido en POSIX y en MSVC, pero ninguno de ellos hace que descarte la entrada del usuario que aún no se ha leído.

Por lo general, se lee la forma correcta de borrar la entrada pendiente y se descartan los caracteres hasta e incluyendo una nueva línea, pero no más allá:

 int c; do c = getchar(); while (c != EOF && c != '\n'); 

No use * scanf () para una entrada potencialmente mal formada

Muchos tutoriales le enseñan a utilizar * scanf () para leer cualquier tipo de entrada, porque es muy versátil.

Pero el objective de * scanf () es realmente leer datos masivos de los que se puede confiar en cierto modo en un formato predefinido. (Como ser escrito por otro progtwig)

Incluso entonces * scanf () puede desconectar al no observador:

  • Usar una cadena de formato que de alguna manera puede ser influenciada por el usuario es un agujero de seguridad enorme.
  • Si la entrada no coincide con el formato esperado, * scanf () detiene inmediatamente el análisis, dejando los argumentos restantes sin inicializar.
  • Le indicará cuántas asignaciones ha realizado con éxito, por lo que debe verificar su código de retorno (consulte más arriba), pero no exactamente dónde dejó de analizar la entrada, lo que dificulta la recuperación elegante de errores.
  • Se salta cualquier espacio en blanco inicial en la entrada, excepto cuando no lo hace (conversiones [ , c n ). (Ver el siguiente párrafo)
  • Tiene un comportamiento algo peculiar en algunos casos de esquina.

Cuando * scanf () no funciona como se esperaba

Un problema frecuente con * scanf () es cuando hay un espacio en blanco no leído ( ' ' , '\n' , ...) en la secuencia de entrada que el usuario no tuvo en cuenta.

Leer un número ( "%d" et al.), O una cadena ( "%s" ), se detiene en cualquier espacio en blanco. Y aunque la mayoría de los especificadores de conversión *scanf() omiten los espacios en blanco iniciales en la entrada, [ , c y n no. Por lo tanto, la nueva línea sigue siendo el primer carácter de entrada pendiente, lo que hace que ni %c ni %[ no coincidan.

Puede omitir la nueva línea en la entrada, leyéndola explícitamente, por ejemplo, vía fgetc () , o agregando un espacio en blanco a su cadena de formato * scanf () . (Un solo espacio en blanco en la cadena de formato coincide con cualquier cantidad de espacios en blanco en la entrada).

Leer, luego analizar

Acabamos de asesorarnos sobre el uso de * scanf (), excepto cuando de verdad, de manera positiva, sabes lo que estás haciendo. Entonces, ¿qué usar como reemplazo?

En lugar de leer y analizar la entrada de una vez, como * scanf () intenta hacer, separe los pasos.

Lea (parte de) una línea de entrada a través de fgets ()

fgets () tiene un parámetro para limitar su entrada a la mayoría de los bytes, evitando el desbordamiento de su búfer. Si la línea de entrada encajó completamente en su memoria intermedia, el último carácter en su memoria intermedia será la línea nueva ( '\n' ). Si no encajaba todo, estás viendo una línea parcialmente leída.

Analizar la línea en la 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 d , i , u , o , x , a , e , g * scanf () .

Pero también te dicen exactamente dónde dejaron de analizar, y tienen un manejo significativo de los 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 . Como tiene la entrada en la memoria y siempre sabe exactamente hasta dónde la ha analizado, puede retroceder tantas veces como desee para 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 flujo que haya abierto (con éxito). Esto vacía cualquier búfer aún no escrito, y evita las pérdidas de recursos.

 fclose(fp);