4 de octubre de 2016

Archivos binarios.


   Aunque C no impone una estructura a los archivos, es posible definir una estructura específica para éstos. Sin embargo, la creación, administración y el acceso a dicha estructura, son responsabilidad del programador.

   Un archivo binario es un archivo con una estructura específica, la cual no puede ser visualizada ni interpretada de manera directa, sino únicamente como un conjunto de bytes relacionados entre sí, los cuales representan los tipos de datos que fueron almacenados en la estructura del archivo.

Archivos de acceso aleatorio.
   La creación de una estructura determinada sobre un archivo tiene, como casi todo en la vida, ventajas y desventajas.

   La principal ventaja es que es posible acceder a un elemento específico dentro del archivo sin la necesidad de procesar todos los elementos anteriores a él, como en el caso de los archivos de texto de acceso secuencial.

   Por otro lado, la principal desventaja es que, antes de poder acceder a los datos del archivo, se debe crear la estructura correspondiente y por lo tanto, es preciso definir desde la creación del archivo, el número de elementos que almacenará.

   Como analogía, puede decirse que los archivos de acceso aleatorio son a las unidades de almacenamiento (discos), lo que los arreglos son a la memoria principal; de hecho, note que tanto la ventaja como la desventaja mencionadas con anterioridad se tienen también presentes en los arreglos.

   En resumen, un archivo de acceso aleatorio es un archivo binario con una estructura específica determinada por el programador, al que se pueden acceder sus elementos de manera aleatoria, de manera análoga a como se acceden los elementos en un arreglo.

Creación de la estructura del archivo.
   El primer paso para la manipulación de archivos de acceso aleatorio es la creación de la estructura del archivo.

   La creación de la estructura consiste básicamente en definir cuáles y de qué tipo de dato serán los elementos almacenados en el archivo. Lo anterior se realiza, habitualmente, encapsulando dentro de una estructura los elementos a almacenar en el archivo; sin embargo, es posible almacenar elementos de un solo tipo de datos en el archivo sin necesidad de representarlos dentro de una estructura.

   El Ejemplo 9.5 muestra la creación de la estructura de un archivo que contendrá un directorio de contactos. Esta idea ha sido ya utilizada en la entrada referente a Archivos de Texto, aquí se retoma para mostrar ahora la representación del directorio de contactos en un archivo de acceso aleatorio.

   Note que la estructura (struct) de las líneas 9-13 ha sido ligeramente modificada respecto de las anteriores, ya que se le ha agregado el elemento miembro num (línea 10), el cual servirá como índice para localizar a una estructura específica dentro del archivo.

   La línea 16 define la variable contacto de tipo CONTACTO misma que se inicializa con el valor cero, y con las cadenas vacías para num, nombre y telefono respectivamente.

   Como elemento clave del Ejemplo 9.5, observe que el modo de apertura seleccionado para el archivo "contactos.dat" en la línea 20 es "wb", el cual especifica que se debe crear un archivo binario en modo de escritura.

   Ahora bien, el ciclo for (línea 23) escribe N veces en el archivo referido por archivoPtr, sizeof(CONTACTO) bytes almacenados en la estructura contacto a través de la función fwrite (línea 24). El número 1 (tercer argumento) de la función fwrite le indica a la función cuántos elementos del tamaño especificado como segundo argumento (el tamaño se especifica en número de bytes) obtendrá de la dirección especificada como primer argumento, para escribirlos en el flujo proporcionado como cuarto argumento. Éste es el uso más común de la función fwrite.

   Por otro lado, si el tercer argumento n proporcionado a la función fwrite es mayor que uno, entonces el primer argumento debe corresponder al nombre de un arreglo del cual se obtendrán los n elementos. El número t de bytes de cada uno de los elementos del arreglo se proporciona como segundo argumento, y el flujo donde se desea que se escriban los t bytes se proporciona como cuarto argumento.

   Resumiendo: la ejecución del programa del Ejemplo 9.5 crea el archivo contactos.dat, cuya estructura está conformada por N elementos (línea 5) de tipo CONTACTO, es decir, genera una especie de arreglo de N elementos de tipo CONTACTO en el archivo contactos.dat.

