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);