17 de enero de 2017

Ejercicios selectos (archivos).

  1. Investigue para qué sirven los modos de apertura w+ y a+.
  2. Modifique el Ejemplo 9.1 en las líneas 47 y 48. Substituya archivoPtr por stdout y vea lo que sucede ¿Se comprueba lo descrito en el blog?
  3. Para el programa del Ejemplo 9.1, ¿qué sucede si en lugar de abrir el archivo en modo agregar (a) se abre en modo lectura (r) sin cambiar ninguna otra sentencia? Pruebe utilizando el archivo "contactos.txt" generado por el Ejemplo 9.1 y también sin dicho archivo (elimínelo).
  4. Para el programa del Ejemplo 9.1, ¿qué sucede si en lugar de abrir el archivo en modo agregar (a) se abre en modo escribir (w) sin cambiar ninguna otra sentencia? Pruebe utilizando el archivo "contactos.txt" generado por el Ejemplo 9.1 y también sin dicho archivo (elimínelo).
  5. Modifique la función llenaDirectorio del Ejemplo 9.1 para que en lugar de la función gets use la función fgets.
  6. Con base en el Ejemplo 9.1 y Ejemplo 9.2, escriba un programa que integre las dos funcionalidades respectivas. Agregue además la posibilidad, a través de funciones, de que los contactos sean ordenados y almacenados en otro archivo, que se puedan realizar búsquedas (lineal y binaria) con base en el nombre o el teléfono del contacto. No olvide trabajar con los datos en memoria, acceda a archivos de texto sólo para leer información o almacenarla. En este sentido, puede resultar útil guardar, como primera línea del archivo, el número de contactos almacenados, con lo que podría generar un arreglo utilizando asignación dinámica de memoria. La idea de este ejercicio es poner en práctica todo lo aprendido en el blog hasta ahora.
  7. Modifique el programa del Ejemplo 9.3 para que procese, y en consecuencia presente en la salida estándar, todos los archivos que se le proporcionen en la invocación.
  8. Escriba un programa que compare dos archivos y determine si dichos archivos son o no iguales. Los archivos procesados serán proporcionados desde la invocación del programa. Así, si su programa se llama compara, la ejecución: $./compara archivo1 archivo2 implica que se deben comparar archivo1 con archivo2. Si los archivos son iguales, se notifica al usuario de dicha situación; en caso contrario, se indicará la primera línea de cada archivo que no coincidió.
  9. Para el programa del Ejemplo 9.4, pruebe lo siguiente:
    1. Seleccione un archivo de texto que contenga la letra de una canción por ejemplo. Busque de preferencia la letra de una canción en inglés para omitir los detalles referentes a la letra ñ, los acentos, y la diéresis de nuestro lenguaje.
    2. Cambie la vocal a del archivo fuente (archivo1) por el símbolo # en el archivo destino (archivo2).
    3. Use el archivo generado en el paso anterior (archivo2) como archivo fuente para una nueva ejecución, en la que cambiará ahora el símbolo # por la vocal a, generándose un nuevo archivo destino: archivo3.
    4. Si todo está bien y suponiendo que en archivo1 no estaba el símbolo #, archivo1 debe ser idéntico a archivo3. Compruebe que así sea. Puede apoyarse de programas que comparan archivos, como el comando diff de Unix y GNU/Linux, y el programa compara del Ejercicio 7.
    10. Modifique el programa del Ejemplo 9.3 para que presente en la salida estándar, dos veces: una seguida de otra, el archivo procesado. La idea de este ejercicio es que pueda practicar el uso de la función rewind descrita brevemente en el blog.
    11. Escriba un programa que procese un archivo de texto proporcionado en la invocación del programa, y presente ordenadas en la salida estándar cada una de las líneas del archivo. Así, la ejecución de: $./ordena archivo presenta en la salida estándar cada una de las líneas de texto del archivo archivo, ordenadas alfabéticamente. Consulte el funcionamiento del programa sort de Unix y GNU/Linux para tener una mejor idea acerca del funcionamiento de su programa.
    12. Tomando como referencia el Ejemplo 9.4, escriba un programa que cambie una palabra por otra. Genere un nuevo archivo con los cambios, y tome en cuenta que deberá analizar tres casos:
  1. Si la palabra a cambiar tiene la misma longitud, el cambio es simple.
  2. Si la palabra a cambiar es de menor longitud, deberá recorrer los caracteres hacia la izquierda después de la nueva palabra para no dejar "basura" en el arreglo y en consecuencia en el nuevo archivo.
  3. Si la palabra a cambiar es de mayor longitud, deberá introducir "más espacio" en la cadena para no sobre escribir caracteres existentes. Analice detenidamente cómo resolver dicha situación.
        Nota: La idea de este ejercicio es poner en práctica lo aprendido en el
        blog. Tome en cuenta que los cambios se deben realizar directamente
        sobre el arreglo antes de escribirlo en el nuevo archivo.

    13. Pruebe lo que sucede si elimina la función getchar (línea 53) del Ejemplo 9.6.
    14. El programa del Ejemplo 9.6 está incompleto. Modifíquelo para completar las opciones de menú:
  • Eliminar contacto. Tome en consideración que la eliminación de un contacto requiere de la confirmación de la eliminación de dicho contacto antes de proceder.
  • Modificar contacto. La modificación del contacto puede ser en varios sentidos:
    • Conservar los datos del contacto pero moverlo de lugar, es decir, que sea ahora otro número de contacto, lo cual implica verificar que el nuevo número de contacto seleccionado no esté ya ocupado.
    • Modificar sólo el nombre del contacto.
    • Modificar únicamente el teléfono del contacto.
    • Modificar tanto el nombre como el teléfono del contacto.
    • No olvide también confirmar los cambios.
    15. En relación con en el Ejercicio 12 de la sección de ejercicios Abstracción de Datos, complemente la funcionalidad planteada en dicho ejercicio para que su programa sea capaz de leer figuras geométricas desde un archivo de texto especificado por el usuario.
    16. Escriba un programa que en base a lo siguiente: $ ./morse cadena archivo1 archivo2 permita leer el archivo de texto archivo1, de tal forma que si cadena es "a", su programa:
  • Procese únicamente símbolos alfanuméricos y espacios de archivo1 y los convierta a su correspondiente representación en código Morse en archivo2. Los espacios quedan sin cambio alguno. Cualquier otro símbolo es ignorado y descartado.
  • Cada símbolo representado en código Morse irá separado por comas en archivo2. Los espacios quedan sin cambio alguno.
  • Por otro lado, si cadena es "de", se deberá hacer el proceso inverso: convertir de Morse a texto (en efecto, se habrá perdido la redacción original pero la esencia del texto deberá ser la misma). Sugerencia: para el caso de Morse a texto, investigue la función strtok de la biblioteca string.h.
    17. En la entrada referente a Abstracción de Datos se desarrolló una biblioteca de funciones que inició con el Ejemplo 8.5. Posteriormente, en la sección de ejercicios, se fueron desarrollando nuevas funciones que fueron incorporadas a dicha biblioteca. Tomando en cuenta lo anterior, realice lo siguiente:
  • Genere una nueva biblioteca de funciones que incorpore gestión de archivos a todas las operaciones realizadas con matrices:
    • suma.
    • resta.
    • producto.
    • transpuesta.
    • esSimetrica.
    • inversa.
  • Presente un menú de opciones con las operaciones que es posible realizar con matrices. Agregue al menú la opción de Matriz Aleatoria la cual deberá realizar lo siguiente:
    • Solicitar las dimensiones del matriz: m (renglones) y n (columnas).
    • Solicitar un rango para el numerador (num) y un rango para el denominador (den).
    • Generar dinámicamente una matriz de m x n de números racionales aleatorios. El numerador y el denominador deberán estar en sus respectivos rangos.
  • Para cada operación se deberá permitir introducir la o las matrices (según sea el caso), desde la entrada estándar o desde un archivo de texto especificado por el usuario.
  • Para cada operación, excepto esSimetrica, se deberá permitir además de visualizar el resultado en la salida estándar, guardarlo en un archivo de texto especificado por el usuario.
    18. Modifique el Ejemplo 9.7 para que pueda procesar archivos de cualquier tamaño. Sugerencia: defina un buffer de un tamaño fijo, digamos 4 KB, y lea bloques de 4 KB en cada acceso al archivo. Tome en cuenta que el último bloque leído podría tener menos de 4 KB y que si el archivo es muy pequeño dicha situación podría presentarse desde el primer bloque leído, por lo que deberá verificar el número de bytes leídos por la función fread en cada lectura.
Tome como referencia el Ejemplo 9.7 para hacer una implementación del comando cp (copy) de GNU/Linux.
 

16 de enero de 2017

Archivos de texto.

   En C los archivos están representados por flujos (streams) de bytes. Por esta razón, cuando un archivo se abre, se dice que le es asociado un flujo.

   C no impone o establece algún tipo de estructura específica para los archivos, la estructura asociada a cada archivo es responsabilidad del programador.

Archivos de acceso secuencial.
Un archivo de acceso secuencial es habitualmente un archivo de texto en el que los datos se van almacenando uno detrás de otro sin tomar en consideración su tipo de dato. El acceso a los datos en este tipo de archivos se realiza normalmente de manera secuencial, es decir, no se accede de manera directa a algún dato específico dentro del archivo sin antes haber pasado por todos los datos anteriores a él.

   Los Ejemplos 9.1 y 9.2 muestran el uso de archivos de acceso secuencial para almacenamiento y lectura de datos respectivamente, y se describen en las secciones siguientes.

Almacenamiento de datos.
   En la entrada referente a Arreglos de Estructuras se trabajó con un programa que administra un directorio de contactos, en esta entrada se retomará el planteamiento utilizado en el Ejemplo 8.4 para almacenar los datos del directorio de contactos en un archivo.

   El Ejemplo 9.1 muestra la forma de utilizar un archivo de acceso secuencial para el almacenamiento de datos. La explicación del ejemplo se centrará en la función guardaDirectorio (líneas 38 - 53).

   La línea 39 declara una variable de tipo apuntador a FILE de nombre archivoPtr. FILE es una estructura (struct) que contiene información indispensable para el control del archivo o flujo incluyendo, entre otras cosas, un apuntador al buffer donde se almacenarán los datos que serán enviados o leídos a o del archivo respectivamente, el indicador de posición del archivo, indicadores de estado del archivo, etc.

   La función fopen (línea 43) abre el archivo especificado en la cadena de su primer argumento (la cadena puede incluir la ruta completa de acceso al archivo; si no se especifica una ruta, se busca el archivo en el directorio de trabajo actual), lo asocia con un flujo, y regresa un apuntador a la estructura FILE que representa dicho flujo, mismo que será utilizado en accesos posteriores a través de la variable archivoPtr.

   Las operaciones que se habilitarán sobre el flujo así como la forma en que éstas se llevan a cabo, son especificadas en la cadena del segundo argumento: el modo de apertura. La siguiente lista  muestra los modos básicos de apertura utilizados por la función fopen.
  • "r"      leer (read) : abre el archivo para lectura de datos. El archivo debe existir. El indicador de posición del archivo se establece al inicio.
  • "w"     escribir (write) : crea un archivo vacío para el almacenamiento de datos. Si el archivo existe, su contenido se descarta y el archivo es tratado como si fuera un nuevo archivo. El indicador de posición del archivo se establece al inicio.
  • "a"     agregar (append) : abre un archivo para el almacenamiento de datos al final del mismo respetando los que ya existen (si hubiera). Si el archivo no existe, se crea. El indicador de posición del archivo se establece al final para poder agregar los datos.
  • "r+"    leer/actualizar (read/update) : abre el archivo para actualización (lectura y escritura) de datos. El archivo debe existir. El indicador de posición del archivo se establece al inicio.
   Con los modos de apertura especificados en la lista anterior, el archivo es abierto y tratado como un archivo de texto. Para que el archivo pueda ser tratado como un archivo binario, se debe agregar el símbolo b (binary) a la cadena de modo de apertura, tal y como se muestra en la línea 15 del Ejemplo 9.7, la línea 20 del Ejemplo 9.5, y la línea 25 del Ejemplo 9.6, de los cuales se hablará más adelante.

   Continuando con el Ejemplo 9.1, note que la apertura del archivo ha sido solicitada en modo agregar y no en modo escribir; éste modo de apertura debe utilizarse con mucha precaución, ya que se pueden perder archivos completos debido a que se eliminan todos los datos de un archivo existente, aunque estos no hayan sido creados por un programa en C. Si la función fopen pudo abrir el archivo regresará, como ya se mencionó con anterioridad, una referencia al flujo que representa dicho archivo; en caso contrario, regresará un apuntador nulo, y ésta situación debe ser siempre verificada, como se muestra en la sentencia if de la línea 43.

   Una vez que el archivo ha sido abierto, almacenar datos en él es tan simple como mandar datos a la salida estándar. La función fprintf (líneas 47 y 48) es análoga a la función printf por lo que su uso debe ser más que natural en este momento; la única diferencia radica en su primer argumento, el cual es un apuntador al flujo que se utilizará para enviar los datos (si el primer argumento de la función fprintf es stdout, fprintf se comporta exactamente igual que printf. Note que el flujo al que se están enviando los datos es precisamente el apuntador a FILE archivoPtr, el cual es el medio de acceso para el archivo "contactos.txt" (vea nuevamente la línea 43).

   Por último, la función fclose (línea 50) cierra el archivo asociado al flujo representado por archivoPtr y elimina también dicha asociación. Una posible salida para el programa del Ejemplo 9.1 se muestra en la siguiente figura.

Una posible salida para el Ejemplo 9.1.

Lectura de datos.
   En la sección anterior se utilizó un archivo de acceso secuencial para almacenar datos en él, en esta sección se hará uso de dicho archivo para leer los datos almacenados en el mismo y mostrarlos en la salida estándar. En este sentido, el Ejemplo 9.2 es la parte complementaria del Ejemplo 9.1.

   Al igual que antes, la explicación estará limitada a la función leeDirectorio (líneas 28 - 40), la cual gestiona los aspectos referentes al manejo del archivo para su lectura.

   La función fopen abre el archivo "contactos.txt" (línea 32) en modo de lectura debido a que no se requiere modificar los datos del mismo, sino sólo procesarlos para su almacenamiento en un arreglo.

   Por otro lado, la función leeDirectorio muestra el uso de la función fgets en las líneas 35 y 36. La función fgets trabaja de manera análoga a la función gets, pero a diferencia de ésta última, fgets recibe tres parámetros:
  1. La cadena donde se almacenarán los datos que se procesen (representada en el Ejemplo 9.2 por el argumento contactos[i].nombre y contactos[i].telefono respectivamente).
  2. El número máximo de caracteres a almacenar en la cadena (representada en el Ejemplo 9.2 por el argumento TAM_NOM y TAM_TEL respectivamente). Esta característica resulta sumamente conveniente debido a que la función fgets protege de desbordamientos: aunque la cantidad de datos que se procesen sea mayor que el tamaño de la cadena, dichos datos son descartados y sólo se almacena como máximo, el número de datos especificado en este argumento menos uno, dado que se preserva el espacio para almacenar el fin de cadena ('\0').
  3. El flujo del cual se procesarán los datos (representado en el Ejemplo 9.2 por el argumento archivoPtr).
   La función fgets lee caracteres de un flujo y los almacena en la cadena correspondiente hasta que se hayan leído a lo más el número de caracteres indicado menos uno (para almacenar el fin de cadena '\0'), se encuentre el símbolo de fin de archivo (el símbolo de fin de archivo está representado por la constante simbólica EOF), o se encuentre el símbolo de avance de línea y retorno de carro ('\n').

   Tome en cuenta que aunque el símbolo '\n' hace que la función fgets termine la lectura, éste es considerado un carácter válido y en consecuencia, es también almacenado en la cadena.

   Ahora bien, si la función fgets encuentra el símbolo EOF antes que cualquier otro carácter, la función regresa un apuntador NULL, situación que es verificada en la línea 35 del Ejemplo 9.2. Se recomienda revisar un manual de referencia para tener una mejor y más amplia comprensión de la función fgets.

   Por último, note que la función leeDirectorio regresa el número (i) de contactos leídos del archivo (línea 39). Una posible salida para el Ejemplo 9.2 se muestra en la siguiente figura:

Una posible salida del Ejemplo 9.2.

6 de enero de 2017

Ejercicios selectos (abstracción de datos).

  1. Compare la estructura del Ejemplo 8.1 con la estructura general de un programa en C descrita en el Ejemplo 1.1.
  2. Con base en el programa del Ejemplo 8.1 y a lo expuesto en el blog en la entrada Abstracción en acción, complete las operaciones de suma y resta para las sentencias case de las líneas 27 y 29 respectivamente. Considere el caso de la siguiente operación o una similar: 1/2 - 1/2 ¿Qué pasaría?, ¿cómo manejaría esta situación? Sugerencia: Si el resultado de alguna operación genera un numerador con cero, no presente el resultado 0/4 por ejemplo, sino únicamente 0. Utilice una sentencia if/else en la presentación de datos.
  3. Reescriba la declaración de variables r1, r2 y res de las líneas 17 - 19 del Ejemplo 8.2 en una sola línea.
  4. Escriba una función con el siguiente prototipo void imprimeRacional(RACIONAL r);, la función deberá imprimir en la salida estándar el racional proporcionado (r) con las consideraciones hechas en el ejercicio 2. Tome ahora también en consideración, que si el resultado de un expresión genera algo como 4/4, 21/21, etc, se deberá imprimir 1 en su lugar.
  5. Considere la función leeExpresion del Ejemplo 8.2. La expresión de la línea 61: r1 -> q == 0 puede reescribirse con notación de apuntadores como: (*r1).q == 0. Reescriba, en su respectiva notación de apuntador en otro programa distinto, todas las expresiones que utilizan el operador flecha en la función leeExpresion y pruebe que dicho programa funciona exactamente igual.
  6. Reescriba la función hazOperacion del Ejemplo 8.2 para que reciba sus parámetros por referencia en lugar de parámetros por valor. No olvide cambiar también el operador punto por el operador flecha.
  7. Reescriba la función hazOperacion del Ejemplo 8.2 para que utilice funciones para realizar la suma, resta, producto y división de números racionales, utilice los siguientes prototipos: 
    1. RACIONAL suma(RACIONAL r1, RACIONAL r2);
    2. RACIONAL resta(RACIONAL r1, RACIONAL r2);
    3. RACIONAL producto(RACIONAL r1, RACIONAL r2);
    4. RACIONAL division(RACIONAL r1, RACIONAL r2);
    8. Escriba una función que dado un número racional, regrese si es posible, dicho número racional simplificado e incorpórela al Ejemplo 8.2. La función deberá tener el siguiente prototipo: RACIONAL simplifica(RACIONAL r);. Sugerencia: obtenga el Máximo Común Divisor de p y q  para simplificar su expresión; para ello, puede utilizar el Algoritmo de Euclides descrito en la sección correspondiente de la entrada Pseudo código.
    9. Reescriba el Ejemplo 8.2 para que utilice un arreglo de apuntadores a funciones en lugar de una estructura switch.
    10. Sean c1 y c2 dos números complejos definidos de la siguiente manera:
                                     c1 = (a + bi)c2 = (c + di)
donde a, b, c, d son números reales. Escriba un programa que permita representar números complejos así como sus respectivas operaciones aritméticas:
  • La suma de c1 y c2 se define como: c1 + c2 = (a + bi) + (c + di)  = (a + c) + (b + d)i.
  • La resta de c1 y c2 se define como: c1 - c2 = (a + bi) - (c + di)  = (a - c) + (b - d)i.
  • La multiplicación de c1 y c2 se define como: c1 * c2 = (a + bi) * (c + di)  = (ac - bd) + (ad + bc)i.
  • La división es un poco más elaborada debido a que se racionaliza el denominador; es decir, se multiplica el numerador y el denominador por el conjugado del denominador. El conjugado de un número complejo se obtiene cambiando el signo de su componente imaginaria, es decir: si c = a + bi  es un número complejo, su conjugado está dado por  c¹ = a - bi, y entonces:
    • c1 / c2 = (a + bi) / (c + di)  = [(a + bi) * (c - di)] / [(c + di) * (c - di)] = [(ac + bd) + (bc - ad)i] / (c² + d²).
    11. Modifique el Ejemplo 8.3 para que, siguiendo la idea planteada en las funciones leeTriangulo y leeVertice, defina primero una función imprimeVertice que, como su nombre lo indica, se encargue de imprimir el vértice que reciba como argumento en la salida estándar; para que entonces la función imprimeTriangulo haga los llamados correspondientes a ésta y se pueda imprimir en la salida estándar, el triángulo que se recibe como parámetro.
    12. Extienda el Ejemplo 8.3 para que realice lo siguiente (para este ejercicio, asuma que todos los vértices serán introducidos en el sentido de las manecillas del reloj):
          12.1. Presente un menú que permita al usuario trabajar con tres tipos de figuras geométricas: triángulos, rectángulos y cuadrados. Los rectángulos y los cuadrados serán ortogonales y sus lados paralelos a los ejes x y y respectivamente. El menú deberá tener el siguiente formato:
.
          12.2. Para el caso de los triángulos, considerar:
  • Si los vértices introducidos constituyen o no un triángulo. Si los vértices no conforman un triángulo se reporta dicha situación, y se repite la lectura de los mismos hasta que constituyan un triángulo.
  • Determinar el tipo de triángulo leído: isósceles, equilátero o escaleno.
          12.3. Represente, por medio de una estructura compuesta, un cuadrado, en cuyo caso deberá:
  • Determinar si los vértices introducidos constituyen o no un cuadrado. Si los vértices no conforman un cuadrado se reporta dicha situación, y se repite la lectura de los mismos hasta que constituyan un cuadrado.
  • Dado un punto en el espacio bidimensional (vértice), determinar si dicho punto está dentro, fuera, o sobre el último cuadrado introducido.
          12.4. Represente, por medio de una estructura compuesta, un rectángulo, en cuyo caso deberá:
  • Determinar si los vértices introducidos constituyen o no un rectángulo. Si los vértices no conforman un rectángulo se reporta dicha situación, y se repite la lectura de los mismos hasta que constituyan un rectángulo.
  • Dado un punto en el espacio bidimensional (vértice), determinar si dicho punto está dentro, fuera, o sobre el último rectángulo introducido.
    13. Reescriba el Ejemplo 8.4 para que tanto la lectura de datos de la entrada estándar, como la presentación de los mismos en la salida estándar se lleve a cabo por medio de funciones; para ello utilice los siguientes prototipos de funciones, donde contactos representa el directorio de contactos y n el número de contactos a leer o presentar respectivamente. Note que las expresiones contactos[ ] y *contactos son equivalentes:
 
void llenaDirectorio(CONTACTO contactos[ ], int n);
void imprimeDirectorio(CONTACTO *contactos, int n);

    14. Escriba una función que permita ordenar, en base al nombre, el directorio de contactos del Ejemplo 8.4. Utilice el ordenamiento por burbuja y el siguiente prototipo de función:

void ordena(CONTACTO *contactos, int n);

Sugerencia: investigue lo que hace y utilice la función strcmp de la biblioteca string.h para la comparación de los nombres de los contactos.
    15. Complete la biblioteca de funciones del Ejemplo 8.5 con la función simplifica realizada en el Ejercicio 8, de tal forma que los valores almacenados en las matrices estén siempre reducidos a su expresión mínima, es decir, que estén simplificados.
    16. Escriba un programa que defina y pruebe una función con el siguiente prototipo: MATRIZ_RACIONAL resta(MATRIZ_RACIONAL a, MATRIZ_RACIONAL b, int m, int n); La función deberá calcular la resta de la matriz a menos la matriz b de manera análoga a como lo hace la función suma del Ejemplo 8.5; tome en cuenta que necesitará de una función restaRacional análoga a la función sumaRacional.
Agregue la función resta del ejercicio anterior a la biblioteca de funciones del Ejemplo 8.5.
    17. Escriba un programa que defina y pruebe una función con el siguiente prototipo: void imprimeTranspuesta(MATRIZ_RACIONAL a, int m, int n);. La función deberá imprimir en la salida estándar, la matriz transpuesta de a. La matriz transpuesta se genera convirtiendo los renglones en columnas y viceversa.
Agregue la función imprimeTranspuesta del ejercicio anterior a la biblioteca de funciones del Ejemplo 8.5.
    18. Escriba un programa que defina y pruebe una función con el siguiente prototipo: MATRIZ_RACIONAL transpuesta(MATRIZ_RACIONAL a, int m, int n);. La función deberá regresar la matriz transpuesta de a. La matriz transpuesta se genera convirtiendo los renglones en columnas y viceversa.
Agregue la función transpuesta del ejercicio anterior a la biblioteca de funciones del Ejemplo 8.5.
    19. Escriba un programa que defina y pruebe una función con el siguiente prototipo: int esSimetrica(MATRIZ_RACIONAL a, int n);. La función deberá determinar si la matriz a es o no simétrica. Una matriz simétrica es aquella que es igual con su matriz transpuesta.
Agregue la función esSimetrica del ejercicio anterior a la biblioteca de funciones del Ejemplo 8.5.
    20. Escriba un programa que defina y pruebe una función con el siguiente prototipo: MATRIZ_RACIONAL producto(MATRIZ_RACIONAL a, MATRIZ_RACIONAL b, int m, int n, int l);. La función deberá calcular el producto de las matrices a y b regresando su resultado. Investigue cómo se define y obtiene el producto de matrices antes de intentar realizar este ejercicio.
Agregue la función producto del ejercicio anterior a la biblioteca de funciones del Ejemplo 8.5.
    21. Sea A una matriz cuadrada, i.e. con el mismo número de renglones que de columnas. La matriz inversa de A, denotada por A*, se define como: A x A* = A* x A = I donde I es la matriz identidad. Escriba un programa defina y pruebe una función con el siguiente prototipo: MATRIZ_RACIONAL inversa(MATRIZ_RACIONAL a, int n);. La función deberá calcular (si existe) la matriz inversa de la matriz a, tome en cuenta que si la matriz inversa de a no existe, la función deberá regresar NULL. Investigue cómo se calcula la matriz inversa de una matriz y utilice el método que más le convenga.
Sugerencia: utilice el método de Gauss-Jordan.
Agregue la función inversa del ejercicio anterior a la biblioteca de funciones del Ejemplo 8.5.
    22. Investigue y estudie el concepto de union en C. Las uniones (union) son parecidas en su estructura y manipulación a las estructuras (struct) pero son esencialmente diferentes, debido a que en las uniones, los elementos miembro que la conforman comparten su espacio de memoria, mientras que en las estructuras, cada elemento miembro tiene su propio espacio de memoria independiente.

5 de enero de 2017

Asignación dinámica de memoria.

Arreglos.
   En esta entrada se analizarán esencialmente dos ejemplos; el primero de ellos se muestra en el Ejemplo 7.6. En la línea 12 se define un arreglo de int de tamaño N de nombre estatico, y en la línea 13 un apuntador a int de nombre dinamico. Las líneas 16-18 llenan el arreglo estatico con números aleatorios entre 0 y 100.

   La parte medular del Ejemplo 7.6 ocurre en la línea 20. El argumento de malloc (la función malloc se encuentra definida en la biblioteca estándar de funciones stdlib.h (línea 6)) representa el tamaño del bloque o la cantidad en bytes de memoria solicitados. Si la memoria solicitada es concedida, malloc regresa un apuntador al inicio del bloque; en caso contrario, se regresa un apuntador nulo (NULL).

   Note que la línea 20 está solicitando la cantidad de memoria necesaria para almacenar N variables de tipo int (N * sizeof(int)), ya que el operador sizeof regresa el número de bytes que utiliza su argumento. Para este ejemplo, N es una constante, pero podría ser una variable. Note también que se está generando memoria para almacenar números enteros, pero podría pedirse para cualquiera de los tipos de datos de C, en general:

malloc( n * sizeof( tipo_de_dato ) );

donde n representa el número de datos (tamaño del arreglo) que se quiere.

   Si la función malloc tiene éxito, asignará a la variable dinamico la dirección de inicio del bloque de memoria solicitado, y dado que la función malloc regresa dicha dirección como un apuntador a void (void *) (un apuntador a void es un tipo genérico; puede interpretarlo como un tipo de dato comodín, y esto se debe a que la función malloc "no sabe" (y de hecho no le interesa) para qué será utilizada ni cómo será interpretada la memoria solicitada). En la línea 20 se hace también una conversión forzada (cast) al tipo de dato de la variable dinamico para su adecuada interpretación de ahí en adelante, para el caso del ejemplo: un apuntador a int que hará referencia a un arreglo de int, y en general:

tipo_de_dato * apuntador;

.  .  . 
apuntador = (tipo_de_dato *) malloc(n * sizeof(tipo_de_dato));

   La verificación de éxito por parte de la función malloc ocurre en la línea 22; en caso contrario (casi siempre la memoria solicitada por medio de malloc es concedida, pero debe tomar en cuenta que no tiene por qué ser siempre así, debido por ejemplo, a un exceso en la cantidad de memoria solicitada, o a que existen otros procesos de mayor prioridad solicitando también memoria, o a que no hay suficiente memoria para atender por el momento las solicitudes de malloc entre otras muchas situaciones), la situación se reporta (linea 33) y el programa termina, lo cual es habitual para los programas que trabajan con asignación dinámica de memoria.

   La representación de la memoria asignada por la función malloc para el Ejemplo 7.6 se muestra en la siguiente figura. Observe que la representación es un arreglo referido por el apuntador dinamico.

Representación del arreglo utilizando asignación dinámica de memoria del Ejemplo 7.6.
   Es de resaltar al lector que, una vez que la función malloc ha conseguido la memoria, ésta es accesible desde cualquier parte del programa y que desde el punto de vista de la funcionalidad, se comporta exactamente igual que un arreglo estático, por lo que es posible recorrerlo, modificarlo, pasarlo a funciones, etc.

   El ciclo for de la línea 23 copia el arreglo estatico (línea 12) en el arreglo dinamico generado en la línea 20 pero con los elementos invertidos. Observe que se usa la notación de arreglos para ambos arreglos y que entre comentarios aparece la notación con apuntadores y arreglos, de hecho:

dinamico[ j ] es equivalente a *(dinamico + j)

   La notación anterior hace uso de la aritmética de apuntadores. Note que no se está modificando la dirección de referencia, sino que a dicha dirección se le está sumando un desplazamiento determinado por j para acceder al elemento correspondiente y hacer la respectiva des referencia.

   Observe también que:

dinamico + j es distinto de dinamico = dinamico + j

   Asegúrese de comprender y entender la diferencia de la expresión anterior antes de continuar.

   Ahora bien, en base a lo expuesto en las notaciones anteriores, la notación de arreglo entonces suma un desplazamiento (determinado por el índice) a una dirección base (el apuntador).

   El ciclo for de la línea 25 recorre ambos arreglos para visualizar su contenido en la salida estándar. La línea 26 hace uso de la notación de arreglo para estatico y de la notación de apuntador para dinamico en la línea 28 en directa correspondencia a sus respectivas naturalezas.

   Por otro lado, las líneas 27 y 29 aparecen en comentario, pero muestran que es posible utilizar notación de apuntadores para un arreglo estático (declarado en la línea 12), y notación de arreglos para un arreglo que ha sido generado con asignación dinámica de memoria (línea 20), lo cual es muestra de la versatilidad y expresividad del lenguaje de programación C.

   La salida del Ejemplo 7.6 se presenta en la siguiente figura:

Salida del Ejemplo 7.6.
   Vale la pena mencionar adicionalmente dos cosas:
  1. El tamaño de un arreglo que se genera utilizando asignación dinámica de memoria a través de la función malloc estará limitado exclusivamente por la cantidad de memoria disponible de la computadora, y por las restricciones que establezca el sistema operativo anfitrión (sistema operativo en donde se ejecuten los programas).
  2. La memoria asignada por malloc proviene de un área especial denominada montículo (heap) y es accesible para todo el programa, es decir, no es local a ninguna función. Los mecanismos de asignación y de negociación de la función malloc con el sistema operativo son intrincados y quedan fuera de los alcances de este blog, basta por ahora con comprender que dicha función casi siempre nos proporciona memoria que se le solicita.

Matrices.
   El segundo ejemplo de esta entrada está compuesto por dos partes, la primera de ellas aparece en el Ejemplo 7.8, y la segunda parte la conforma su correspondiente biblioteca de funciones del Ejemplo 7.7.

   La parte medular de los ejemplos yace en la función creaMatriz (líneas 16-33) de la biblioteca de funciones, razón por la cual, los detalles de explicación inician ahí.

   Primero que nada, observe que la función creaMatriz regresa un doble apuntador a int (línea 16), que corresponde al tipo de dato de la variable matriz de la línea 17, la cual se utilizará como valor de retorno en la línea 32. Una variable doble apuntador es un apuntador que almacena direcciones de memoria de variables apuntador.

   La línea 20 genera un arreglo parecido al del Ejemplo 7.6 en la línea 20, con la diferencia de que el arreglo generado en la línea 20 es un arreglo de m apuntadores a int, y que el cast se realiza en función del tipo de dato de la variable matriz (int **) La siguiente figura muestra de forma vertical (para facilitar la explicación), el arreglo de apuntadores generado:

Representación del arreglo generado en el Ejemplo 7.7.
 
    La figura anterior debería ser suficiente para entender por qué matriz (línea 17) es un doble apuntador: el arreglo vertical es de apuntadores y por lo tanto, sólo puede ser referido por un apuntador que haga referencia a variables de tipo apuntador, es decir, un doble apuntador. De hecho matriz almacena la dirección en memoria del inicio del arreglo vertical, el cual (como ya se mencionó) es un arreglo de apuntadores que sólo puede ser referido por un apuntador a un apuntador, i.e. un doble apuntador.

   Ahora bien, si el arreglo de apuntadores es concedido (línea 21), para cada uno de los m apuntadores (línea 22) se genera un arreglo de n int (línea 24) (este sí es un arreglo como el de la línea 20 del Ejemplo 7.6); los arreglos generados en la línea 24 están representados de manera horizontal en la figura anterior. Note el uso de la notación de arreglos para la variable matriz en la línea 24, y la notación de apuntadores entre comentarios en la línea 25, ambas líneas son equivalentes.

   Para cada arreglo de enteros solicitado (línea 24) se debe verificar si éste ha sido otorgado o no (línea 26). En caso de que no, se procede a eliminar uno por uno los arreglos de int (en caso de que hayan sido creados los arreglos parciales de la matriz generados hasta ese momento) por medio de la función liberaMatriz.

   La función liberaMatriz de las líneas 7-14 recibe un doble apuntador a entero (la matriz a liberar), y el número de renglones de la matriz. La función libera cada uno de los elementos del arreglo de apuntadores (representados horizontalmente de la figura anterior). Por último, la función libera el arreglo de apuntadores (línea 13).

   Continuando con el ejemplo, la función leeMatriz de las líneas 35-46 es análoga a la versión realizada en el Ejemplo 6.7 de la entrada correspondiente a Arreglos Bidimensionales, la diferencia es que ahora matriz se declara con notación de apuntador y se recibe además un apuntador a char (c), el cuál es utilizado para imprimir el nombre de la matriz (%s de la línea 41) que se le envíe (líneas 21 y 22 del Ejemplo 7.8), lo cual hace más versátil e informativa la presentación y su correspondiente lectura de datos. Asegúrese de comparar ambas funciones y de comprender su equivalencia.

   Por otro lado, la función imprimeMatriz de las líneas 48-58, es también análoga a la versión realizada en el Ejemplo 6.7 en cuestión. Ahora matriz se declara con notación de apuntador y se ha agregado un cuarto parámetro: el apuntador a char c, el cuál es utilizado para imprimir el nombre de la matriz (%s de la línea 54) que se le envíe (líneas 24, 25 y 26 del Ejemplo 7.8), lo cual la dota también de mayor versatilidad. Al igual que antes, asegúrese también de comparar las funciones y de comprender su equivalencia.

   Por último en lo que compete al Ejemplo 7.7, la función suma de las líneas 60-69 es la versión que se deseaba en el Ejemplo 6.10. Note que la función regresa la matriz resultado c como un doble apuntador (líneas 60, 61, 64 (en esta línea es de hecho en donde se crea la matriz c valiéndose de la función creaMatriz. Note que la memoria regresada por la función malloc no es local a ninguna función y que ésta está disponible y accesible mientras no se haga una liberación explícita o termine el programa en donde se generó) y 68), y que recibe únicamente los operandos (matrices a y b) también como doble apuntador (línea 60).

   Pasando ahora al Ejemplo 7.8, lo primero que debe observar es la inclusión de la biblioteca personalizada de funciones del Ejemplo 7.7 en la línea 6.

   La línea 9 muestra tres dobles apuntadores a entero de nombres matA, matB y matC, mismos que representarán las matrices A, B y C respectivamente, mientras que las líneas 12-15 realizan una validación como la del Ejemplo 6.7.

   Por otro lado, en las líneas 17 y 18 se genera la memoria, a través de la función creaMatriz para las matrices matA y matB de m renglones y n columnas cada una. Si las matrices pudieron ser creadas (línea 20), se leen de la entrada estándar por medio de la función leeMatriz, matA (línea 21) y matB (línea 22).

   La línea 23 realiza la suma de las matrices matA y matB de m x n, y la matriz que regresa como resultado se asigna a matC.

   Por ultimo, las líneas 24, 25 y 26 imprimen en la salida estándar las matrices matA, matB y matC respectivamente, mientras que las líneas 27, 28 y 29 las liberan.

   La salida del Ejemplo 7.8 puede ser bastante extensa dependiendo de las dimensiones de las matrices, una posible salida se muestra en la siguiente figura:

Una posible salida del Ejemplo 7.8.
 
    Experimente con el programa de ejemplo, genere varias matrices y vea lo que sucede hasta tener un nivel de comprensión satisfactorio de todas las sentencias que lo componen.

   La idea aquí planteada se puede generalizar del modo siguiente:
  • Una variable doble apuntador es un apuntador que almacena direcciones de memoria de variables apuntador.
  • Una variable triple apuntador es un apuntador que almacena direcciones de memoria de variables doble apuntador.
  • Una variable n apuntador es un apuntador que almacena direcciones de memoria de variables n-1 apuntador.

   Finalmente, considere el Ejemplo 7.7.1 y compárese con el Ejemplo 7.7. Asegúrese de comprender las sutiles pero significativas diferencias que existen entre ellos, con las cuales podrá reforzar su aprendizaje y ahorrarse minutos, probablemente horas de angustia innecesarias, si por alguna razón tiene que hacer un uso intensivo de la memoria a través de las funciones malloc y free.


4 de enero de 2017

Ejercicios selectos (doble apuntador).

  1. Tomando como base el Ejemplo 7.7, pruebe que las líneas 24 y 25 son intercambiables y reescriba la línea 26 utilizando notación de apuntadores en lugar de la notación de arreglos.
  2. La línea 30 del programa del Ejemplo 7.8 está comentada, ¿qué sucedería si se descomenta dicha línea, se compila y ejecuta nuevamente el programa? Determine su respuesta y posteriormente compruébese.
  3. Modifique la biblioteca de funciones del Ejemplo 7.7 para que las funciones trabajen con valores de tipo double. Genere una nueva biblioteca de funciones que gestione matrices con dicho tipo de dato.
  4. Escriba un programa que defina y pruebe una función con el siguiente prototipo: double **resta(double **a, double **b, int m, int n). La función deberá calcular la resta de la matriz a menos la matriz b de manera análoga a como lo hace la función suma del Ejemplo 7.7.
  5. Agregue la función resta del ejercicio anterior a la biblioteca de funciones creada en el Ejercicio 3.
  6. Escriba un programa que defina y pruebe una función con el siguiente prototipo: void imprimeTranspuesta(double **a, int m, int n). La función deberá imprimir en la salida estándar la matriz transpuesta de a. La matriz transpuesta se genera convirtiendo los renglones en columnas y viceversa.
  7. Escriba un programa que defina y pruebe una función con el siguiente prototipo: double **transpuesta(double **a, int m, int n). La función deberá regresar la matriz transpuesta de a. La matriz transpuesta se genera convirtiendo los renglones en columnas y viceversa.
  8. Agregue la función transpuesta del ejercicio anterior a la biblioteca de funciones creada en el Ejercicio 3.
  9. Escriba un programa que defina y pruebe una función con el siguiente prototipo: int esSimetrica(double **a, int n). La función deberá determinar si la matriz a es o no simétrica. Una matriz simétrica es aquella que es igual con su matriz transpuesta.
  10. Agregue la función esSimetrica del ejercicio anterior a la biblioteca de funciones creada en el Ejercicio 3.
  11. Escriba un programa que defina y pruebe una función con el siguiente prototipo: double **producto(double **a, double **b, int m, int n, int l). La función deberá calcular el producto de las matrices a y b regresando su resultado. Investigue cómo se define y obtiene el producto de matrices antes de intentar realizar este ejercicio.
  12. Agregue la función producto del ejercicio anterior a la biblioteca de funciones creada en el Ejercicio 3.
  13. Considere el Triángulo de Pascal. Escriba un programa que, utilizando una matriz, calcule y presente el triángulo de Pascal. Sugerencia: llene inicialmente la matriz con 0's y al final imprima espacios para los ceros y los valores correspondientes para los coeficientes calculados.
  14. Sea A una matriz cuadrada, i.e. con el mismo número de renglones que de columnas. La matriz inversa de A, denotada por A*, se define como: A x A* = A* x A = I donde I es la matriz identidad. Escriba un programa defina y pruebe una función con el siguiente prototipo: double **inversa(double **a, int n). La función deberá calcular (si existe) la matriz inversa de la matriz a, tome en cuenta que si la matriz inversa de a no existe, la función deberá regresar NULL. Investigue cómo se calcula la matriz inversa de una matriz y utilice el método que más le convenga. Sugerencia: utilice el método de Gauss-Jordan.
  15. Agregue la función inversa del ejercicio anterior a la biblioteca de funciones creada en el Ejercicio 3.
  16. Escriba un programa que represente un laberinto por medio de una matriz, de tal forma que su programa:
    1. Permita generar con asignación dinámica de memoria una matriz cuadrada de tamaño n donde n > 2 . La matriz almacenará únicamente 0's y 1's, y se identificará como laberinto.
    2. Lea un entero que represente el porcentaje de obstáculos o de 1's que contendrá la matriz, de tal forma que si la matriz es de 10 x 10 por ejemplo y el porcentaje es 40, se deberán generar 40 1's de los 100 posibles valores a almacenar en la matriz. Determine el porcentaje con una división entera, es decir, sin considerar fracciones.
    3. Piense en una forma de distribuir los obstáculos sobre toda la matriz, tomando en consideración el porcentaje de 1's especificado en el punto anterior. Puede generar números aleatorios que representen los índices y colocar el obstáculo en esa posición siempre y cuando no exista ya uno ahí o corresponda a la entrada/salida del laberinto.
    4.  La siguiente figura muestra un laberinto de 5 x 5 con el 35% de obstáculos:
    5. Genere una sucesión de laberintos distintos en cada ejecución y asegúrese de que en la posición (0, 0) y (n - 1, n - 1) haya siempre 0's, ya que representan el inicio y la meta del laberinto respectivamente. Las reglas para caminar por el laberinto son las siguientes:
      1. Se intenta avanzar primero hacia la derecha, si se puede avanzar se continúa con este criterio, en otro caso, se prueba el siguiente criterio.
      2. Se intenta avanzar hacia abajo, si se puede avanzar se inicia recursivamente el intento de avance, si no se puede hacia abajo, se prueba el siguiente criterio.
      3. Se intenta avanzar hacia la izquierda, si se puede avanzar se inicia recursivamente el intento de avance, en caso de que no, se intenta con el siguiente criterio.
      4. Se intenta avanzar hacia arriba, si se puede, se inicia recursivamente el intento de avance hasta encontrar la meta (posición (n - 1, n - 1)).
    6. Su programa deberá encontrar, si existe, una solución al laberinto representado por la matriz laberinto haciendo uso de recursividad, donde los 0's representan caminos y los 1's obstáculos. Obviamente, los límites de la matriz representan también obstáculos. La solución estará indicada en formato de coordenadas de renglones y columnas, y por la naturaleza recursiva, la salida puede estar dada de la meta al inicio del laberinto. Para el laberinto representado en la figura anterior por ejemplo, la solución es: (4, 4) <= (4, 3) <= (4, 2) <= (3, 2) <= (2, 2) <= (1, 2) <= (1, 1) <= (0, 1) <= (0, 0).
    16. Con base en los Ejemplos 7.7 y 7.8, generalice las ideas expuestas en el blog para generar una estructura tridimensional como la de la entrada Arreglos de n dimensiones. La idea es que su programa permita leer valores de la entrada estándar para que sean asignados a su estructura tridimensional; así mismo, se requiere poder visualizar dichos valores en la salida estándar. Note que tendrá que hacer uso de un triple apuntador.
    17. Usando como referencia el Ejemplo 7.9 para procesar los argumentos en la invocación de programas, escriba un programa que procese entradas como las siguientes:
  •       $cambiaBase 101010101010 2 5
  •       $cambiaBase 1a4DEA 15 4
  •       $cambiaBase Fea 16 12
  •       $cambiaBase 12343102 5
   En general:   $cambiaBase Nb1 b1 b2, donde Nb1 es un número en base b1 y b2 representa la base a la que se convertirá Nb1 por medio de su programa. Su programa deberá también:
Verificar que Nb1 represente un número válido en la base b1, en caso de que no, deberá indicar dicha situación e identificar el primer dígito inválido:



Verificar que b1, b2 pertenezcan al conjunto {2, 3, . . ., 16}, ya que cualquier otra base se procesará e indicará como incorrecta.

Considerar que si b2 no se especifica, entonces Nb1 se convertirá a base 10 por omisión de b2.

Procesar mayúsculas o minúsculas en Nb1 para las bases que contienen letras como parte de sus "dígitos".

    18. Escriba un programa que permita realizar las cuatro operaciones aritméticas básicas de suma, resta, multiplicación y división para dos números de cualquier cantidad de “dígitos” representados en la misma base b, donde b pertenece al conjunto {2, 3, . . ., 16}.

    Tome en cuenta que ninguno de los tipos de datos de C tiene la capacidad solicitada, por lo que su programa deberá valerse de cadenas para la representación de dichos números, y realizar la implementación de las operaciones aritméticas tomando en cuenta dicha representación así como la manipulación simbólica de los "dígitos". En este sentido, es de esperarse que el resultado esté dado también por medio de una cadena, pero esto será transparente para el usuario.

     El programa no debe convertir los números a base 10, sino realizar las operaciones en la misma base en la que se expresan. Al igual que en el ejercicio anterior, su programa deberá:
  • Procesar y obtener antes que nada la base b de los operandos.
  • Presentar un menú con las opciones de operaciones aritméticas al estilo del Ejemplo 7.13, y utilizar un arreglo de apuntadores a funciones para invocar a la operación correspondiente de suma, resta, multiplicación o división.
  • Verificar que b pertenezcan al conjunto {2, 3, . . ., 16}, ya que cualquier otra base se procesará e indicará como incorrecta.
  • Verificar que los operandos representen un número válido en la base b, en caso de que no, deberá indicar dicha situación e identificar el primer dígito inválido.
  • Procesar mayúsculas o minúsculas en los operandos para las bases que contienen letras como parte de sus "dígitos".
Sugerencias:
  • Analice y resuelva primero el caso para la base 10, con la finalidad de poder generalizar su solución hacia las demás bases.
  • Resuelva primero las operaciones de suma y resta, y las de multiplicación y división resuélvalas con sumas y restas sucesivas respectivamente.