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) | |
|
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) | |
|
NOMPRO.DAT | Nombres de proveedores | ||
Campo | Descripción | Tipo | |
nom | Nombre del proveedor | char(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 |