9 GESTIÓN BÁSICA DE FICHEROS Contenido ___________________________________________________________________________ 9.1.- Introducción . 9.2.- Archivos C++. 9.2.1.- Apertura de ficheros. 9.2.2.- Cierre de ficheros. 9.2.3.- Detección de fin de fichero y otras funciones. 9.2.4.- Comprobación de apertura correcta. 9.3.- Lectura/Escritura en ficheros de texto. 9.3.1.- Avance del cursor. 9.3.2.- Ficheros de texto. 9.4.- Ficheros binarios. 9.4.1.- Utilidad de los ficheros binarios. 9.4.2.- Lectura/Escritura byte a byte. 9.4.3.- Lectura/Escritura por bloques de bytes. 9.5.- Acceso aleatorio a ficheros. 9.6.- Ficheros pasados como argumentos. 9.7.- Ejemplos de utilización de ficheros binarios y de texto. Ejercicios. ___________________________________________________________________________ En este capítulo vamos a tratar la gestión básica de ficheros (también llamados archivos) en C++ y lo haremos a partir del concepto de flujo. Es importante hacer notar que no trataremos el concepto de fichero como tradicionalmente se ha empleado en el lenguaje C pues esto requeriría el conocimiento del concepto de puntero el cual se trata en temas posteriores. Vamos a estudiar cómo declarar los identificadores que establecen el modo de flujos de los ficheros (de entrada o salida básicamente) y procuraremos no entrar en el concepto de clase de ___________________________________________________________________________ manera que la gestión de ficheros resulte asimilable por el alumno con los conceptos aprendidos hasta el momento. 9.1.- INTRODUCCION. En este tema se verán las características básicas que nos permitirán trabajar con ficheros (también llamados archivos). • ¿ Por qué se necesitan los ficheros? Todos los programas vistos hasta ahora trabajaban con información almacenada en memoria principal, no obstante, hay situaciones en que esto no es apropiado. Algunas de esas situaciones podrían ser: * Los datos con los que necesita trabajar el programa son demasiado grandes (ocupan mucha memoria) para que entren en la memoria principal. * Nos interesa mantener la información después de cada ejecución, necesitamos utilizar datos procedentes de otros programas (editores, etc.), o generar datos para que puedan ser utilizados por otros programas. En dichos casos se utilizarán ficheros para contener la información en memoria secundaria (disco duro, disquetes, etc.). • Definición de Fichero: Es una colección de elementos lógicamente relacionados y almacenados en memoria secundaria. A más bajo nivel, un fichero es una secuencia de bits almacenado en algún dispositivo externo (como por ejemplo uno de memoria secundaria). 9.2.- ARCHIVOS EN C++. En los temas anteriores hemos visto que los datos de entrada o salida en un programa C++ son obtenidos o enviados directamente a través de lo que llamamos flujos de entrada o salida. Hasta hora hemos manejado los flujos estándares cin y cout, y una serie de operadores (<< y >>) y funciones (como get o getline por ejemplo) disponibles a partir de la biblioteca iostream y que son útiles para la entrada/salida de la información. En C++ un fichero es simplemente un flujo externo que se puede abrir para entrada (dando lugar a un flujo de archivo de entrada que, para simplificar, llamaremos simplemente archivo o fichero de entrada), para salida (dando lugar a un flujo de archivo de salida que, para simplificar, llamaremos simplemente archivo o fichero de salida) o para entrada-salida (archivo o fichero de entrada-salida o archivo de E/S). ___________________________________________________________________________ ! C++ soporta dos tipos de archivos: de texto y binarios. Los primeros almacenan datos como códigos ASCII. Los valores simples, tales como números y caracteres están separados por espacios. Los segundos almacenan bits de forma directa y se necesita usar la dirección de una posición de almacenamiento. Una biblioteca en C++ que proporciona “funciones” y operadores para el manejo de ficheros es la biblioteca fstream. En general emplearemos ésta para la gestión básica de ficheros por lo que deberemos incluir el archivo de cabecera correspondiente en nuestros programas mediante la declaración adecuada, o sea, incluyendo la directiva de preprocesamiento siguiente #include <fstream.h> La biblioteca fstream ofrece un conjunto de funciones y operadores comunes a todas las operaciones de Entrada/Salida (E/S) de ficheros. 9.2.1.- Apertura de ficheros. Antes de que un programa pueda manipular un fichero para leer o escribir información se debe abrir (o crear si es necesario) el fichero para identificar la posición del mismo en el programa (o sea, la dirección de memoria a partir de la cual almacenaremos o leeremos el contenido del fichero). Ya hemos indicado anteriormente que los archivos son tratados como flujos de entrada/salida por lo que, al igual que sucede con los flujos estándares y por defecto cin y cout, la información “debe” ser transferida en una sola dirección por lo que, salvo en ciertos casos, los ficheros deben abrirse bien para entrada o bien para salida. 9.2.1.1.- Ficheros de entrada o salida. Supongamos que tenemos un fichero cuyo nombre en el sistema operativo es “nombre.extensión”. Entonces hay varias formas de abrirlo. (A) Como fichero de entrada: Para ello empleamos la sentencia ifstream descriptor (“nombre.extensión”); (B) Como fichero de salida: Para ello empleamos la sentencia ___________________________________________________________________________ " ofstream descriptor (“nombre.extensión”); En ambos casos descriptor es una variable que se asocia al fichero cuyo nombre es “nombre.extensión”. Comentario: Observe que a partir de la apertura de un fichero, el descriptor del fichero será utilizado en todas las operaciones que se realicen sobre el fichero (OJO! no utilizamos el nombre original del fichero). Otra forma de abrir un fichero para operar sobre él consiste en crear primero el flujo de entrada asociado al mismo, o sea, en asociar una variable descriptor a un fichero en una forma similar a como se declara una variable en C++, es decir, mediante una sentencia tal como ifstream descriptor; // Para ficheros de entrada ofstream descriptor; // Para ficheros de salida y, posteriormente, abrir el fichero (en el modo deseado: de entrada o salida por ejemplo) mediante el uso de la función open aplicada a ese flujo de entrada, o sea en la forma descriptor.open(“nombre.extensión”,int modo); donde la variable modo indica el modo de apertura del fichero y los modos de apertura, posiblemente combinados mediante el símbolo ‘|’ como veremos a continuación, pueden ser: ios:: in ios:: out ios:: app // Modo entrada // Modo salida // Modo añadir, o sea, posicionar el cursor del fichero (ver abajo) // al final del fichero antes de cada escritura ios:: binary // El archivo se abre en modo binario ios:: ate // El archivo se abre y el cursor se posiciona al final ios:: nocreate // Genera un error si el fichero no existe ios:: noreplace // Genera un error si el fichero existe ya Un archivo abierto con ofstream puede serlo en dos modos: ___________________________________________________________________________ # 1. Modo salida, usando ios::out. En este caso todos los datos en el fichero se descartan (o sea, se borra el contenido del fichero si existe previamente). Naturalmente si el fichero no existe lo crea. En ambos casos el cursor del fichero (o sea, la cabeza de lectura/escritura del mismo) se posiciona al principio del fichero. Este es el modo por defecto de apertura de un fichero de salida. 2. Modo añadir, usando ios::app. En este caso los datos adicionales se añaden a partir del final del fichero (o sea, el cursor se sitúa al final del fichero y es ahí a partir de donde se empieza a realizar la escritura). También se pueden combinar las dos formas anteriormente vistas de forma que el descriptor de fichero es declarado a la vez que el fichero es abierto. Para ello empleamos las siguientes declaraciones (según el fichero se abra para entrada o para salida): ifstream descriptor(“nombre.extensión”,int modo); // para entrada ofstream descriptor(“nombre.extensión”,int modo); // para salida donde modo es como se ha mostrado anteriormente. Ejemplo 1: Las dos líneas siguientes abren el fichero “mio.txt” como fichero de entrada (para lectura) y lo asocian al descriptor in. ifstream in; in.open(“mio.txt”); // descriptor del fichero a abrir // Apertura del fichero; Esas dos líneas equivalen a la siguiente: ifstream in (“mio.txt”); // Apertura del fichero; Ejemplo 2: Para abrir el fichero “salida.dat” en modo salida (si el fichero no existe lo crea, y si existe borra su contenido) asociándolo al descriptor out podemos usar la siguiente sentencia; ofstream out("salida.dat"); o la siguiente ___________________________________________________________________________ $ ofstream out("salida.dat", ios::out); o también ofstream out; out.open("salida.dat"); 9.2.1.2.- Ficheros de entrada/ salida. Un fichero puede ser también abierto para entrada-salida. En este caso emplearemos una declaración fstream combinada con la operación de apertura correspondiente, o sea: fstream descriptor; descriptor.open(“nombrefichero.ext”, ios::in | ios::out) O alternativamente podemos combinar la declaración y la apertura en una sola sentencia. Por ejemplo: fstream descriptor(“nombre.extensión”,ios::in | ios:: out); // para entrada-salida La declaración fstream puede ser usada arbitrariamente tanto sobre archivos de escritura como sobre archivos de lectura, aunque recomendamos que para archivos de solo-lectura se use ifstream y para aquellos de solo-escritura se use ofstream. Comentario: Cuando un fichero de E/S se declara con fstream, abrirlo con open debemos especificar el modo de apertura que queremos (entrada, salida o bien entrada.salida). Ejemplo 3: // Abrimos el fichero “F1.dat” como fichero de entrada fstream inout; inout.open("F1.dat", ios::in); // Intentamos abrir el fichero “F1.dat” pero tenemos un error puesto que no // hemos especificado el modo de apertura fstream inout; inout.open("F1.dat"); ___________________________________________________________________________ % // Abrimos el fichero “F2.txt” como fichero de salida en modo AÑADIR. fstream inout; inout.open("F2.txt", ios::app); // Abrimos el fichero “binario.dat” para entrada-salida binaria y le asociamos el // descriptor ejemplo. fstream ejemplo ("binario.dat", ios::in | ios:: out | ios::binary); 9.2.2.- Cierre de ficheros. Un fichero anteriormente abierto y con un descriptor asociado a él debe ser cerrado con el fin de liberar los recursos asociado a él de la siguiente forma: descriptor.close() 9.2.3.- Detección de fin de fichero y otras funciones. Además de las funciones open y close, existen otras funciones disponibles en la biblioteca fstream, que pueden ser aplicadas directamente al descriptor de un fichero en la forma Descriptor.función(); Donde función es una de las siguientes: • La función eof() que devuelve "true" si se ha alcanzado el final del fichero y falso en cualquier otro caso. LECTURA ADELANTADA: Para que la función eof() devuelva un valor de verdad (actualizado) es necesario, en muchos casos, realizar una operación de lectura previa. Esto permite que se actualice el valor a devolver por la función eof comprobándose si tras realizar dicha operación se ha llegado al final del fichero. A este proceso lo llamaremos lectura adelantada (ver el ejemplo 6). • La funciones fail() o bad() que devuelven "true" si existe un error en una operación de flujo asociada al fichero identificado por la variable descriptor ___________________________________________________________________________ & (bien sea en una apertura, un cierre, una escritura o una lectura entre otras posibles operaciones) y "false" en caso contrario. La diferencia entre estas dos funciones es muy sutil. En realidad un flujo puede estar en un estado de fallo (i.e., fail) pero no en un estado erróneo (i.e. bad) en el caso de que el flujo no se considere corrupto ya que no se han perdido los caracteres que provienen del flujo (en un estado erróneo, esto puede no ocurrir). Recomendamos un vistazo a algún manual de C++ para aclarar las diferencias tan sutiles. • La función good() que devuelve "true" si no existe un error en una operación de flujo y "false" en caso contrario. 9.2.4.- Comprobación de apertura correcta Antes de empezar a leer o escribir en un fichero es siempre conveniente verificar que la operación de apertura se realizó con éxito. La comprobación del "buen estado" de un determinado flujo asociado a un fichero se puede realizar preguntando directamente sobre el descriptor de fichero asociado (al igual que se hace con los flujos estándares cin y cout) en la forma siguiente: if (descriptor){ \\Buen estado. Continuamos \\ operando sobre el fichero ..... } if (! descriptor){ \\ Mal estado. Escribimos un mensaje \\ a la salida estándar cout << “Error en la apertura “ ..... } o mejor aun, utilizando las funciones good y bad (o fail) vistas anteriormente, o sea: if (descriptor,good()){ \\Buen estado. Continuamos \\ operando sobre el fichero ..... } if (descriptor.bad()){ // o descriptor.fail() \\ Mal estado. Escribimos un mensaje \\ a la salida estándar cout << “Error en la apertura “ } Ejemplo 4: ifstream in("F1.dat"); if (!in) // O bien, // if (in.bad() ) { ___________________________________________________________________________ ' cout << "\n Incapaz de crear o abrir el fichero "; // salida estándar cout << " para salida " << endl; } else{ …. // Se opera sobre el fichero } 9.3.- LECTURA-ESCRITURA EN FICHEROS (DE TEXTO). 9.3.1.- Avance del cursor Tanto para los ficheros de texto como para los ficheros binarios existe lo que se denomina el cursor o apuntador del fichero que es un índice que direcciona una posición relativa del fichero. El cursor marca en todo momento la posición actual del fichero a partir de la cual va a tener lugar la siguiente operación de entrada-salida a ejecutar sobre el mismo (bien sea una operación de lectura o una de escritura). Cada vez que se realiza una operación de entrada o salida, el cursor del fichero avanza automáticamente el número de bytes leídos o escritos. 9.3.2.- Ficheros de texto La lectura y la escritura en un archivo de texto se puede realizar directamente con los operadores << y >> al igual que se realiza sobre los flujos estándares cin y cout. Ejemplo 5: El siguiente programa escribe tres líneas en un fichero llamado “EJEMPLO5.TXT” que se crea en el programa (si ya existe borramos su contenido). Cada línea consta de un entero, un real y una cadena de caracteres. Los datos en cada línea están separados por espacios en blanco. #include <fstream.h> // Biblioteca para el manejo de ficheros #include <iostream.h> // Biblioteca para la entrada-salida estándar int main(){ ofstream fichout("EJEMPLO5.TXT",ios::out); if (!fichout) cout << "\n Incapaz de crear este o abrir el fichero \n"; else { fichout << 1 << " " << 5.0 << " APROBADO" << endl; // Escritura en el fichero fichout << 2 << " " << 1.1 << " SUSPENSO" << endl; fichout << 3 << " " << 8.0 << " NOTABLE " << endl; fichout.close(); } } // Fin del main ___________________________________________________________________________ ( Comentario: El operador >> omite los espacios en blanco al igual que ocurría en la entrada estándar. Ejemplo 6: El siguiente programa lee el fichero de texto “EJEMPLO5.TXT”, creado en el ejemplo anterior, y visualiza su contenido en el monitor. #include <fstream.h> // Libreria para el manejo de ficheros #include <iostream.h> typedef char TCadena[30]; int main(){ int i; float r; TCadena cad; ifstream fichin("EJEMPLO5.TXT"); // declaracion y apertura del fichero if (!fichin) cout << "\n Incapaz de crear o abrir el fichero "; else{ fichin >> i; // Observese la lectura adelantada!!! while (!fichin.eof()){ cout << i << " "; // Lectura de valores en el fichero fichin >> r; cout << r << " "; // Lectura de valores en el fichero fichin >> cad; cout << cad << "\n"; // Lectura de valores en el fichero fichin >> i; } fichin.close(); } // Fin del main Comentarios: (1) Observe en el ejemplo anterior que ha sido necesario realizar una lectura adelantada previamente al chequeo del fin de fichero. Si no se realiza de esta forma podriamos tener problemas. (2) Observe también que no es necesario realizar una lectura para “saltarse” los espacios en blanco que fueron introducidos en el fichero “EJEMPLO5.TXT” en el ejemplo 5. Esto es debido a que, como ya se comentó anteriormente, el operador >> omite los espacios en blanco (3) No olvide cerrar los ficheros!! 9.4.- FICHEROS BINARIOS. Hasta ahora hemos trabajado con ficheros de texto, éstos pueden ser vistos como una serie de ___________________________________________________________________________ ) caracteres que pertenecen a un determinado código de entrada/salida, en nuestro caso al código ASCII. También hemos visto cómo se abre un fichero binario. En esta sección vamos a ver cómo podemos leer y escribir datos en binario. 9.4.1.- Utilidad de los ficheros binarios. En esta sección explicamos la diferencia de los ficheros binarios con los de texto y lo hacemos a partir de un ejemplo sencillo.. Por ejemplo, si creamos un fichero con ofstream fich("F1.dat",ios::out); if (!fich) { cout << "\n Incapaz de crear este o abrir el fichero "; cout << " para salida " << endl; } else{ fich << "HOLA ADIOS"; fich.close(); } // Escribe la cadena en el fichero "F1.dat" “F1.dat” será un fichero que contiene 10 bytes, donde cada uno será el código ASCII correspondiente a cada uno de los caracteres que hemos escrito en el fichero (un carácter ocupa un byte). Esta característica hace que un fichero de texto pueda ser usado por otros programas que supongan que el contenido del fichero es un conjunto de caracteres del código ASCII. Sabemos que los datos se almacenan en memoria según las características del tipo con que han sido declarados. Así, para: char C; unsigned N; La variable C usa un byte en memoria principal, y en el se almacena el código ASCII de un carácter, mientras que la variable N, al ser de tipo entero sin signo, usa dos bytes, y en ellos se almacena el código binario correspondiente a un número entero sin signo. Esta característica sugiere que para almacenar en el fichero de texto el número contenido en la variable N no basta con grabar directamente los dos bytes en el fichero (Ejemplo, si N contiene el número 13864, para almacenarlo en el fichero se requieren 5 caracteres, mientras ___________________________________________________________________________ que en memoria la variable N solo ocupa dos bytes). Así pues, una instrucción fich << N; ocasionará dos pasos: 1) Convertir N a su configuración en caracteres ASCII. 2) Escribir en el fichero los caracteres ASCII obtenidos. En el caso del tipo CHAR, no es necesario efectuar la conversión pues la variable en memoria principal se corresponde con (es decir, contendrá como valor) el código ASCII correspondiente al carácter almacenado en la misma. Hay situaciones en las que en lugar de convertir los valores a escribir en el fichero en una serie de caracteres, puede resultar útil efectuar la grabación en el fichero directamente a partir contenido de la memoria. A los ficheros creados de esta forma se les llama ficheros binarios. Ventajas del uso de ficheros binarios:: • El proceso de lectura ó escritura es más rápido, pues nos ahorramos el tiempo necesario para la conversión. • Normalmente un fichero binario ocupa menos memoria que su correspondiente fichero de texto (Ejemplo, escribir 32534 en formato de texto ocuparía cinco bytes, mientras que hacerlo en formato binario ocuparía dos bytes). Inconvenientes del uso de ficheros binarios: • No podrán ser usados por programas de carácter general. Por ejemplo, si intento usar la orden TYPE (de MS-DOS) o la orden ls (de UNIX) con un fichero binario, la información que obtengo por pantalla no es legible. De todo lo dicho anteriormente se desprende que es conveniente usar ficheros de texto cuando pueda interesarnos tratar la información contenida en éstos con programas de carácter general (Editor, TYPE, PRINT, etc.), y en otro caso usar ficheros binarios. Tratamiento de ficheros binarios. Puesto que no hay que distinguir entre diferentes formatos para diferentes tipos de datos, sólo se necesitan instrucciones que lean y escriban un cierto número de bytes por lo que en realidad sólo necesitamos un tipo de instrucción para lectura y un tipo de instrucción para escritura. Aún así veremos dos formas diferentes de leer y escribir desde y en ficheros binarios. ___________________________________________________________________________ ! 9.4.2.- Lectura/Escritura byte a byte. 9.4.2.1.- Lectura. Para leer un carácter (es decir un byte) desde un fichero usamos la función get aplicada sobre el descriptor del mismo de la siguiente manera descriptor.get(ch); Esta instrucción extrae un único carácter del flujo de entrada, incluyendo el espacio en blanco y lo almacena en la variable ch (que debe ser declarada de tipo carácter). Esta función avanza además un byte la posición del cursor del archivo identificado por la variable descriptor. Ejemplo 7: El siguiente programa lee (byte a byte) un fichero binario que contiene caracteres y visualiza su contenido en el monitor. #include <fstream.h> // Libreria para el manejo de ficheros #include <iostream.h> int main() { char c; ifstream fichin("F1.dat",ios::in | ios::binary); if (!fichin) cout << "\n Incapaz de Abrir el fichero "; else{ fichin.get(c); // LECTURA ADELANTADA!! while (fichin){ // Tambien vale 'while (!fichin.eof())' cout << c; fichin.get(c); }; fichin.close(); } // Fin del main 9.4.2.2.- Escritura. Para escribir o mandar un carácter (es decir un byte) a un archivo de de salida usamos la función put aplicada al descriptor de fichero del archivo de la siguiente manera descriptor.put(ch); ___________________________________________________________________________ " Esta instrucción coloca el carácter contenido en ch en el archivo de salida identificado por la variable descriptor (o sea, en su caso en el fichero asociado a dicho descriptor) y lo hace a partir de la posición actual del cursor. A continuación avanza el cursor del fichero al siguiente byte. Ejemplo 8: El siguiente programa escribe un texto (byte a byte) en el fichero “Ejemplo8.dat”. #include <fstream.h> #include <iostream.h> int main(){ char cad[17]="TEXTO A ESCRIBIR"; ofstream fichout("Ejemplo8.dat", ios::out | ios::binary); if (!fichout) cout << "\n Incapaz de Crear o Abrir el fichero "; else{ for (int i=0;i<=16;i++) fichout.put(cad[i]); // Escritura en el fichero fichout.close(); } } // Fin del main 9.4.3.- Lectura/Escritura por bloque de bytes. Otra manera alternativa de leer y escribir en ficheros binarios consiste en utilizar las funciones read() y write(). 9.4.3.1.- Lectura. La función read tiene varios formatos aunque a continuación explicamos el formato básico De forma intuitiva, una llamada de la forma descriptor.read(&c, num); donde c es una variable de un tipo arbitrario (por ejemplo de un tipo TElemento), pasada por ___________________________________________________________________________ # referencia, y num es un entero o una variable de tipo entero, ejecuta una lectura (a partir de la posición actual del cursor del fichero asociado a la variable descriptor) de num bytes del fichero y los coloca en la variable c. En definitiva la llamada lee num bytes del fichero y los coloca en la variable c. Esta función puede extraer caracteres hasta alcanzar el fin del fichero. 9.4.3.2.- Escritura. Asumiendo que c es una variable de un tipo arbitrario (por ejemplo de un tipo TElemento) pasada por referencia, que num es un entero o una variable conteniendo un entero que descriptor esta asociado a un fichero, una llamada de la forma Descriptor.write(&c, num); Escribe, a partir de la posición del cursor, el contenido de c en el fichero asociado a descriptor. En realidad sólo escribe num bytes que corresponden a los num bytes siguientes que se encuentran en memoria a partir de la dirección en la que se encuentra almacenado el contenido de c. Esta instrucción escribe a partir de la posición indicada por el puntero de lectura/escritura del fichero asociado a descriptor los num bytes contenidos en el parámetro c. Se puede observar que en el segundo parámetro hay que indicar el número de bytes que ocupa el valor que se desea escribir. Para conocer éste dato de una forma fácil, podremos usar la función sizeof. Ejemplo 9: El siguiente programa declara el fichero “F.dat” para entrada-salida, graba en dicho fichero el valor 1234.86 en binario y después los veinte primeros enteros. Posteriormente, lee el fichero visualizando su información en la salida estándar (el monitor). #include <fstream.h> #include <iostream.h> int main(){ float R=1234.86; int i,N; fstream fichbin("F.dat",ios::binary | ios::out); // Apertura como salida fichbin.write(&R,sizeof(float)); for (i=1;i<=20;i++) ___________________________________________________________________________ $ fichbin.write(&i,sizeof(int)); fichbin.close(); fichbin.open("F.dat",ios::binary | ios::in); fichbin.read(&R,sizeof(float)); cout <<endl << "R= " << R << endl; for (i=1;i<=20;i++){ fichbin.read(&N,sizeof(int)); cout <<endl << i << "= " << N << endl; } } // Fin del main // Apertura como entrada Observa que la apertura y el cierre de ficheros binarios se puede efectuar con las mismas operaciones estudiadas para ficheros de texto. 9.5. Acceso aleatorio a ficheros. Sabemos que las operaciones de lectura y escritura siempre se realizan a partir de la posición indicada por el puntero de lectura/escritura del fichero. Para poder acceder correctamente al fichero, debemos saber como cambia dicho puntero de lectura/escritura. Hasta ahora hemos visto, que tenía un comportamiento secuencial, es decir, siempre se incrementa en el número de caracteres leídos ó escritos con cada instrucción, no obstante, puede haber situaciones en las que éste comportamiento no se adapte a nuestras necesidades. Supongamos que deseamos acceder a información situada cerca del final del fichero. Según lo visto hasta ahora, debemos leer previamente toda la información que le precede con objeto de ir avanzando el puntero de lectura/escritura progresivamente. Puesto que conocemos la situación de la información que queremos leer ó escribir, parece más conveniente poder situar el puntero de lectura/escritura directamente en dicha posición. Para gestionar éste tipo de comportamiento disponemos de una serie de funciones. Ya hemos visto que el acceso a un fichero se describe en términos de lo que denominamos el cursor o apuntador del fichero que es un índice que apunta a una posición relativa del fichero de manera que en todo momento la posición actual del cursor marca el lugar a partir del cual tendrá lugar la operación (de lectura o escritura) sobre el fichero. Además, cada vez que se realiza una operación de entrada o salida, el cursor del fichero avanza automáticamente dependiendo del número de bytes leídos o escritos. Básicamente existen dos instrucciones de acceso aleatorio para cada tipo de acceso a un fichero. Las vemos a continuación. ___________________________________________________________________________ % 9.5.1.- Lectura. Las funciones de acceso aleatorio para lectura son seekg y tellg. • seekg(pos) mueve la posición del cursor del fichero a la posición relativa del fichero • indicada por pos, donde pos es un entero (o variable conteniendo un entero). tellg() devuelve la posición relativa actual del cursor asociado al fichero de entrada y devuelve un –1 en caso de existir algún error. 9.5.2.- Escritura. Las funciones de acceso aleatorio equivalentes para escritura on seekp y tellp. • seekp(pos) mueve la posición del cursor del fichero a la posición absoluta del fichero • indicada por pos. tellp() devuelve la posición absoluta actual del cursor asociado al fichero de salida y devuelve un –1 en caso de existir algún error. Ejemplo 10: El siguiente programa almacena en un fichero los 10 primeros enteros, luego muestra por pantalla el quinto entero (o sea el 5), posteriormente lo reemplaza por el valor 100, y al final visualiza en el monitor el contenido del fichero. #include <fstream.h> #include <iostream.h> int main(){ int i,N; fstream fichbin("ejemplo11.dat",ios::binary | ios::in | ios::out); for (i=1;i<=10;i++) fichbin.write(&i,sizeof(int)); // Almacena los 10 primeros enteros fichbin.seekp(4*sizeof(int)); // se posiciona al principio del quinto/ entero fichbin.read(&N,sizeof(int)); // Lee dicho entero cout <<endl << "Quinto= " << N << endl; // visualiza el valor 5 en pantalla fichbin.seekp(4*sizeof(int)); // se posiciona de nuevo al principio del quinto entero // pues el cursor había avanzado. i=100; fichbin.write(&i,sizeof(int)); // Modifica el valor 5 por el valor 100; fichbin.seekp(0*sizeof(int)); // se posiciona de nuevo al principio del fichero for (i=1;i<=10;i++){ fichbin.read(&N,sizeof(int)); ___________________________________________________________________________ & cout <<endl << i << "= " << N << endl; } fichbin.close(); } // Fin del main // Se visualiza el contenido en pantalla 9.6.- Ficheros pasados como argumentos. En esta sección simplemente comentamos que cuando el descriptor de un fichero es pasado como argumento, la mayoría de las veces debe hacerlo como parámetro por referencia puesto que cualquier operación de lectura/escritura (y la mayoría de las de acceso directo) modifican la posición del cursor asociado a ese fichero. Ejemplo 11: El siguiente programa simplemente añade la cadena "HOLA" al fichero "poesia.txt". Observe que el argumento formal del procedimiento escribir_hola esta declarado por referencia. #include <fstream.h> void escribir_hola(fstream &canal_salida){ canal_salida << endl << "HOLA" << endl; } int main(){ fstream out("poesia.txt",ios::app); escribir_hola(out); out.close(); } // Fin del main 9.7.- Ejemplos de utilización de ficheros binarios y de texto. En esta sección mostramos dos ejemplos de cómo podemos leer/escribir datos desde/a ficheros binarios o de texto. Comentario: Observe que en la apertuira de ficheros binarios ES NECESARIO, en algunos compiladores 8como es el caso del DEV-C++) especificar el modo de apertura (in, out, app, ..., etc) a pesar de que este sea el modo por defecto. Si no se especifica entonces podemos tener problemas en la lectura de los datos. ___________________________________________________________________________ ' Ejemplo 12: El siguiente programa lee, desde teclado una información relativa a una serie de empleados de una empresa. Los datos a leer son el nombre, un código formado por cuatro enteros - ojo, no por cuatro dígitos- y una cantidad correspondiente al sueldo mensual. Entonces, toda la información recogida es grabada en dos ficheros (uno de texto y otro binario) para ser posteriormente leída desde los mismos y visualizada en pantalla. #include <iostream.h> #include <stdlib.h> #include <fstream.h> //CONSTANTES const int MAXCAD = 20; const int MAXVECT = 4; //TIPOS typedef int TVector[MAXVECT]; typedef char TCadena[MAXCAD+1]; typedef struct TPersona{ TCadena nombre; TVector codigo; // exactamente cuatro cifras float sueldo; }; void VisualizaPersonasFichero(TCadena nombreFichero,int numempleados); void EscribePersonaFichero(TCadena nombreFichero, TPersona p); void VisualizaPersonasFicheroBin(TCadena nombreFichero,int numempleados); void EscribePersonaFicheroBin(TCadena nombreFichero, Tpersona p); void pintaVector(TVector v); void pintaPersona(TPersona p); void leePersona(TPersona &p); int main(){ Tpersona per; TVector cod; int i,N; cout << "introduzca el número de personas a incluir "; cin >> N; for (i=1; i <= N ; i ++){ leePersona(per); // Escribo la información en texto y en binario EscribePersonaFichero("empleados.txt",per); EscribePersonaFicheroBin("empleados.dat",per); } cout << "Mostramos la información almacenada" << endl; cout << "Visualizo desde el fichero de texto: " << endl; VisualizaPersonasFichero("empleados.txt",N); cout << endl; cout << "Visualizo desde el fichero binario: " << endl; ___________________________________________________________________________ ( VisualizaPersonasFicheroBin("empleados.dat",N); cout << endl; system("PAUSE"); return 0; } // Fin del main // Este proc. visualiza la información almacenada en el fichero de texto void VisualizaPersonasFichero(TCadena nombreFichero, int numempleados) { ifstream in; int i,j; TPersona p; in.open(nombreFichero); if (in.bad()) // Comprobamos el estado de la apertura cout << "Error al abrir el fichero: " << nombreFichero << endl; else{ for (i=1;i<=numempleados; i++){ in >> p.nombre; for(j=0;j<MAXVECT;j++) in >> p.codigo[j]; in >> p.sueldo; pintaPersona(p); } in.close(); } } // Fin VisualizaPersonasFichero // El siguiente proc. Escribe la informacion de una persona en un fichero de texto void EscribePersonaFichero(Tcadena nombreFichero, TPersona p) { ofstream out; int i; out.open(nombreFichero,ios::app); if (out.bad()) cout << "Error al abrir el fichero: " << nombreFichero << endl; else{ out << p.nombre << endl; for(i=0;i<MAXVECT;++i) out << p.codigo[i] << " "; // Ojo, incluimos un espacio en medio!! out << p.sueldo << endl; out.close(); } } // Fin EscribePersonaFichero // Este proc. visualiza la información almacenada en el fichero binario void VisualizaPersonasFicheroBin(TCadena nombreFichero,int numempleados) { ifstream in; ___________________________________________________________________________ ! ) int i; TPersona p; in.open(nombreFichero, ios::in | ios::binary); // Ojo, hay que poner 'in' forzosamente if (in.bad()) cout << "Error al abrir el fichero: " << nombreFichero << endl; else{ for (i=1;i<=numempleados;i++) { in.read(&p,sizeof(TPersona)); // Se lee toda la información !! pintaPersona(p); } in.close(); } } // Fin VisualizaPersonasFicheros // El siguiente proc. Escribe la informacion de una persona en un fichero binario void EscribePersonaFicheroBin(TCadena nombreFichero, TPersona p) { ofstream out; out.open(nombreFichero, ios::binary | ios::app); // Ojo, especificamos el modo AÑADIR if (out.bad()) cout << "Error al abrir el fichero: " << nombreFichero << endl; else{ out.write(&p,sizeof(TPersona)); out.close(); } } // Fin EscribePersonaFicheroBin // El siguiente proc. visualiza un código de cuatro enteros (o sea, un vector) a la pantalla void pintaVector(TVector v){ int i; cout << "CODIGO : { "; for(i=0;i<MAXVECT-1;++i) cout << v[i] << ", " ; cout << v[MAXVECT-1] << " } "; } // El siguiente proc. visualiza la información de una persona en la pantalla void pintaPersona(TPersona p){ cout << "NOMBRE : " << p.nombre << " "; pintaVector(p.codigo); cout << "SUELDO : " << p.sueldo << endl; } // El siguiente proc, lee información de una persona desde el teclado void leePersona(TPersona &p) { int i; ___________________________________________________________________________ ! cout << "Nombre ? "; cin >> p.nombre; cout << endl << "Codigo (4 enteros-NO cifras solo) ?"; for(i=0;i<MAXVECT;++i) cin >> p.codigo[i]; cout << endl << "Sueldo (euros) ?"; cin >> p.sueldo; } Comentario: El primer argumento de read y write no necesita el símbolo & cuando es de tipo array como se ve en el siguiente ejemplo. Ejemplo 13: El siguiente programa escribe el vector que contiene la información {0,1,2,3,....,8,9} tanto en un fichero binario como en uno de texto. #include <iostream.h> #include <stdlib.h> #include <fstream.h> //CONSTANTES const int MAXCAD = 20; const int MAXVECT = 10; //TIPOS typedef int TVector[MAXVECT]; typedef char TCadena[MAXCAD+1]; void LeeVectorFichero(TCadena nombreFichero, TVector &v); void EscribeVectorFichero(TCadena nombreFichero, TVector v); void LeeVectorFicheroBin(TCadena nombreFichero, TVector &v); void EscribeVectorFicheroBin(TCadena nombreFichero, TVector v); void pintaVector(TVector v); int main() { TVector v; int i; for(i=0;i<MAXVECT;++i) // Inicializo el vector { v[i]=i; } cout << "Inicializo el vector y lo escribo " << "en ficheros de texto y binario: " << endl; pintaVector(v); ___________________________________________________________________________ ! ! EscribeVectorFichero("vector.txt",v); EscribeVectorFicheroBin("vector.dat",v); for(i=0;i<MAXVECT;++i) // Borro el vector { v[i]=0; } cout << "Reseteo el contenido del vector:" << endl; pintaVector(v); cout << "Leo v desde el fichero de texto: " << endl; LeeVectorFichero("vector.txt",v); pintaVector(v); cout << "Leo v desde el fichero binario: " << endl; LeeVectorFicheroBin("vector.dat",v); pintaVector(v); system("PAUSE"); return 0; } // Lee un vector desde un fichero de texto void LeeVectorFichero(TCadena nombreFichero, TVector &v) { ifstream in; int i; in.open(nombreFichero); if (in.bad()) // Comprobamos el estado de la apertura { cout << "Error al abrir el fichero: " << nombreFichero << endl; } else { for(i=0;i<MAXVECT;++i) in >> v[i]; in.close(); } } // Escribe un vector en un fichero de texto void EscribeVectorFichero(TCadena nombreFichero, TVector v) { ofstream out; int i; out.open(nombreFichero); if (out.bad()) { cout << "Error al abrir el fichero: " << nombreFichero << endl; } else ___________________________________________________________________________ ! " { for(i=0;i<MAXVECT;++i) out << v[i] << " "; out.close(); } } // Lee un vector desde un fichero binario void LeeVectorFicheroBin(TCadena nombreFichero, TVector &v) { ifstream in; in.open(nombreFichero); if (in.bad()) { cout << "Error al abrir el fichero: " << nombreFichero << endl; } else { in.read(v,sizeof(TVector)); in.close(); } } // Escribe un vector a un fichero binario void EscribeVectorFicheroBin(TCadena nombreFichero, TVector v) { ofstream out; out.open(nombreFichero); if (out.bad()) { cout << "Error al abrir el fichero: " << nombreFichero << endl; } else { out.write(v,sizeof(TVector)); out.close(); } } // Visualiza un vector a la pantalla void pintaVector(TVector v) { int i; cout << " v = { "; for(i=0;i<MAXVECT-1;++i) { cout << v[i] << ", " ; } cout << v[MAXVECT-1] << " } " << endl; } Una vez ejecutado el programa la salida es la siguiente: ___________________________________________________________________________ ! # * + , - , 3 L 2 4 > 2 1 2 1 2 @ A ? @ @ A B ? @ 1 A 2 C A ; 2 A 2 C A - A A A . / 9 E 2 E A / A A P A , - , - A F G A 2 A H @ I A 2 A 2 4 J 8 1 2 + 9 , - : 2 5 1 7 ; 2 4 2 < 4 1 6 8 , + . 5 , 1 = - 4 1 5 2 < 4 1 K , J / , K 8 - 5 3 2 A I 4 / ; 1 - K 2 A A 7 J 1 5 5 2 A @ H 2 G , A 5 A : 1 I ; @ 2 A O A 1 : F / H ; @ , 6 G + @ 9 5 A 2 A / 1 F 4 @ D 4 A 2 D O - E + @ ; A B + A 1 A 7 2 D - @ 2 3 A 7 A / C / 2 ; , 2 A 2 ; 3 7 1 B 1 3 > 5 0 A 2 ? > 3 , @ 2 3 N / ? 7 3 M M . > + . 5 , 1 K . Q . 5 . - 1 + 4 , + O . 5 R R R R R R R R R R R ___________________________________________________________________________ ! $ EJERCICIOS 1.Escribir un programa con la opción de encriptar y de desencriptar un fichero de texto. La encriptación (codificación) consiste en que dado un fichero de texto de entrada genere otro fichero de salida de extensión <nombre>.COD donde el texto estará codificado (encriptado). Esta codificación consiste en reemplazar cada carácter por el tercero siguiente (ej. a -> d). Si el carácter resultante es no imprimible éste no se cambia. La opción de desencriptado consiste en leer un fichero <nombre>.COD y recuperar la información original. 2.Escriba un programa que cree un fichero de texto llamado "PERSONAS.DAT" en el que se guarde la información de un número indeterminado de personas. La información que se guardará por cada persona será: Nombre: De 1 a 30 caracteres. Edad CARDINAL. Sexo CHAR (M/F). Arte CHAR (S/N). Deporte CHAR (S/N). Libros CHAR (S/N). MúsicaCHAR (S/N). La información correspondiente a cada persona se leerá del teclado. El proceso finalizará cuando se teclee un campo "Nombre" que esté vacío. 3.Amplíe el programa que procesa clientes de una agencia matrimonial para que tome los datos de todos los candidatos a estudiar del fichero PERSONAS.DAT del ejercicio anterior, lea el cliente del teclado y finalmente genere como resultado un listado por pantalla con los nombres de los candidatos aceptados y un fichero llamado ACEPTADOS.DAT con toda la información correspondiente a los candidatos aceptados. Una persona del fichero PERSONAS.DAT se considerará aceptable como candidato si tiene diferente sexo y por lo menos tres aficiones comunes con respecto al aspirante introducido por pantalla. (El programa debe ser capaz de trabajar con cualquier número de personas en el fichero PERSONAS.DAT). 4.Codifique un programa que cree un fichero para contener los datos relativos a los artículos de un almacén. Para cada artículo habrá de guardar la siguiente información: Código del artículo (Numérico) Nombre del artículo (Cadena de caracteres) Existencias actuales (Numérico) Precio (Numérico). Se deberán pedir datos de cada artículo por teclado hasta que como código se teclee 0. El fichero se habrá de crear ordenado por el código del articulo. 5.Escriba un programa que dados dos ficheros generados por el programa anterior y ordenados genere un tercer fichero que sea el resultado de mezclar de formar ordenada los dos primeros. 6.Escriba un programa que tome como entrada el fichero del ejercicio 4 y una condición sobre los campos Existencias actuales o Precio. La condición podrá ser: ___________________________________________________________________________ ! % <campo> [<,<=,>,>=, != ] <número> Este programa debe generar como salida un fichero llamado "salida.dat" que contenga todos aquellos artículos para los que se cumple la condición de entrada. 7.Escriba un programa que tenga como entrada un fichero que contenga un texto y dé como resultado una estadística de las palabras que hay de cada longitud, así como de los caracteres especiales (",", ".", ":", ";"). 8.Escribir un programa que genere a partir de un fichero de entrada que contiene una tabla de números reales otro fichero de salida <nombre>.BIN grabado en formato binario. Ej: 1.23 3.45 12.5 4.8 3.9 0.83 ........................ 9.Dado una tabla grabada en un fichero en formato binario <nombre>.BIN hallar la suma horizontal y vertical y grabar la tabla y los resultados en otro fichero de texto o binario según se introduzca por pantalla. El resultado en texto sería el siguiente: Ej: 1.23 3.45 12.5 17,18 4.8 3.9 0.83 9,53 6,03 7,35 13,33 ___________________________________________________________________________ ! &