Entrada y Salida en C

Author
By Darío Rivera
Posted on 2022-09-06 in Lenguaje C

La entrada y salida en C es la base de casi todo programa en C, ya que permite a partir de datos de entradas que son procesados generar resultados o salidas esperadas por el usuario. De ninguna manera pienses que este es un tópico más, ya que entender cómo funcionan las entradas y salidas en C nos ahorrará cientos de horas y varios dolores de cabeza si no se tienen estos conceptos claros.

Flujos de Entrada y Sálida

Un programa necesita obtener los datos que necesita procesar de algún medio externo, por ejemplo, el teclado o un archivo en disco. Estos datos son utilizados para realizar cálculos sobre ellos (procesarlos) y obtener una salida o resultado y enviarla hacia un medio externo como el monitor u otro archivo en disco.

entrada y salida en c

En C, todos los datos se tratan como flujos o secuencias (stream) de bytes, y son tratados como si fueran archivos. Es por esto, que en algunas firmas de funciones verás la estructura FILE aún cuando pudieramos estar hablando de una entrada por teclado.

La comunicación entre el origen de cierta información y el destino se realiza mediante streams. Estos streams son respectivamente el flujo de entrada y el flujo de salida. Estas estructuras son intermediarios entre el programa y el origen y/o destino, lo que significa, que el programa escribirá y leerá información sin importarle desde dónde viene la información o hacia dónde va.

entrada y salida en c

Entrada y salida estándar

Hablamos de entrada y salida estándar ya que C por defecto abre un flujo de entrada y otro de salida de manera automática en cada programa. Estos flujos son stdin (standard input) y stdout (standard output). La entrada estándar está por defecto vinculada con el teclado y la sálida estándar con la pantalla.

Entrada con Formato

La función por excelencia para leer datos de la entrada estándar es scanf. Esta función lee bytes digitados por el teclado de acuerdo al formato especificado en sus argumentos. Por otro lado, esta función siempre recibirá direcciones de memoria en vez del valor asignado a una variable (ver punteros en C).

Las funciones de E/S que utiliza C proporcionan entrada y salida con memoria intermedia (buffer), formateada o no formateada. Esto es así, ya que la escritura en disco suele ser lenta comparada con el uso del buffer. Esta memoria intermedia se utiliza para guardar datos asociados a la entrada estándar como veremos a continuación.

Memoria intermedia o buffer

Cuando se pulsa la tecla ENTER a través del teclado también se introduce el caracter \n. Por otra parte, cuando se imprime esta caracter en la salida se obtiene CR+LF (ASCII 13 y ASCII 10), o bien \r\n. Este carácter puede traer consigo resultados indeseados si no tenemos en cuenta el buffer, veamos el siguiente ejemplo:

#include <stdio.h>

int main(void)
{
    float precio = 0;

    printf("Precio: ");
    scanf("%g", &precio);
    printf("Precio = %g\n", precio);
}

Al ejecutar este programa debemos ingresar el valor solicitado como %g, para este ejemplo, supongamos que digitamos 2319. Cuando digitamos el valor tendremos los siguiente en el buffer asociado a stdin.



Después de la lectura asociada a scanf solo quedará el carácter de nueva línea.



Esto es así ya que el carácter de nueva línea no es válido para la especificación de formato %g, así que en ese punto se rompe la lectura. Sin embargo, este carácter queda en el buffer. Esto puede causar problemas si después intentamos leer un dato string o un simple carácter. Vemos que pasa si queremos leer una letra después de esto.

#include <stdio.h>

int main(void)
{
    float precio = 0;
    char letter;

    printf("Precio: ");
    scanf("%g", &precio);

    printf("Digite una letra: ");
    scanf("%c", &letter);

    printf("Precio = %g\n", precio);
    printf("Letra = %c\n", letter);
}

La ejecución del programa sería similar a la siguiente:

user@server$ ./program
Precio: 2319
Digite una letra: Precio = 2319
Letra = 
user@server$

Solamente fue posible digitar el valor numérico ya que después de esto, la función scanf toma como entrada el valor que está en el buffer el cual es el carácter \n.

Limpieza de la memoria intermedia

Para solucionar el problema causado por el buffer respecto al carácter de nueva línea, podemos agregar en el formato de la función scanf un caractér adicional.

scanf("%*c%c", &letter);

El carácter asterisco (*) suprime la asignación del siguiente dato en la entrada. Esta solución no es del todo la mejor, aunque en primera instancia funciona. Supongamos ahora que digitamos el valor 1234jjjhf. Obtendríamos una salida como la siguiente:

user@server$ ./program
Precio: 1234jjjhf
Digite una letra: Precio = 1234
Letra = j
user@server$

