6 de octubre de 2016

Lectura de datos.

Lectura de datos.
   El Ejemplo 2.3 muestra un sencillo programa que lee desde la entrada estándar dos números enteros decimales (los operandos), y los suma. Este ejemplo también se describirá línea a línea.

   Las líneas 1-7 deberían resultar ahora  familiares, en base a la explicación de un primer programa en C. Por otro lado, la línea 8 presenta un nuevo elemento: la declaración de variables.

   La declaración de variables en C puede hacerse al iniciar un bloque ({ ... }) y tiene la siguiente estructura general:

          {
                tipo_de_dato lista de variables;
                             .
                             .
                             .
          }

donde tipo_de_dato es alguno de los tipos de datos de C, y la lista de variables es una lista de identificadores separada por comas. En base a lo anterior, la línea 8 está declarando tres variables de tipo entero a través de la palabra reservada int.

   Una palabra reservada es una palabra (token) que no puede ser utilizada como identificador y que es propia del lenguaje, lo cual quiere decir que ninguna variable o función (ningún identificador) por ejemplo, puede llamarse “int”.

   Por otro lado, un identificador es una secuencia de caracteres sin espacios que inicia con una letra. Los identificadores sirven para dar nombre a las variables que se declaran y que serán utilizadas dentro de un programa.

   En C existen diferentes tipos de datos: int, float y char son tres de ellos. La idea de tipo de dato se refiere a una categoría de datos pertenecientes a un dominio pero, desde el punto de vista del lenguaje de programación C ¿qué es un tipo de dato?

   Lo que una computadora almacena en su memoria principal no es otra cosa que una secuencia de bits; pues bien, esa misma secuencia de bits tiene diferentes interpretaciones, y la interpretación específica está en función del tipo de dato al que se refiera.

   Considere la siguiente secuencia de bits: 1 1 0 1 0 1 0 1. Suponga que dicho byte representa, como número, al entero decimal 213. Si esta misma secuencia de bits, se interpreta ahora como carácter (char) por ejemplo, podría representar a la hermosa letra 'R' (no es que así sea, es sólo una suposición), mientras que interpretada como un número con punto decimal (float) podría representar al 69.5.

   En resumen, un tipo de dato es la manera en que una computadora interpreta un patrón de bits.

   Continuando con el ejemplo que iniciamos, la línea 10 debería resultar familiar. A este tipo de sentencias se les conoce en el argot computacional como prompt, es decir, un mensaje de solicitud de datos.

   La línea 11 es nueva y muestra uno de los usos de la función scanf. La función scanf se compone básicamente de dos partes:

  1. El (los) especificador(es) de formato indicados entre comillas.
  2. Una lista de variables separadas por comas en donde se almacenarán los valores procesados (leídos) de la entrada estándar.

   El especificador de formato%d”, le indica a la función scanf que leerá un entero decimal, mientras que la expresión “&operando1”, especifica la variable en la que se almacenará el valor leído de la entrada estándar: operando1.

   Debe observarse que el nombre de la variable ha sido antecedido por el símbolo “&”, la justificación de ello se comprenderá mejor cuando se trate el tema de Apuntadores; por ahora, basta con que no olvide anteceder con dicho símbolo el nombre de la variable en la que se desee almacenar el valor procesado, ya que su omisión no es detectada por el compilador como un error, debido a que no es una violación a la gramática del lenguaje C.

   Las líneas 12 y 13 son análogas a las líneas 10 y 11 respectivamente. Asegúrese de comprender la similitud. Por otro lado, la línea 14 presenta la expresión:

suma = operando1 + operando2;

   Dicha expresión le indica a C que realice la operación aritmética de adición sobre los valores almacenados en operando1 y operando2, y que el resultado se asigne (almacene) en la variable suma.

   La tabla siguiente muestra los operadores aritméticos en C:

Operador      Descripción
                   /             División (cociente)
               %            Módulo (residuo)
                             *             Multiplicación (producto)
             +             Adición (suma)
                  -              Sustracción (resta)

   Los operadores se evalúan siguiendo una precedencia. La precedencia de operadores le indica al compilador cuáles de los operadores en una expresión deben ser evaluados primero. Considere la siguiente expresión:

3 + 5 * 4

   La expresión anterior es evaluada como 23 y no como 32, debido precisamente a que la multiplicación tiene una precedencia mayor sobre la adición, sin importar que la adición sea la operación que aparece primero (más a la izquierda).

   Si lo que se desea es primero realizar la suma y luego el producto, la expresión debe escribirse como:

(3 + 5) * 4

   Los paréntesis modifican la forma en que son evaluadas las expresiones, es decir, modifican la precedencia de cualquier operador y éste es su uso más común, aunque en muchas ocasiones se utilizan sólo por claridad en la forma de representar una expresión sin que modifiquen la precedencia de los operadores involucrados.

   Los operadores aritméticos “/”, “%” y “*” tienen la misma precedencia y es más alta que la de los operadores “+” y “-”. Si en una expresión existen operadores con la misma precedencia, la expresión es evaluada de izquierda a derecha.

   Finalmente, la línea 15 (la línea 17 se explicó a detalle en un primer programa en C ) introduce una novedad respecto al primer ejemplo, ya que muestra el uso de especificadores de formato dentro de la función printf.

   Al igual que en la función scanf, el especificador de formato “%d” le indica a la función printf que en la posición del especificador de formato va a imprimir, en la salida estándar, un entero decimal cuyo valor está almacenado en la variable suma, la cual se encuentra después de la coma. En este sentido, por cada especificador de formato debe existir su correspondiente variable asociada, de tal forma que tanto el número de especificadores de formato como el de variables, debe corresponder.

   Una posible salida de nuestro ejemplo se muestra en la siguiente figura. Asegúrese de comprender la descripción realizada hasta el momento, así como de entender lo que sucede con la ejecución y la salida correspondiente antes de continuar.

Una posible salida del Ejemplo 2.3.

   Considere ahora el Ejemplo 2.4 y compárese con el anterior. Note que todas las líneas son iguales excepto por la línea 15. La línea 15 contiene tres especificadores de formato “%d”. Cada especificador de formato, le indica a la función printf que imprima en orden, el entero decimal correspondiente almacenado en cada una de las variables correspondientes: operando1, operando2 y suma.

   La lógica del programa de este nuevo ejemplo no cambia en nada con respecto de la lógica del ejemplo anterior, sólo se ha cambiado el estilo en la forma de presentar los resultados, la cual es más ad hoc con la intención del programa.

   El segundo ejemplo  muestra la forma de incluir más de un especificador de formato en la función printf, de tal forma que puede observarse que por cada especificador de formato existe su correspondiente variable asociada. Es importante mencionar también que es responsabilidad del programador el asegurar que exista una correspondencia entre el especificador de formato y el tipo de dato de la variable, C no realiza esta verificación.

   Los especificadores de formato para printf tienen la siguiente forma general:

%[-]m.nx

donde % delimita el inicio del especificador de formato y x representa el especificador de formato a utilizar. El guión o signo de menos alinea el campo a la izquierda; si se omite, el campo se alinea a la derecha. Ahora bien, dependiendo del valor de x, los número enteros representados por m y n se interpretan de manera diferente:
  • Usualmente, m es la longitud mínima y n es la longitud máxima del campo (ancho de campo o espacio) que se utilizará para imprimir x.
  • Si x representa el especificador de formato para un número con punto decimal, n es interpretado como la precisión que deberá ser utilizada (número de decimales después del punto).
   Una posible salida para el Ejemplo 2.4 se muestra en la siguiente figura. Pruebe ambos ejemplos con distintos valores y observe sus resultados; repita el procedimiento hasta que se sienta cómodo y entienda por completo el mecanismo de funcionamiento de los programas.

Una posible salida del Ejemplo 2.4.

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.

2 de octubre de 2016

Dos aplicaciones de archivos de texto.

   Esta entrada presenta dos sencillas pero prácticas aplicaciones de los archivos de texto; recuerdo al lector que las posibilidades están limitadas únicamente a la imaginación del programador.

Implementación de cat.
   El comando cat (por concatenar) es un programa de Unix y GNU/Linux utilizado para concatenar y mostrar archivos en la salida estándar.

   El programa del Ejemplo 9.3 realiza una implementación básica del comando cat respecto a la presentación de archivos en la salida estándar, no en cuanto a la concatenación.

   La mayor parte de los elementos y sentencias utilizadas en el Ejemplo 9.3 ya han sido estudiados y presentados con anterioridad, por lo que debería comprenderse sin ningún problema. En este sentido, sólo se enfatizarán los siguientes aspectos:
  1. En caso de que no se proporcionen o se exceda el número de argumentos requeridos para su ejecución (líneas 11 y 12), se presenta una sentencia de uso del comando al estilo de los comandos de Unix y GNU/Linux.
  2. Se utiliza directamente la cadena almacenada en argv[1] para acceder al archivo indicado en la línea de comandos (línea 13).
  3. Se hace uso de la función fputs (línea 15) la cual imprime lo que se le envía como primer argumento, en el flujo especificado como su segundo argumento es decir: imprime en la salida estándar (stdout) la cadena "cadena", misma que fue leída por la función fgets en la línea 14.
   Pruebe el funcionamiento del programa del Ejemplo 9.3 y asegúrese de proporcionarle archivos de texto como argumento; puede probar con el código fuente del mismo programa por ejemplo.

Cambia caracteres.
   Suponga que se tiene la necesidad de cambiar cada ocurrencia (aparición) de un carácter dentro de un archivo por otro carácter distinto. Un escenario de solución para esto es realizar manualmente el cambio, otro posible escenario, es escribir un programa en C que lleve a cabo dicha tarea por nosotros; el Ejemplo 9.4 muestra un programa que satisface dicha necesidad.

   Al igual que antes, las sentencias del Ejemplo 9.4 deben resultar familiares al lector, por lo que sólo se resaltarán los siguientes puntos:
  1. La función main procesa los argumentos introducidos en la línea de comandos, y los utiliza para enviárselos a la función que realizará el cambio sobre el archivo: cambiaOcurrenciaArchivo.
  2. La función cambiaOcurrenciaArchivo recibe cuatro parámetros:
    1. El carácter a reemplazar v.
    2. El carácter n que substituirá a v.
    3. La cadena a1 que contiene la ruta y el nombre del archivo sobre el que se buscará a v.
    4. La cadena a2 que contiene la ruta y el nombre del archivo sobre el que se realizará el cambio de v por n.
  3. La constate simbólica TAM: en este tipo de uso, su ámbito se define de la línea 31 a la 49.
  4. La función cambiaOcurrenciaCadena, que se encarga de substituir el carácter v por el carácter n en la cadena cad utilizando notación de apuntadores.
  5. El uso de la función cambiaOcurrenciaCadena de la línea 44 para cambiar la cadena "cadena" leída del archivo archivoPtr1 (línea 43), en la cadena cadena que se almacenará en el archivo archivoPtr2 (línea 45) por medio de la función fputs (la cual trabaja de manera análoga a la función puts pero recibe como segundo parámetro, un apuntador al flujo sobre el que se desea escribir la cadena referida por su primer parámetro).

     Pruebe con distintos archivos de texto el programa del Ejemplo 9.4. La salida en pantalla es sólo informativa respecto a la terminación del programa, lo interesante ocurre en los archivos procesados, los cuales son proporcionados en la invocación del programa.