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

9 minutos de lectura

avatar de usuario
DevSolar

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

¿Ayudar?


Esta es una entrada de preguntas frecuentes.

StackOverflow tiene muchos 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 el cuadro completo.

Este es un intento de cubrir una serie de errores comunes de manera integral, por lo que esta familia específica de preguntas puede responderse simplemente marcándolas como duplicados de esta:

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

La respuesta está marcada como wiki de la comunidad. Siéntete libre de mejorar y (con cautela) ampliar.

  • Me parece un tutorial. Buena idea, pero no estoy en SO.

    – demasiado honesto para este sitio

    3 de febrero de 2016 a las 13:46

  • @Olaf: Nacido de mí dando básicamente las mismas respuestas, con solo ligeras variaciones, muchos veces a lo largo de los años porque realmente no había una buena respuesta de “cubrirlo todo” para usar para cerrar como duplicado. Este, y la reacción de chux, finalmente me hicieron escribir uno, al estilo de las entradas de c++-faq.

    – DevSolar

    3 de febrero de 2016 a las 13:52


  • Lo siento, pero aunque reconozco tu esfuerzo y entiendo completamente que estés cansado de recibir la misma mierda todos los días, el tema es demasiado amplio. ¿Dónde, por ejemplo, establece el umbral para usar un enfoque adecuado de lexxer/parser? O lectura de un solo carácter vs. strtoXXX, etc. Mientras escribe: cada pregunta tiene ligeras variaciones. Para preguntas duplicadas, ya existe un procedimiento establecido. La mayoría de las respuestas para tales son más compactas. Tenga en cuenta que uno de los principales problemas que tienen los que preguntan es la abstracción y la adaptación de conceptos generales a sus problemas. Eso no cambiará con un tutorial genérico (no FAQ) como este.

    – demasiado honesto para este sitio

    3 de febrero de 2016 a las 14:02


  • Si este es un intento de crear un “duplicado canónico” que podemos usar para cerrar las preguntas frecuentes de los novatos con respecto a scanf y los caracteres de nueva línea sobrantes en stdin, entonces lo apoyo totalmente.

    – Lundin

    3 de febrero de 2016 a las 14:13

  • Definitivamente diría que este tipo de “pregunta” es demasiado amplia aquí…

    – Marco Bonelli

    4 de febrero de 2016 a las 3:00

Introducción a la entrada de C para principiantes

  • Modo de texto frente a modo binario
  • Cheque fopen() por fracaso
  • Trampas
    • Verifique cualquier función que llame para el éxito
    • EOF, o “¿por qué la última línea se imprime dos veces?”
    • No utilice obtiene()alguna vez
    • No utilice fflush() sobre stdin o cualquier otra corriente abierta para la lectura, nunca
    • No utilice *escanear() para entradas potencialmente malformadas
    • Cuando *escanear() no funciona como se esperaba
  • Leer, entonces analizar gramaticalmente
    • Leer (parte de) una línea de entrada a través de fgets()
    • Analizar la línea en memoria
  • Limpiar

Modo de texto frente a modo binario

Un flujo de “modo binario” se lee exactamente como se ha escrito. Sin embargo, podría (o no) haber un número definido por la implementación de caracteres nulos (‘\0‘) añadido al final de la transmisión.

Una secuencia de “modo de texto” puede hacer una serie de transformaciones, que incluyen (pero no se limitan a):

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

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

Cheque fopen() por fracaso

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

Cuando fopen falla, por lo general establece el global error variable para indicar por qué Falló. (Técnicamente, esto 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.hpero en programas simples, por lo general, 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 le pasó fopen; si no lo hace, estará muy confundido cuando el problema sea que el nombre del archivo no es el que pensaba que era.

#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;
}

Trampas

Verifique cualquier función que llame para el éxito

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

Estos son errores que son fáciles cuando detecta la afección a tiempo, pero que si no lo hace, le causarán muchos problemas.

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

