TEMA 6 : Entrada y salida (E/S) de datos 

6.0 Introducción

        El  objetivo de  este tema  es hacer  un estudio completo en
todo  lo referente  a la  entrada y,  salida (E/S)  en C, estudiando
también los dos sistemas de ficheros que existen en este lenguaje.

6.1 Entrada y salida (E/S)

        Las operaciones de entrada  y salida (abreviadamente E/S) no
forman parte del lenguaje C propiamente dicho, sino que están en una
biblioteca  o   librería:  <stdio.h>.  Todo   programa  que  utilice
funciones de entrada y salida estándar deberá contener la línea:

#include <stdio.h>

6.1.1 E/S estándar

        Por defecto, la  entrada estándar es el teclado  y la salida
estándar es la pantalla o monitor. Hay dos formas básicas de cambiar
la entrada y la salida estándar:

  1. Con los símbolos de redirección (<, >, <<, >>) o de tubería (|)
del  sistema operativo  al ejecutar  el programa  desde la  línea de
órdenes.

  2. Con determinadas funciones y  variables que se encuentran en la
librería <stdio.h> en el código fuente del programa.


6.2 Flujos y ficheros

        Hay dos  conceptos muy importantes en  C relacionados con la
E/S:  flujos  (streams,  en  inglés)  y  ficheros.  Los  flujos  son
sucesiones  de  caracteres  a  través  de  los cuales realizamos las
operaciones  de  E/S.  Para  el  programador  todos  los  flujos son
iguales. Para el C (en general para el sistema operativo) un fichero
es un concepto lógico que puede  ser aplicado a cualquier cosa desde
ficheros  de discos  a terminales.  A cada  fichero se  le asigna un
flujo  al  realizar  la  operación  de  apertura  sobre  él. Para el
programador un fichero  es un dispositivo externo capaz  de una E/S.
Todos los  ficheros no son  iguales pero todos  los flujos sí.  Esto
supone una  gran simplificación para  el usuario, ya  que sólo tiene
que pensar en términos de flujo  y no de dispositivos concretos. Por
ejemplo, si el usuario hace:  printf ("mensaje"); sabe que ®mensaje¯
se escribe  en el flujo estándar  de salida, ya sea  la pantalla, un
fichero de disco, una cinta, ...


6.3 Tipos de flujos: flujos de texto y flujos binarios

        Cuando  hemos dicho  que todos  los flujos  son iguales,  es
cierto que lo son en su  utilización por parte del programador, pero
en realidad, podemos distinguir dos tipos:

  - Flujos  de texto:  son una  sucesión de  caracteres originado en
    líneas que  finalizan con un  carácter de nueva-línea.  En estos
    flujos  puede no  haber una  relación  de  uno a  uno entre  los
    caracteres  que  son  escritos  (leídos)  y  los del dispositivo
    externo, por ejemplo, una  nueva-línea puede transformarse en un
    par de caracteres (un retorno de carro y un carácter de salto de
    línea).

  - Flujos   binarios:  son   flujos   de   bytes  que   tienen  una
    correspondencia uno  a uno con  los que están  almacenados en el
    dispositivo externo. Esto es, no se presentan desplazamientos de
    caracteres. Además  el número de  bytes escritos (leídos)  es el
    mismo que los almacenados en el dispositivo externo.

        Esta diferencia de flujos es importante tenerla en cuenta al
leer ficheros de discos. Supongamos  que tenemos un fichero de disco
con  7 caracteres  donde el  cuarto carácter  es el  carácter fin de
fichero (en  sistema operativo DOS  es el carácter  con código ASCII
26). Si  abrimos el fichero en  modo texto, sólo podemos  leer los 3
primeros  caracteres, sin  embargo, si  lo abrimos  en modo binario,
leeremos los 7 caracteres ya que  el carácter con código ASCII 26 es
un carácter como cualquier otro.


6.4 Programas C con flujos

        Al principio de la ejecución de  un programa C se abren tres
flujos de tipo texto predefinidos:

  stdin : dispositivo de entrada estándar
  stdout: dispositivo de salida estándar
  stderr: dispositivo de salidad de error estándar

        Al finalizar el programa, bien  volviendo de la función main
al  sistema operativo  o bien  por una  llamada a  exit(), todos los
ficheros se  cierran automáticamente. No se  cerrarán si el programa
termina a través de una llamada a abort() o abortando el programa.

        Estos   tres  ficheros   no  pueden   abrirse  ni   cerrarse
explicítamente.


6.5 Resumen de lo anterior

        Como todo  lo que acabamos  de decir puede  resultar un poco
confuso a  las personas que  tienen poca experiencia  en C, vamos  a
hacer un pequeño resumen en términos generales:

1. En C, cualquier  cosa externa de la que podemos  leer o en la que
   podemos escribir datos es un fichero.

2. El programador escribe (lee) datos  en estos ficheros a través de
   los flujos de cada fichero.  De esta forma el programador escribe
   (lee) los datos de la misma  forma en todos los tipos de ficheros
   independientemente del tipo de fichero que sea.

3. Aunque conceptualmente todos los  flujos son iguales, en realidad
   hay dos tipos: flujos de texto y flujos binarios.

4. Hay   tres   flujos   de   texto   predefinidos   que   se  abren
   automáticamente  al  principio  del  programa:  stdin,  stdout  y
   stderr. Estos tres flujos se cierran automáticamente al final del
   programa.

6.6 Pasos para operar con un fichero

        Los  pasos  a  realizar  para  realizar  operaciones  con un
fichero son los siguientes:

 1) Crear un nombre interno de fichero. Esto se hace en C declarando
un puntero de  fichero (o puntero a fichero).  Un puntero de fichero
es una  variable puntero que  apunta a una  estructura llamada FILE.
Esta estructura está definida en  el fichero stdio.h y contiene toda
la  información necesaria  para poder  trabajar con  un fichero.  El
contenido de esta estructura es  dependiente de la implementación de
C y del sistema, y no es interesante saberlo para el programador.

        Ejemplo:

        FILE *pf; /* pf es un puntero de fichero */

 2)  Abrir el  fichero. Esto  se hace  con la  función fopen()  cuyo
prototipo se encuentra en el fichero stdio.h y es:

        FILE *fopen (char *nombre_fichero, char *modo);

        Si el  fichero con nombre  nombre_fichero no se  puede abrir
devuelve NULL.

        El parámetro nombre_fichero puede  contener la ruta completa
de fichero  pero teniendo en cuenta  que la barra invertida  (\) hay
que repetirla en una cadena de caracteres.

        Los valores válidos para el parámeto modo son:
 
   +-------+-------------------------------------------------+
   | Modo  | Interpretación                                  |
   +-------+-------------------------------------------------+
   | "r"   | Abrir un fichero texto para lectura             |
   | "w"   | Crear un fichero texto para escritura           |
   | "a"   | Añadir a un fichero texto                       |
   | "rb"  | Abrir un fichero binario para lectura           |
   | "wb"  | Crear un fichero binario para escritura         |
   | "ab"  | Añadir a un fichero binario                     |
   | "r+"  | Abrir un fichero texto para lectura/escritura   |
   | "w+"  | Crear un fichero texto para lectura/escritura   |
   | "a+"  | Abrir un fichero texto para lectura/escritura   |
   | "rb+" | Abrir un fichero binario para lectura/escritura |
   | "wb+" | Crear un fichero binario para lectura/escritura |
   | "ab+" | Abrir un fichero binario para lectura/escritura |
   +-------+-------------------------------------------------+

        Si se utiliza fopen() para  abrir un fichero para escritura,
