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.