Esto es así, ya que cuando digitamos el valor en la terminal tendríamos lo siguiente en el buffer.



Después de la lectura asociada a scanf quedará lo siguiente en el buffer.



Ya que el primer caracter del buffer es válido para la especificación de formato %c, este es el caracter obtenido por scanf. Dejando el buffer así despuṕes de las dos lecturas.



Debido a esto, la solución más eficaz para limpiar correctamente el buffer asociado a stdin es utilizar un bucle que lea carácter a carácter hasta que en contremos un salto de línea. Veamos como quedaría.

#include <stdio.h>

int main(void)
{
    float precio = 0;
    char letter;

    printf("Precio: ");
    scanf("%g", &precio);

    char next;
    do {
        scanf("%c", &next);
    } while (next != '\n');

    printf("Digite una letra: ");
    scanf("%c", &letter);

    printf("Precio = %g\n", precio);
    printf("Letra = %c\n", letter);
}

Dos consideraciones finales, para no crear una nueva variable y leerla mediante scanf, podemos utilizar la función getchar que obtiene un caractér de la entrada estándar y lo devuelve como resultado. Segundo, podemos simplificar el bucle a tan solo una sentencia, de modo que la limpieza del buffer quedaría resumida así:

while (getchar() != '\n');

Porsupuesto, nuestra solución tampoco es la final, lo ideal sería validar el primer dato de entrada y en caso de que sea incorrecto, volver a solicitarlo. Esto lo veremos en otro artículo.

Fin del archivo

Otro aspecto interesante acerca de los flujos de información es detectar cuándo hemos llegado al final del stream. Recordemos que los dispositivos de entrada y salida son tratados por C como archivos.

Por un lado, en la entrada estándar para ingresar el final del stream debemos teclear Ctrl + D en sistemas UNIX y Ctrl + Z en Windows. Mientras que C define la constante EOF para detercar el final del stream. Observemos el siguiente ejemplo:

#include <stdio.h>

void flushstdin(void);

int main(void)
{
    float precio = 0;
    char letter = 'A';
    int r = 0;

    printf("Precio: ");
    r = scanf("%g", &precio);

    (r == EOF)
        ? printf("Fin de la entrada de datos\n")
        : printf("Precio = %g\n", precio);

    flushstdin();

    printf("Digite una letra: ");
    scanf("%c", &letter);
    printf("Letra = %c\n", letter);
}

void flushstdin(void)
{
    if (!feof(stdin))     // detecta si se llegó al fin del stream
        while (getchar() != '\n');
}

El camino "feliz" de este programa se obtiene al digital los valores esperados.

Precio: 5256
Precio = 5256
Digite una letra: V
Letra = V

La función scanf puede retornar EOF en caso de que se detecte el final del stream. Veamos ahora que pasa si digitamos Ctrl + D.

user@server$ ./program
Precio: 5256
Precio: Fin de la entrada de datos
Digite una letra: Letra = A
user@server$

Nótese que se ha impreso "Fin de la entrada de datos". Después de que se cierra el flujo de entrada, el comportamiento del programa es inesperado, ninguna de las llamadas a scanf pediran datos ya que el flujo está cerrado.

Nótese también que se ha provisto de la función feof para verificar que no se ha llegado al fin del stream antes de intentar limpiar la memoria intermedia. Si no hicieramos esto, entraríamos en un bucle en la terminal ya que el flujo se cerro y estamos esperando un carácter \n.

Error en el archivo

Otra de las cosas que puede suceder al trabajar con streams es un error de lectura/escritura del dispositivo.

Nótese que las funciones scanf y getchar retornan el valor EOF cuando se detecta el final del stream o cuando hay un error.

Para detectar un error sobre la última operación realizada en un stream, puede utilizarse la función ferror. Observa como en el siguiente ejemplo se maneja el error en la escritura de un archivo.

#include <stdio.h>

int main ()
{
  FILE * pFile;
  pFile=fopen("myfile.txt","r");

  if (pFile==NULL) perror ("Error opening file");
  else {
    fputc ('x',pFile);
    if (ferror (pFile))
      printf ("Error Writing to myfile.txt\n");
    fclose (pFile);
  }
  return 0;
}

Para manejar el error sobre la entrada o salida estándar se se debe pasar stdin o stdout como parámetro de la función:

ferror(stdin);      // error sobre la entrada estándar
ferror(stdout);     // error sobre la salida estádar

Acerca de Darío Rivera

Author

Ingeniero de desarrollo en PlacetoPay , Medellín. Darío ha trabajado por más de 6 años en lenguajes de programación web especialmente en PHP. Creador del microframework DronePHP basado en Zend y Laravel.

Sólo aquellos que han alcanzado el éxito saben que siempre estuvo a un paso del momento en que pensaron renunciar.