entonces cualquier fichero que exista con ese nombre es borrado y se
comienza  con un  fichero nuevo.  Si no  existe un  fichero con  ese
nombre, entonces se crea uno. Si lo que se quiere es añadir al final
del fichero, se debe utilizar el  modo "a". Si no existe el fichero,
devuelve  error.  Abrir  un  fichero  para  operaciones  de  lectura
necesita  que  el  fichero  exista.  Si  no  existe  devuelve error.
Finalmente,   si   se   abre   un   fichero   para   operaciones  de
lectura/escritura, no se  borra en caso de existir.  Sin embargo, si
no existe se crea.

        Ejemplo:

        FILE *pf;
        pf = fopen ("c:\\autoexec.bat", "r");
        if (pf == NULL) /* siempre se debe hacer esta comprobación*/
        {
            puts ("No se puede abrir fichero.");
            exit (1);
        }

 3) Realizar las operaciones deseadas con el fichero como pueden ser
la escritura en él y la  lectura de él. Las funciones que disponemos
para hacer esto las veremos un poco más adelante.

 4) Cerrar el fichero. Aunque  el C cierra automáticamente todos los
ficheros  abiertos  al  terminar  el  programa,  es  muy aconsejable
cerrarlos explícitamente. Esto se hace  con la función fclose() cuyo
prototipo es:

        int fclose (FILE *pf);

        La función fclose() cierra el  fichero asociado con el flujo
pf y vuelca su buffer.

        Si fclose() se ejecuta correctamente devuelve el valor 0. La
comproba ción del  valor devuelto no se hace  muchas veces porque no
suele fallar.

        Ejemplo:

        FILE *pf;
        if ((pf = fopen ("prueba", "rb")) == NULL)
        {
            puts ("Error al intentar abrir el fichero.");
            exit (1);
        }
        /* ... */
        if (fclose (pf) != 0)
        {
            puts ("Error al intentar cerrar el fichero.");
            exit (1);
        }

Resumen de los 4 pasos para la manipulación de un fichero:
----------------------------------------------------------

1) Declarar un puntero de fichero.
  FILE *pf;

2) Abrirlo el fichero.
  if ((pf = fopen ("nombre_fichero", "modo_apertura")) == NULL)
    error ();
  else
    /* ... */

3) Realizar las operaciones deseadas con el fichero.
  /* En las siguientes ventanas veremos las
     funciones que tenemos para ello. */

4) Cerrar el fichero.
  if (fclose (pf) != 0)
    error ();
  else
    /* ... */

6.7 Sistema de ficheros tipo UNIX

        Debido a  que el lenguaje C  se desarrolló inicialmente bajo
el sistema operativo UNIX, se creó un segundo sistema de ficheros. A
este sistema  se le llama  E/S sin buffer,  E/S de bajo  nivel o E/S
tipo UNIX. Hoy  en día, este sistema de  ficheros está totalmente en
desuso  y se  considera obsoleto,  además el  nuevo estándar ANSI ha
decidido no estandarizar el sistema de E/S sin buffer tipo UNIX. Por
todo  lo dicho  no se  puede recomendar  este sistema  a los  nuevos
programadores C. Sin embargo, todavía  existen programas que lo usan
y es  soportado por  la mayoría  de los  compiladores de  C. Así que
incluimos una breve explicación al respecto.

6.7.1 Descriptores de ficheros

        A diferencia del sistema de E/S de alto nivel, el sistema de
bajo nivel no  utiliza punteros a ficheros de  tipo FILE; el sistema
de bajo  nivel utiliza descriptores de  fichero de tipo int.  A cada
fichero se le asocia un número (su descriptor de fichero).

        Hay tres descriptores de fichero predefinidos:

   0   entrada estándar
   1   salida estándar
   2   salida de errores estándar

        Los cuatro pasos para la manipulación de ficheros con E/S de
alto nivel que vimos antes se transforman en la E/S de bajo nivel en
los siguientes pasos:

1) Declarar un descriptor de fichero para el fichero a abrir.
  int fd;

2) Abrir el fichero.
  if ((fd = open (nombre_fichero, modo_apertura)) = -1)
    {
      printf ("Error al intentar abrir el fichero.\n");
      exit (1);
    }

3)  Manipular el  fichero con  las funciones  que tenemos disponible
para ello.

4) Cerrar el fichero.
  close (fd);