Acceso aleatorio a la estructura del archivo.
   El programa del Ejemplo 9.6 muestra los pormenores respecto al manejo de archivos binarios de acceso aleatorio. Tome en cuenta que este ejemplo se basa en la estructura de archivo creada en el Ejemplo 9.5 descrito en la sección anterior.

   Las funciones menu, leeContacto e imprimeContacto se dejan como material de análisis y comprensión para el lector. Todos los detalles de dichas funciones deberían ser claramente comprendidos; asegúrese de que así sea antes de continuar.

   Respecto a la función main sólo se harán dos observaciones, todo lo demás debe resultar familiar:

  1. La línea 25 muestra el modo de apertura rb+, lo cual le indica a la función fopen que abra el archivo binario contactos.dat para actualización.
  2. A diferencia de todos los ejemplos anteriores, el archivo contactos.dat es abierto al iniciar el programa, y permanece así hasta su terminación (línea 40). Observe cómo la variable archivoPtr es enviada como argumento a las funciones que trabajan con el archivo (líneas 28 y 31).
   Teniendo en cuenta lo anterior, la atención del Ejemplo 9.6 se centrará entonces en dos funciones:
  1. leeDirectorio: es la encargada de leer los datos del archivo por medio de la función fread (línea 74). Mientras fread pueda leer datos del archivo, regresará el número total de elementos exitosamente procesados, de tal forma que cuando se llegue al fin de archivo y la función no pueda leer más datos, regresará cero. La función fread trabaja de manera semejante pero en el sentido contrario a la función fwrite explicada en el Ejemplo 9.5. Si el elemento miembro num de algún contacto es distinto de cero (línea 75), se imprimen sus datos y se contabiliza. Aquí el valor cero indica que es un contacto en blanco, dado que así se inicializó la estructura del archivo (Ejemplo 9.5), cualquier otro caso hace referencia a un número de contacto con datos ya almacenados previamente en el archivo.
  2. agregaContacto: Esta función se desglosará en varias partes por orden de secuencia:
    1. Solicita y valida un número de contacto (líneas 49 - 52). Recuerde que en el Ejemplo 9.5 se generó una estructura en el archivo para almacenar 100 contactos.
    2. La función getchar (línea 53) absorbe el '\n' introducido en la entrada estándar después del número leído en la línea 51; el número proporcionado se almacena en num, pero si el '\n' no es procesado, será el elemento del que disponga la siguiente lectura de datos y si ésta es de una cadena dará la impresión de leer una cadena vacía.
    3. La función fseek (líneas 55 y 63) establece el indicador de posición del archivo archivoPtr en una determinada posición, la cual es proporcionada como segundo argumento en la forma de un desplazamiento en bytes. El tercer argumento le indica a la función la posición utilizada como referencia para el desplazamiento:
      1. SEEK_SET: del inicio del archivo.
      2. SEEK_CUR: de la posición actual del indicador de posición del archivo. Note que la expresión de desplazamiento siguiente, realiza el cálculo del elemento específico dentro del archivo, lo cual, respecto a la analogía planteada con anterioridad, es equivalente al índice de un arreglo: 
      (num - 1) * sizeof(CONTACTO)
    4. La combinación de las funciones fseek y fread (líneas 55 y 56) y de fseek y fwrite (líneas 63 y 64) realizan la parte medular respecto a la inserción de un nuevo contacto en el archivo.
    5. Note la necesidad de volver a calcular la posición del elemento a insertar en el archivo (línea 63), debido a que la lectura del contacto (línea 56) deja el indicador de posición del archivo, en el elemento siguiente al de interés.
   La salida del Ejemplo 9.6 puede ser bastante extensa; la invitación es hacia compilar el programa y probarlo, así como a entender la relación del funcionamiento de cada una de la sentencias que componen el programa hasta obtener una total comprensión de él.

Archivos con cualquier contenido.
   El Ejemplo 9.7 abre un archivo binario independientemente de su contenido, formato o estructura, lee su contenido y presenta, en la salida estándar, los bytes que lo conforman. La mayoría de los elementos del programa han sido ya analizados o mencionados con anterioridad, por lo que su comprensión debería ser sencilla; sin embargo, se enfatizarán algunos aspectos relevantes:
  1. El programa procesa datos de la línea de comandos, es decir, recibe el archivo a procesar como argumento en la invocación del programa (líneas 8 y 11 - 16).
  2. Note que la función rewind (línea 24) establece el indicador de posición nuevamente al inicio del archivo referido por archivoPtr, debido a que en la línea 21 se lee el archivo byte por byte para poder determinar su tamaño a través de la función ftell (línea 23). La función ftell regresa el indicador de posición actual del flujo asociado al archivo archivoPtr. Si el archivo al que se hace referencia es binario, dicho indicador corresponde al número de bytes respecto del inicio del archivo.
  3. Observe cómo la función fread lee el archivo completo en la línea 33; y que el número de bytes leídos es almacenado en tamanio2 para ser posteriormente comparado (línea 34) con el número de bytes que se le especificó a la función fread que leyera.
  4. Finalmente, note en la línea 42 el uso del especificador de formato de salida "%x", el cual le indica a la función printf que imprima el dato almacenado en buffer[i] en formato hexadecimal (minúsculas); mientras que el especificador de formato de salida "%u" (línea 43) se utiliza para imprimir un entero sin signo.
   Pruebe el programa del Ejemplo 9.7 con distintos archivos binarios; puede probarlo con archivos ejecutables, de música, de video, de imágenes, e incluso de texto.

   Por último, tome en cuenta que el Ejemplo 9.7 fue diseñado para contener en memoria todos los bytes del archivo procesado, por lo que si el archivo es muy grande, la solicitud de una gran cantidad de memoria podría ser rechazada. Se deja como ejercicio para el lector la oportunidad de corregir dicha deficiencia leyendo el archivo por bloques.