La función feof() devoluciones true si se ha alcanzado EOF. Un malentendido de lo que realmente significa “alcanzar” 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 vecesporque cuando se lee la última línea (hasta la nueva línea final, el último carácter en el flujo de entrada), EOF es no colocar.

EOF solo se establece cuando intenta leer pasado el ultimo personaje!

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

En su lugar, compruebe si fgets falló directamente:

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

No utilice obtiene()alguna vez

No hay forma de utilizar esta función de forma segura. Debido a esto, ha sido remoto del lenguaje con la llegada de C11.

No utilice fflush() sobre stdin o cualquier otra corriente abierta para la lectura, nunca

Mucha gente espera fflush(stdin) para descartar la entrada del usuario que aún no se ha leído. No hace eso. En ISO C simple, llamando fflush() en un flujo de entrada tiene 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, 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 *escanear() para entradas potencialmente malformadas

Muchos tutoriales te enseñan a usar *escanear() para leer cualquier tipo de entrada, porque es muy versátil.

Pero el propósito de *escanear() es realmente leer datos masivos que pueden ser algo confiado al estar en un formato predefinido. (Como ser escrito por otro programa.)

Incluso entonces *escanear() puede hacer tropezar al desatento:

  • 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, *escanear() inmediatamente deja de analizar, dejando los argumentos restantes sin inicializar.
  • te dirá cuantos asignaciones lo ha hecho con éxito, razón por la cual debe comprobar su código de retorno (ver arriba), pero no dónde exactamente dejó de analizar la entrada, lo que dificulta la recuperación elegante de errores.
  • Omite cualquier espacio en blanco inicial en la entrada, excepto cuando no lo hace ([, c, and n conversions). (See next paragraph.)
  • It has somewhat peculiar behaviour in some corner cases.

When *scanf() does not work as expected

A frequent problem with *scanf() is when there is an unread whitespace (' ', '\n', …) in the input stream that the user did not account for.

Reading a number ("%d" et al.), or a string ("%s"), stops at any whitespace. And while most *scanf() conversion specifiers skip leading whitespace in the input, [, c and n do not. So the newline is still the first pending input character, making either %c and %[ fail to match.

You can skip over the newline in the input, by explicitly reading it e.g. via fgetc(), or by adding a whitespace to your *scanf() format string. (A single whitespace in the format string matches any number of whitespace in the input.)

Read, then parse

We just adviced against using *scanf() except when you really, positively, know what you are doing. So, what to use as a replacement?

Instead of reading and parsing the input in one go, as *scanf() attempts to do, separate the steps.

Read (part of) a line of input via fgets()

fgets() has a parameter for limiting its input to at most that many bytes, avoiding overflow of your buffer. If the input line did fit into your buffer completely, the last character in your buffer will be the newline ('\n'). If it did not all fit, you are looking at a partially-read line.

Parse the line in-memory

Especially useful for in-memory parsing are the strtol() and strtod() function families, which provide similar functionality to the *scanf() conversion specifiers d, i, u, o, x, a, e, f, and g.

But they also tell you exactly where they stopped parsing, and have meaningful handling of numbers too large for the target type.

Beyond those, C offers a wide range of string processing functions. Since you have the input in memory, and always know exactly how far you have parsed it already, you can walk back as many times you like trying to make sense of the input.

And if all else fails, you have the whole line available to print a helpful error message for the user.

Clean Up

Make sure you explicitly close any stream you have (successfully) opened. This flushes any as-yet unwritten buffers, and avoids resource leaks.

fclose(fp);

  • A file open example in text mode rather than binary mode, for the purpose of a beginner’s guide, would be more applicable. fopen(argv[1]"rb"); –> fopen(argv[1], "r");

    – chux – Reincorporar a Monica

    10 mayo 2018 a las 12:35

  • Esta sería una buena respuesta, pero no mencionaste ninguno de los fgetc o fgetstrampas relacionadas, y su “CÓDIGO BUENO” se rompe de manera severa debido a eso.

    – autista

    17 de julio de 2018 a las 22:08

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad