Subsecciones

16. Entrada y salida (E/S) stdio.h

En este capítulo se verán varias formas de entrada y salida (E/S). Se han mencionado brevemente algunas formas y ahora se revisarán con un poco más de detalle.

Los programas que hagan uso de las funciones de la biblioteca de E/S deben incluir la cabecera, esto es:

#include <stdio.h>

16.1 Reportando errores

En muchas ocasiones es útil reportar los errores en un programa de C. La función de la biblioteca estándar perror es la indicada para hacerlo. Es usada conjuntamente con la variable errno y frecuentemente cuando se encuentra un error se desea terminar el programa antes. Además se revisa la función exit() y errno, que en un sentido estricto no son parte de la biblioteca stdio.h, para ver como trabajan con perror.

16.1.1 perror()

El prototipo de la funcion perror es:

void perror(const char *s);

La función perror() produce un mensaje que va a la salida estándar de errores, describiendo el último error encontrado durante una llamada al sistema o a ciertas funciones de biblioteca. La cadena de caracteres s que se pasa como argumento, se muestra primero, luego un signo de dos puntos y un espacio en blanco; por último, el mensaje y un salto de línea. Para ser de más utilidad, la cadena de caracteres pasada como argumento debería incluir el nombre de la función que incurrió en el error. El código del error se toma de la variable externa errno, que toma un valor cuando ocurre un error, pero no es puesta a cero en una llamada no errónea.

16.1.2 errno

A la variable especial del sistema errno algunas llamadas al sistema (y algunas funciones de biblioteca) le dan un valor entero, para indicar que ha habido un error. Esta variable esta definida en la cabecera #include <errno.h> y para ser usada dentro de un programa debe ser declarada de la siguiente forma:

extern int errno;

El valor sólo es significativo cuando la llamada devolvió un error (usualmente -1), algunas veces una función también puede devolver -1 como valor válido, por lo que se debe poner errno a cero antes de la llamada, para poder detectar posibles errores.

16.1.3 exit

La función exit tiene el siguiente prototipo de acuerdo a la cabecera #include <stdlib.h>:

void exit(int status);

La función produce la terminacón normal del programa y la devolución de status al proceso padre o al sistema operativo. El valor de status es usado para indicar como ha terminado el programa:

o
sale con un valor EXIT_SUCCESS en una terminación
o
sale con un valor EXIT_FAILURE en una terminación

Por lo tanto cuando se encuentre un error se llamará a la función como exit(EXIT_FAILURE) para terminar un programa con errores.

16.2 Flujos

Los flujos son una forma flexible y eficiente para leer y escribir datos.

Existe una estructura interna de C, FILE, la cual representa a todas los flujos y esta definida en stdio.h. Por lo tanto simplemente se necesita referirse a la estructura para realizar entrada y salida de datos.

Para usar los flujos entonces se debe declarar una variable o apuntador de este tipo en el programa. No se requiere conocer más detalles acerca de la definición. Se debe abrir un flujo antes de realizar cualquier E/S, después se puede accesar y entonces se cierra.

El flujo de E/S usa un BUFFER, es decir, un pedazo fijo de área temporal de la memoria (el buffer) es leído o escrito a un archivo. Lo siguiente se muestra en la figura 17.1. Observar que el apuntador del archivo actualmente apunta a éste buffer.

Figura 16.1: Modelo de entrada salida usando un buffer.
\includegraphics[width=6in, clip]{figuras/streams.eps}

Esto conduce a un uso eficiente de E/S pero se debe tener cuidado: los datos escritos a un buffer no aparecen en un archivo (o dispositivo) hasta que el buffer es escrito (con \n se puede hacer). Cualquier salida anormal del código puede causar problemas.

16.2.1 Flujos predefinidos

En UNIX se tienen predefinidos 3 flujos (en stdio.h): stdin, stdout y stderr.

Todas ellas usan texto como método de E/S.

Los flujos stdin y stdout pueden ser usadas con archivos, programas, dispositivos de E/S como el teclado, la consola, etc. El flujo stderr siempre va a la consola o la pantalla.

La consola es el dispositivo predefinido para stdout y stderr. El teclado lo es para stdin.

Los flujos predefinidos son automáticamente abiertas.

16.2.1.1 Redireccionamiento

Lo siguiente no es parte de C, pero depende del sistema operativo. Se hace redireccionamiento desde la línea de comandos con:

> que redirecciona stdout a un archivo.

Por lo tanto, si se tiene un programa llamado salida, que usualmente muestra en pantalla algo, entonces:

salida > archivo_sal

mandará la salida al archivo archivo_sal

< redirecciona stdin desde un archivo a un programa.

Por lo tanto, si se espera entrada desde el teclado para un programa llamado entrada, se puede leer en forma similar la entrada desde un archivo.

entrada < archivo_ent

Con | entubamiento o pipe se coloca stdout de un programa en stdin de otro,

prog1 | prog2

Por ejemplo, mandar la salida (usualmente a consola) de un programa directamente a la impresora

out | lpr

16.3 E/S Basica

Hay un par de funciones que dan las facilidades básicas de E/S. Quizás las más comunes son: getchar() y putchar(). Están definidas y son usadas como sigue:

main() {
	int ch;

	ch = getchar();
	(void) putchar((char) ch); }

Otras funciones relacionadas son:

int getc(FILE *flujo);
int putc(char ch, FILE *flujo);

16.4 E/S formateada

Se han visto ya algunos ejemplos de como C usa la E/S formateada. En esta sección se revisarán con más detalle.

16.4.1 printf

El prototipo de la función esta definido como:

int printf( const char *formato, lista arg ...);

que muestra en stdout la lista de argumentos de acuerdo al formato especificado. La función devuelve el número de caracteres impresos.

La cadena de formateo tiene dos tipos de objetos:


Tabla 16.1: Caracteres de format printf/scanf

Especificador (%) Tipo Resultado
c char un sólo caracter
i,d int número base diez
o int número base ocho
x,X int número base dieciseis
    notación minús/mayús
u int entero sin signo
s char * impresión de cadena
    terminada por nulo
f double/float formato -m.ddd ...
e,E " Notación Científica
    -1.23e002
g,G " e ó f la que sea
    más compacta
% - caracter %


Entre el % y el caracter de formato se puede poner:

Por lo tanto:

printf("%-2.3f\n",17.23478);

la salida en pantalla será:

17.235

y

printf("VAT=17.5%%\n");

genera la siguiente salida:

VAT=17.5%

16.4.2 scanf

La función esta definida como sigue:

int scanf( const char *formato, lista arg ...);

Lee de la entrada estándar (stdin) y coloca la entrada en la dirección de las variables indicadas en lista args. Regresa el número de caracteres leídos.

La cadena de control de formateo es similar a la de printf.

Importante: se requiere la dirección de la variable o un apuntador con scanf.

Por ejemplo:

scanf("%d", &i);

Para el caso de un arreglo o cadena sólo se requiere el nombre del mismo para poder usar scanf ya que corresponde al inicio de la dirección de la cadena.

char cadena[80];
scanf("%s",cadena);

16.5 Archivos

Los archivos son la forma más común de los flujos.

Lo primero que se debe hacer es abrir el archivo. La función fopen() hace lo siguiente:

FILE *fopen(const char *nomb, const char *modo);

fopen regresa un apuntador a un FILE. En la cadena nomb se pone el nombre y la trayectoria del archivo que se desea accesar. La cadena modo controla el tipo de acceso. Si un archivo no puede ser accesado por alguna razón un apuntador NULL es devuelto.

Los modos son:

Para abrir un archivo se debe tener un flujo (apuntador tipo archivo) que apunte a la estructura FILE.

Por lo tanto, para abrir un archivo denominado miarch.dat para lectura haremos algo como lo siguiente;

FILE *flujo; /* Se declara un flujo */
flujo = fopen("miarch.dat","r");

es una buena práctica revisar si un archivo se pudo abrir correctamente

if ( (flujo = fopen("miarch.dat","r")) == NULL )
{
	printf("No se pudo abrir %s\n","miarch.dat");
	exit(1);
}
.....

16.5.1 Lectura y escritura de archivos

Las funciones fprintf y fscanf son comúnmente empleadas para accesar archivos.

int fprintf(FILE *flujo, const char *formato, args ... );
int fscanf(FILE *flujo, const char *formato, args ... );

Las funciones son similares a printf y scanf excepto que los datos son leídos desde el flujo, el cual deberá ser abierto con fopen().

El apuntador al flujo es automáticamente incrementado con todas las funciones de lectura y escritura. Por lo tanto, no se debe preocupar en hacer lo anterior.

char *cadena[80];
FILE *flujo;

if ( (flujo = fopen( ... )) != NULL)
	fscanf(flujo,"%s",cadena);

Otras funciones para archivos son:

int getc(FILE *flujo)			int fgetc(FILE *flujo)
int putc(char ch, FILE *s)			int fputc(char ch, FILE *s)

Estas son parecidas a getchar y putchar.

getc esta definida como una macro del preprocesador en stdio.h. fgetc es una función de la biblioteca de C. Con ambas se consigue el mismo resultado.

Para el volcado de los datos de los flujos a disco, o bien, para disasociar un flujo a un archivo, haciendo previamente un volcado, usar:

int fflush(FILE *flujo);
int fclose(FILE *flujo);

También se puede tener acceso a los flujos predeterminados con fprintf, etc. Por ejemplo:

fprintf(stderr,"¡¡No se puede calcular!!\n");
fscanf(stdin,"%s",string);

16.6 sprintf y sscanf

Son parecidas a fprintf y fscanf excepto que escriben/leen una cadena.

int sprintf(char *cadena, char *formato, args ... )
int sscanf(char *cadena, cahr *formato, args ... )

Por ejemplo:

float tanque_lleno = 47.0;		/* litros */
float kilometros = 400;
char km_por_litro[80];

sprintf( km_por_litro, "Kilometros por litro = %2.3f", kilometros/tanque_lleno);

16.6.1 Petición del estado del flujo

Existen unas cuantas funciones útiles para conocer el estado de algún flujo y que tienen los prototipos siguientes:

int feof(FILE *flujo);
int ferror(FILE *flujo);
void clearerr(FILE *flujo);
int fileno(FILE *flujo);

16.7 E/S de bajo nivel o sin almacenamiento intermedio

Esta forma de E/S es sin buffer -cada requerimiento de lectura/escritura genera un acceso al disco (o dispositivo) directamente para traer/poner un determinado número de bytes.

No hay facilidades de formateo -ya que se están manipulando bytes de información.

Lo anterior significa que se estan usando archivos binarios (y no de texto).

En vez de manejar apuntadores de archivos, se emplea un manejador de archivo de bajo nivel o descriptor de archivo, el cual da un entero único para identificar cada archivo.

Para abrir un archivo usar:

int open(char* nomb, int flag);

que regresa un descriptor de archivo ó -1 si falla.

flag controla el acceso al archivo y tiene los siguientes macros definidas en fcntl.h:

para ver otras opciones usar man.

La función:

int creat(char* nomb, int perms);

puede también ser usada para crear un archivo.

Otras funciones son:

int close(int fd);
int read(int fd, char *buffer, unsigned longitud);
int write(int fd, char *buffer, unsigned longitud);

que pueden ser usadas para cerrar un archivo y leer/escribir un determinado número de bytes de la memoria/hacia un archivo en la localidad de memoria indicada por buffer.

La función sizeof() es comúnmente usada para indicar la longitud.

Las funciones read y write regresan el número de bytes leídos/escritos o -1 si fallan.

Se tiene a continuación dos aplicaciones que usan algunas de las funciones indicadas:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

main(int argc, char **argv)
{
	float buffer[]={23.34,2.34,1112.33};
	int df;
	int bytes_esc;
	int num_flot;

	/* Primeramente se crea el archivo */
	if ( (df = open(argv[1], O_CREAT, S_IRUSR | S_IWUSR) ) == -1)
	{ /* Error, archivo no abierto */
		perror("Archivo datos, apertura");
		exit(1);
	}
	else
		printf("Descriptor de archivo %d\n",df);

	/* Despues se abre para solamente escribir */
	if ( (df = open(argv[1], O_WRONLY) ) == -1)
	{ /* Error, archivo no abierto */
		perror("Archivo datos, apertura");
		exit(1);
	}
	else
		printf("Descriptor de archivo %d\n",df);


	/* En primer lugar se escribe el n´umero de flotantes que seran escritos */
	num_flot = 3;
	if ( (bytes_esc = write(df, &num_flot, sizeof(int)) ) == -1)
	{ /* Error en la escritura */
		perror("Archivo datos, escritura");
		exit(1);
	}
	else
		printf("Escritos %d bytes\n",bytes_esc);
	
	/* Se escribe el arreglo de flotantes */
	if ( (bytes_esc = write(df, buffer, num_flot*sizeof(float)) ) == -1)
	{ /* Error en la escritura */
		perror("Archivo datos, escritura");
		exit(1);
	}
	else
		printf("Escritos %d bytes\n",bytes_esc);

	close(df);

}

Ejemplo de lectura del ejemplo anterior:

/* Este programa lee una lista de flotantes de un archivo binario. */
/* El primer byte del archivo es un entero indicando cuantos       */
/* flotantes hay en el archivo. Los flotantes estan despues del    */
/* entero, el nombre del archivo se da en la linea de comandos.    */

#include <stdio.h>
#include <fcntl.h>

main(int argc, char **argv)
{
	float buffer[1000];
	int fd;
	int bytes_leidos;
	int num_flot;

	if ( (fd = open(argv[1], O_RDONLY)) == -1)
	{ /* Error, archivo no abierto */
		perror("Archivo datos");
		exit(1);
	}

	if ( (bytes_leidos = read(fd, &num_flot, sizeof(int))) == -1)
	{ /* Error en la lectura */
		exit(1);
	}
	
	if ( num_flot > 999 ) { /* arch muy grande */ exit(1); }

	if ( (bytes_leidos = read(fd, buffer, num_flot*sizeof(float))) == -1)
	{ /* Error en la lectura */
		exit(1);
	}

}

16.8 Ejercicios

  1. Escribir un programa para copiar un archivo en otro. Los 2 nombres de los archivos son dados como los primeros argumentos del programa.

    Copiar bloques de 512 bytes cada vez.

    Revisar que el programa:
    	- tenga dos argumentos o mostrar "El programa requiere dos argumentos";
    	- el primer nombre de archivo sea de lectura o mostrar "No se puede abrir archivo ... para lectura";
    	- que el segundo nombre del archivo sea de escritura o mostrar "No se puede abrir archivo ... para escritura".
    
  2. Escribir un programa "ultimas" que muestre las últimas n líneas de un archivo de texto. n y el nombre del archivo deberán especificarse desde la línea de comandos. Por defecto n deberá ser 5. El programa deberá hacer el mejor uso del espacio de almacenamiento.
  3. Escribir un programa que compare dos archivos y muestre las líneas que difieran. Tip: buscar rutinas apropiadas para manejo de cadenas y manejo de archivos. El programa no deberá ser muy grande.