Ingeniero Técnico en Informática de Gestión


Introducción al lenguaje C. Ficheros


10

Ficheros

Canales y ficheros

Ya quedó dicho que toda la E/S en C se hace mediante funciones de biblioteca. Esto, que ya se estudió para la E/S por consola, se estudiará en este capítulo para la E/S sobre cualquier dispositivo, en particular sobre archivos de disco.

El sistema de E/S del ANSI C proporciona un intermediario entre el programador y el dispositivo al que se accede. Este intermediario se llama canal o flujo y es un buffer independiente del dispositivo al que se conecte. Al dispositivo real se le llama archivo o fichero. Por tanto, programa y archivo están conectados por medio de un canal y la misma función permite escribir en pantalla, impresora, archivo o cualquier puerto. Existen dos tipos de canales:

  • Canales de texto: Son secuencias de caracteres. Dependiendo del entorno puede haber conversiones de caracteres (LF ⇔ CR + LF). Esto hace que el número de caracteres escritos/leídos en un canal pueda no coincidir con el número de caracteres escritos/leídos en el dispositivo.

  • Canales binarios: Son secuencias de bytes. A diferencia de los canales de texto, en los canales binarios la correspondencia de caracteres en el canal y en el dispositivo es 1 a 1, es decir, no hay conversiones.

Un archivo es, por tanto, un concepto lógico que se puede asociar a cualquier cosa susceptible de realizar con ella operaciones de E/S. Para acceder a un archivo hay que relacionarlo con un canal por medio de una operación de apertura que se realiza mediante una función de biblioteca. Posteriormente pueden realizarse operaciones de lectura/escritura que utilizan búfers de memoria. Para estas operaciones hay disponible un amplio conjunto de funciones. Para desasociar un canal de un archivo es necesario realizar una operación de cierre. Hay 5 canales que se abren siempre que comienza un programa C. Son:

  • stdin Canal estándar de entrada. Por defecto el teclado. (ANSI)

  • stdout Canal estándar de salida. Por defecto la pantalla. (ANSI)

  • stderr Canal estándar de salida de errores. Por defecto la pantalla. (ANSI)

  • stdaux Canal auxiliar (canal serie COM1). (A partir de Turbo C v2.0)

  • stdprn Canal para la impresora. (A partir de Turbo C v2.0)

Abrir y cerrar ficheros

Para poder manejar un fichero es necesario asociarle un canal. Esto se consigue mediante una operación de apertura que se realiza mediante la función de biblioteca fopen(), que devuelve un puntero al canal que se asocia. El prototipo de esta función está definido en stdio.h y es el siguiente:

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

Esta función abre el fichero nombre y le asocia un canal mediante el puntero a FILE que devuelve. El tipo de dato FILE está definido también en stdio.h y es una estructura que contiene información sobre el fichero. Consta de los siguientes campos:

typedef struct {

short level; //nivel de ocupación del buffer

unsigned flags; //indicadores de control

char fd; //descriptor del fichero (nº que lo identifica)

char hold; //carácter de ungetc()

short bsize; //tamaño del buffer

unsigned char *buffer; //puntero al buffer

unsigned char *curp; //posición en curso

short token; //se usa para control

} FILE;

No debe tocarse ninguno de los campos de esta estructura a menos que se sea un programador muy experto. Cualquier cambio no controlado en los valores de esas variables, posiblemente dañaría el archivo. Si se produce cualquier error en la apertura del fichero, fopen() devuelve un puntero nulo y el fichero queda sin canal asociado.

Los valores posibles del parámetro modo se muestran en la tabla siguiente:

MODO

DESCRIPCIÓN

r

Abre un fichero sólo para lectura. Si el fichero no existe fopen() devuelve un puntero nulo y se genera un error.

w

Crea un nuevo fichero para escritura. Si ya existe un fichero con este nombre, se sobreescribe, perdiéndose el contenido anterior.

a

Abre o crea un fichero para añadir. Si el fichero existe, se abre apuntando al final del mismo. Si no existe se crea uno nuevo.

r+

Abre un fichero para leer y escribir. Si el fichero no existe fopen() devuelve un puntero nulo y se genera un error. Si existe, pueden realizarse sobre él operaciones de lectura y de escritura.

w+

Crea un nuevo fichero para leer y escribir. Si ya existe un fichero con este nombre, se sobreescribe, perdiéndose el contenido anterior. Sobre el archivo pueden realizarse operaciones de lectura y de escritura.

a+

Abre o crea un fichero para leer y añadir. Si el fichero ya existe se abre apuntando al final del mismo. Si no existe se crea un fichero nuevo.

Para indicar si el canal asociado al fichero es de texto o binario, se añade al modo la letra t o b, respectivamente. Así, si modo es rt, se está abriendo el fichero en modo texto sólo para lectura, mientras que si modo es w+b se abrirá o creará un fichero en modo binario para lectura y para escritura.

Para cerrar un fichero y liberar el canal previamente asociado con fopen(), se debe usar la función fclose() definida en stdio.h y cuyo prototipo es

int fclose (FILE *canal);

Esta función devuelve 0 si la operación de cierre ha tenido éxito, y EOF en caso de error. EOF es una macro definida en stdio.h de la siguiente forma:

#define EOF (-1)

El siguiente programa abre sólo para lectura un fichero de nombre DATOS.DAT y, posteriormente lo cierra.

#include <stdio.h>

#include <process.h> //Para exit()

void main (void)

{

FILE *f;

int st;

f = fopen ("DATOS.DAT", "rt");

if (!f) {

puts ("Error al abrir el fichero");

exit (1);

}

//Aquí vendrían las operaciones sobre el fichero

st = fclose (f);

if (st) puts ("Error al cerrar el fichero");

}

Es habitual escribir la sentencia de apertura del fichero como sigue:

if (!(f = fopen ("DATOS.DAT", "rt"))) {

puts ("Error al abrir el fichero");

exit (1);

}

Control de errores y de fin de fichero

Cada vez que se realiza una operación de lectura o de escritura sobre un fichero debemos comprobar si se ha producido algún error. Para ello disponemos de la función ferror() cuyo prototipo, definido en stdio.h, es

int ferror (FILE *canal);

Esta función devuelve 0 si la última operación sobre el fichero se ha realizado con éxito. Hay que tener en cuenta que todas las operaciones sobre el fichero afectan a la condición de error, por lo que debe hacerse el control de error inmediatamente después de cada operación o, de lo contrario, la condición de error puede perderse. La forma de invocar esta función puede ser

if (ferror (f)) puts ("Error de acceso al fichero");

siendo f un puntero a FILE.

Se puede conseguir un mensaje asociado al último error producido mediante la función perror() cuyo prototipo, definido en stdio.h, es

void perror (const char *cadena);

Esta función envía a stderr (generalmente la pantalla) la cadena indicada en el argumento, dos puntos y, a continuación, un mensaje del sistema asociado al último error producido. La forma en que se relaciona el error con el mensaje es mediante una variable global predefinida llamada errno (definida en errno.h) que se activa cuando se producen errores. Por ejemplo, dado el segmento de programa

FILE *f;

if (!(f = fopen ("DATOS.DAT", "rb"))) {

printf ("\nError %d. ", errno);

perror ("DATOS.DAT");

exit (1);

}

si no existe el fichero DATOS.DAT se envía a pantalla el mensaje

Error 2. DATOS.DAT: No such file or directory

Cada vez que se realiza una operación de lectura sobre un fichero, el indicador de posición del fichero se actualiza. Es necesario, pues, controlar la condición de fin de fichero. Por ello, debemos saber que cuando se intentan realizar lecturas más allá del fin de fichero, el carácter leído es siempre EOF. Sin embargo en los canales binarios un dato puede tener el valor EOF sin ser la marca de fin de fichero. Es aconsejable, por ello, examinar la condición de fin de fichero mediante la función feof(), cuyo prototipo, definido en stdio.h, es

int feof (FILE *canal);

Esta función devuelve un valor diferente de cero cuando se detecta el fin de fichero.

Un algoritmo que lea todo un archivo puede ser el siguiente:

#include <stdio.h>

#include <process.h>

void main (void)

{

FILE *f;

if (!(f = fopen ("DATOS.DAT", "rt"))) {

perror ("\nDATOS.DAT");

exit (1);

}

//operación de lectura sobre DATOS.DAT

while (!feof (f)) {

//tratamiento

//operación de lectura sobre DATOS.DAT

}

fclose (f);

if (ferror (f)) puts ("Error al cerrar el archivo DATOS.DAT");

}

E/S de caracteres

Para leer caracteres individuales de un fichero se utiliza la función

int fgetc (FILE *canal);

o su macro equivalente

int getc (FILE *canal);

Ambas están definidas en stdio.h y son completamente idénticas. Devuelven el carácter leído e incrementan el contador de posición del fichero en 1 byte. Si se detecta la condición de fin de fichero, devuelven EOF, pero para canales binarios es mejor examinar dicha condición mediante feof().

Para escribir caracteres individuales en un fichero se utiliza la función

int fputc (int carácter, FILE *canal);

o su macro equivalente

int putc (int carácter, FILE *canal);

Ambas tienen el prototipo definido en stdio.h y son completamente idénticas. Escriben el carácter indicado en el argumento (que puede ser también una variable char) en el fichero asociado a canal. Si no hay error, devuelven el carácter escrito; en caso contrario devuelven EOF.

El siguiente programa copia carácter a carácter el fichero DATOS.DAT en COPIA.DAT.

#include <stdio.h>

#include <process.h>

void main (void)

{

FILE *fent, *fsal;

char caracter;

if (!(fent = fopen ("DATOS.DAT", "rb"))) {

perror ("DATOS.DAT");

exit (1);

}

if (!(fsal = fopen ("COPIA.DAT", "wb"))) {

perror ("COPIA.DAT");

exit (1);

}

caracter = getc (fent);

while (!feof (fent)) {

putc (caracter, fsal);

if (ferror (fsal)) puts ("No se ha escrito el carácter");

caracter = getc (fent);

}

fclose (fent);

fclose (fsal);

}

La misma operación puede realizarse en una sola línea mediante

while (!feof (fent)) putc (getc (fent), fsal);

Existen dos funciones análogas a getc() y putc() pero para leer y escribir enteros en lugar de caracteres:

int getw (FILE *canal);

int putw (int entero, FILE *canal);

Ambas están definidas en stdio.h y la única diferencia con getc() y putc() está en que se procesan dos bytes en cada operación. Estos dos bytes deben interpretarse como un número entero. La función getw() no debe usarse con ficheros abiertos en modo texto.

E/S de cadenas de caracteres

Para leer cadenas de caracteres se utiliza la función

char *fgets (char *cadena, int n, FILE *canal);

cuyo prototipo está definido en stdio.h. Esta función lee caracteres del fichero asociado a canal y los almacena en cadena. En cada operación de lectura se leen n - 1 caracteres, a menos que se encuentre primero un carácter nueva línea (que también se almacena en cadena). Si no se produce error, la función devuelve un puntero a cadena; en caso contrario, devuelve un puntero nulo.

El siguiente programa muestra el contenido del fichero AUTOEXEC.BAT, numerando las líneas. Se supone que ninguna línea tiene más de 80 caracteres.

#include <stdio.h>

#include <process.h>

void main (void)

{

FILE *f;

register int i = 1;

char cadena[81];

if (!(f = fopen ("AUTOEXEC.BAT", "rt"))) {

perror ("AUTOEXEC.BAT");

exit (1);

}

fgets (cadena, 80, f);

while (!feof (f)) {

printf ("%d: %s", i, cadena);

i++;

fgets (cadena, 80, f);

}

fclose (f);

}

Para escribir cadenas de caracteres se utiliza la función

int fputs (const char *cadena, FILE *canal);

cuyo prototipo está definido en stdio.h, y que escribe la cadena indicada en el argumento en el fichero asociado a canal. Hay que tener en cuenta que fputs() no copia el carácter nulo ni añade un carácter nueva línea al final. Si no se produce error, fputs() devuelve el último carácter escrito; en caso contrario devuelve EOF.

El siguiente programa escribe en la impresora las cadenas de caracteres que se van tecleando, hasta que se pulsa .

#include <stdio.h>

#include <string.h>

void main (void)

{

char *cadena[85];

printf ("\nTeclee cadena: ");

gets (cadena);

while (cadena[0]) {

strcat (cadena, "\n\r");

fputs (cadena, stdprn);

gets (cadena);

}

}

E/S de bloques de datos

La biblioteca estándar de C dispone de funciones que permiten leer y escribir bloques de datos de cualquier tipo. Las funciones que realizan estas operaciones son, respectivamente, fread() y fwrite(), cuyos prototipos, definidos en stdio.h, son los siguientes:

int fread (void *buffer, int nbytes, int contador, FILE *canal);

int fwrite (void *buffer, int nbytes, int contador, FILE *canal);

La función fread() lee contador bloques de datos del fichero asociado a canal y los sitúa en buffer. Cada bloque contiene nbytes bytes. La función fwrite() vuelca en el fichero asociado a canal los contador bloques de nbytes bytes que se encuentran almacenados a partir de buffer. Si la operación tiene éxito, fread() devuelve el número de bloques (no de bytes) realmente leídos. Análogamente, fwrite() devuelve el número de bloques realmente escritos. La declaración

void *buffer

indica que buffer es un puntero a cualquier tipo de variables.

En el programa siguiente se almacenan en el fichero MATRIZ.FLO un conjunto de 10 números de tipo float. La escritura se hace con una sentencia fwrite() para cada dato. En la operación de lectura se leen los 10 datos de una sola vez con una sentencia fread() y, a continuación, se muestran en pantalla.

#include <stdio.h>

#include <process.h>

#include <conio.h>

void main (void)

{

register int i;

FILE *f;

float elem, matriz[10];

if (!(f = fopen ("MATRIZ.FLO", "w+b"))) {

perror ("MATRIZ.FLO");

exit (1);

}

for (i = 0; i <= 9; i++) {

printf ("\nTeclee número: ");

scanf ("%f", &elem);

fwrite (&elem, sizeof (elem), 1, f);

}

rewind (f);

fread (matriz, sizeof (matriz), 1, f);

clrscr ();

for (i = 0; i <= 9; i++) printf ("\n%d: %f", i, matriz[i]);

fclose (f);

}

Fijémonos en algunos detalles del programa anterior. La sentencia

fwrite (&elem, sizeof (elem), 1, f);

vuelca en el fichero, cada vez, los 4 bytes -sizeof (elem)- de elem. Una vez finalizado el primero de los bucles for se han ejecutado 10 operaciones fwrite() y, por tanto, el indicador de posición del fichero apunta al final del mismo. Para poder realizar la lectura de los datos hemos de recolocar este indicador al principio del fichero. Esto puede hacerse de dos formas:

  • Cerrando el fichero y volviéndolo a abrir para lectura.

  • Mediante la función rewind().

En el ejemplo se utiliza el segundo método. La función rewind() reposiciona el indicador de posición del fichero al principio del mismo. Su prototipo, definido en stdio.h, es el siguiente:

void rewind (FILE *canal);

Siguiendo con el programa anterior, la sentencia

fread (matriz, sizeof (matriz), 1, f);

lee del fichero 40 bytes -sizeof (matriz)- y los sitúa en matriz. Fijémonos que ahora no es necesario escribir &matriz en el primer parámetro, pues matriz ya es un puntero.

Habitualmente fread() y fwrite() se utilizan para leer o escribir estructuras. Veámoslo con un programa que crea un archivo de listín telefónico. El registro de ese archivo constará de los siguientes campos:

Nombre 40 caracteres

Domicilio 40 caracteres

Población 25 caracteres

Provincia 15 caracteres

Teléfono 10 caracteres

El siguiente programa crea ese fichero, llamado LISTIN.TEL.

#include <stdio.h>

#include <conio.h>

#include <process.h>

typedef struct {

char nom[41];

char dom[41];

char pob[26];

char pro[16];

char tel[11];

} REG;

void main (void)

{

FILE *f;

REG var;

if (!(f = fopen ("LISTIN.TEL", "wb"))) {

perror ("LISTIN.TEL");

exit (1);

}

clrscr ();

printf ("Nombre: ");

gets (var.nom);

while (var.nom[0]) {

printf ("\nDomicilio: ");

gets (var.dom);

printf ("\nPoblación: ");

gets (var.pob);

printf ("\nProvincia: ");

gets (var.pro);

printf ("\nTeléfono: ");

gets (var.tel);

fwrite (&var, sizeof (var), 1, f);

if (ferror (f)) {

puts ("No se ha almacenado la información");

getch ();

}

clrscr ();

printf ("Nombre: ");

gets (var.nom);

}

fclose (f);

}

El siguiente programa ilustra como se pueden leer bloques de registros de un fichero. Concretamente lee los registros del fichero LISTIN.TEL en grupos de 4, mostrando en pantalla los campos Nombre y Teléfono.

#include <stdio.h>

#include <conio.h>

#include <process.h>

typedef struct {

char nom[41];

char dom[41];

char pob[26];

char pro[16];

char tel[11];

} REG;

void main (void)

{

FILE *f;

REG var[4];

int i, n;

if (!(f = fopen ("LISTIN.TEL", "rb"))) {

perror ("LISTIN.TEL");

exit (1);

}

do {

clrscr ();

n = fread (var, sizeof (REG), 4, f);

for (i = 0; i < n; i++) printf ("\n%-41s %s", var[i].nom, var[i].tel);

puts ("\nPulse una tecla ...");

getch ();

} while (!feof (f));

fclose (f);

}

Si nos fijamos en la sentencia fread() de este programa, se leen en cada operación 4 bloques de sizeof (REG) bytes (135, tamaño de cada registro). La misma cantidad de bytes se leería mediante la sentencia

fread (var, sizeof (var), 1, f);

sin embargo, así no tendríamos un control adecuado sobre la variable n.

E/S con formato

La función que permite escribir con formato en un fichero es

int fprintf (FILE *canal, const char*formato, lista de argumentos);

que escribe los argumentos de la lista, con el formato indicado, en el fichero asociado a canal. Esta función es idéntica a printf() salvo en que permite escribir en cualquier dispositivo y no sólo en stdout. Un uso de fprintf() se estudió en el Capítulo 4 para salida por impresora.

Para leer con formato existe la función

int fscanf (FILE *canal, const char*formato, lista de argumentos);

que es idéntica a scanf() salvo en que puede leer de cualquier dispositivo, no necesariamente de stdin.

Aunque estas funciones pueden ser de gran utilidad en ciertas aplicaciones, en general es más recomendable usar fread() y fwrite().

Acceso directo

El acceso directo (tanto en lectura como en escritura) a un archivo, se realiza con la ayuda de la función fseek() que permite situar el indicador de posición del archivo en cualquier lugar del mismo. El prototipo de esta función es:

int fseek (FILE *canal, long nbytes, int origen);

Esta función sitúa el indicador de posición del fichero nbytes contados a partir de origen. Los valores posibles del parámetro origen y sus macros asociadas se muestran en la siguiente tabla:

ORIGEN

VALOR

MACRO

Principio del fichero

0

SEEK_SET

Posición actual

1

SEEK_CUR

Fin del fichero

2

SEEK_END

La función devuelve 0 cuando ha tenido éxito. En caso contrario devuelve un valor diferente de 0.

Esta función simplemente maneja el indicador de posición del fichero, pero no realiza ninguna operación de lectura o escritura. Por ello, después de usar fseek() debe ejecutarse una función de lectura o escritura.

El siguiente programa crea un archivo llamado FRASE.TXT con una cadena de caracteres. Posteriormente lee un carácter de la cadena cuya posición se teclea.

#include <stdio.h>

#include <conio.h>

#include <string.h>

#include <process.h>

void main (void)

{

FILE *f;

int nbyte, st;

char frase[80], caracter;

if (!(f = fopen ("FRASE.TXT", "w+t"))) {

perror ("FRASE.TXT");

exit (1);

}

clrscr ();

printf ("Teclee frase: ");

gets (frase);

fwrite (frase, strlen (frase) + 1, 1, f);

printf ("\nLeer carácter nº: ");

scanf ("%d", &nbyte);

st = fseek (f, (long) nbyte, SEEK_SET);

if (st) puts ("Error de posicionamiento");

else {

caracter = getc (f);

if (caracter != EOF) printf ("\nEl carácter es: %c", caracter);

else puts ("Se sobrepasó el fin de fichero");

}

fclose (f);

}

Puede usarse fseek() para acceder a registros. Para ello debe calcularse previamente en qué byte del fichero comienza el registro buscado. El siguiente programa escribe registros ayudándose de fseek().

#include <stdio.h>

#include <conio.h>

#include <process.h>

typedef struct {

char nombre[40];

int edad;

float altura;

} REGISTRO;

void main (void)

{

FILE *f1;

REGISTRO mireg;

int num;

long int puntero;

if (!(f1 = fopen ("REGISTRO.DAT", "r+b"))) {

puts ("Error de apertura");

exit (1);

}

clrscr ();

printf ("Escribir registro nº: ");

scanf ("%d", &num);

while (num > 0) {

getchar ();

printf ("Nombre: ");

gets (mireg.nombre);

printf ("Edad: ");

scanf ("%d", &mireg.edad);

printf ("Altura: ");

scanf ("%f", &mireg.altura);

puntero = (num - 1) * sizeof (REGISTRO);

if (fseek (f1, puntero, SEEK_SET)) puts ("Error de posicionamiento");

else {

fwrite (&mireg, sizeof (mireg), 1, f1);

if (ferror (f1)) {

puts ("ERROR de escritura");

getch ();

}

}

clrscr ();

printf ("Escribir registro nº: ");

scanf ("%d", &num);

}

fclose (f1);

}

Ejercicios

1. Escribe un programa que vaya almacenando en un archivo todos los caracteres que se tecleen hasta que se pulse CTRL-Z. El nombre del archivo se pasará como parámetro en la línea de órdenes.

2. Escribe un programa que lea un archivo de texto y cambie todas las apariciones de una palabra determinada, por otra. El programa se ejecutará con la orden

PROG fichero palabra1 palabra2

siendo palabra1 la palabra sustituida por palabra2. El programa debe informar del número de sustituciones efectuadas.

Nota: Se supondrá que las líneas del fichero no tienen más de 80 caracteres.

3. En el club de baloncesto BAHEETO'S BASKET CLUB se está realizando una campaña de captación de jugadores altos. Se dispone de un fichero con datos de aspirantes, llamado ALTOS.DAT, que se describe a continuación

ALTOS.DAT

Aspirantes a jugadores del club

Campo

Descripción

Tipo

nom

Nombre del aspirante

char(41)

alt

Altura del aspirante (en metros)

float

pro

Provincia de nacimiento

char(26)

  • Los campos de tipo char incluyen el nulo final

  • El fichero almacena un máximo de 500 registros.

  • La provincia se almacena en mayúsculas.

  • El fichero no está ordenado.

Construye un programa que realice las siguientes operaciones:

  • Solicitar por teclado una provincia y almacenar en sendas matrices los nombres y las alturas de los aspirantes nacidos en la provincia indicada.

  • Calcular la altura media de todos los aspirantes de dicha provincia.

  • Emitir un informe impreso con los nombres y alturas de los aspirantes de la provincia cuya altura supere la media. El formato del listado debe ser el siguiente:

Provincia: xxxxxxxxxxxxxxxxxxxxxxxxx

Altura Media: x.xx

Nombre Altura

xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx x.xx

... 50 líneas de detalle por página

4. Se tienen dos archivos de datos ARTIC.DAT y NOMPRO.DAT, que se describen a continuación:

ARTIC.DAT

Maestro de Artículos

Campo

Descripción

Tipo

cod

Código del artículo

char(7)

des

Descripción

char(31)

exi

Existencia

unsigned

pco

Precio de compra

unsigned

pvp

Precio de Venta

unsigned

pro

Código del proveedor

char(5)

  • Los campos de tipo char incluyen el nulo final

  • Los campos cod y pro almacenan sólo dígitos numéricos y están completados a la izquierda con ceros. Por ejemplo, el artículo de código 65 se almacena como 000065.

  • El fichero está ordenado ascendentemente por el campo cod.

  • No están todos los valores de cod.

NOMPRO.DAT

Nombres de proveedores

Campo

Descripción

Tipo

nom

Nombre del proveedor

char(41)

  • Cada nombre de proveedor está almacenado en el registro cuyo número coincide con su código. Así, el nombre del proveedor de código 000041 se almacena en el registro 41.

Escribe un programa que emita un informe impreso con el formato de la página siguiente.

Al inicio del programa se solicitarán por teclado los códigos de artículo inicial y final que limitarán los registros que se deben listar.




Descargar
Enviado por:Juan
Idioma: castellano
País: España

Te va a interesar