CAPÍTULO 9: Punteros y Asignación Dinámica de Memoria

Hay momentos donde es conveniente asignar memoria en tiempo de ejecución mediante malloc(), calloc(), u otras funciones de asignación. Este enfoque permite aplazar la decisión del tamaño del bloque de memoria necesario para guardar un array en tiempo de ejecución, o también permite disponer de una sección de memoria para guardar un array de enteros en un momento dado, y después, cuando esa memoria no sea necesaria, podría ser liberada para otros usos, como el almacenamiento de un array de estructuras.

Cuando se asigna memoria, la función de asignación (como malloc(), calloc(), etc.) devuelve un puntero. El tipo de este puntero depende de si utilizas un compilador antiguo como K&R o un más nuevo de tipo ANSI. Con el compilador antiguo, el puntero devuelto es de tipo char, y con el compilador ANSI es void.

Si utilizas un compilador antiguo y quieres asignar memoria para un array de enteros, necesitarás realizar una conversión (cast) del puntero devuelto a un puntero entero. Por ejemplo, para asignar espacio para 10 enteros podemos escribir:

    int *iptr;
    iptr = (int *)malloc(10 * sizeof(int));
    if (iptr == NULL)

    { .. AQUÍ VA LA RUTINA DE ERROR .. }
Si utilizas un compilador ANSI, malloc() devuelve un puntero void y como un puntero void puede ser asignado a una variable puntero de cualquier tipo, no es necesaria la conversión a (int *) que hemos visto. La dimensión del array puede determinarse en tiempo de ejecución y no es necesaria en tiempo de compilación, esto es, el 10 que hemos visto podría ser una variable leída de un archivo o teclado, o calculada en tiempo de ejecución.

Como la notación de arrays y punteros es equivalente, una vez que iptr ha sido asignado como hemos visto, puede utilizarse la notación de arrays. Por ejemplo, podemos escribir:

    int k;
    for (k = 0; k < 10; k++)
       iptr[k] = 2;
para iniciar todos los elementos con el valor 2.

Incluso con una buena comprensión de punteros y arrays, es posible que un principiante en C tropiece primero con la asignación dinámica de arrays multidimensionales. En general y siempre que nos sea posible, nos gustaría acceder a los elementos de estos arrays mediante la notación de array en lugar de la de punteros. Dependiendo de la aplicación, podemos conocer o no ambas dimensiones en tiempo de compilación. A partir de aquí se abren varios caminos para seguir con la tarea.

Como hemos visto, cuando se asigna de forma dinámica un array de una dimensión, se puede determinar su dimensión en tiempo de ejecución. Ahora, cuando utilizamos asignación dinámica de memoria de arrays con más de una dimensión, no es necesario conocer la primera dimensión en tiempo de compilación. Dependiendo de cómo escribamos el código, necesitaremos conocer o no el resto de dimensiones. Aquí voy a mostrar varios métodos de asignación dinámica de espacio para arrays de dos dimensiones de enteros.

En primer lugar vamos a considerar los casos donde se conoce la segunda dimensión en tiempo de compilación.

MÉTODO 1:

Una forma de abordar el problema es utilizando typedef. Recordemos que para asignar un array de dos dimensiones de enteros podemos utilizar cualquiera de las dos notaciones siguientes, pues ambas generan el mismo código objeto:

    multi[row][col] = 1;     *(*(multi + row) + col) = 1;

También es cierto que las siguientes dos anotaciones generan el mismo código:

    multi[row]            *(multi + row)

La notación de la derecha se evalúa como un puntero, y la notación de la izquierda también se evalúa como un puntero. De hecho, multi[0] devuelve un puntero al primer entero de la primera fila (row), multi[1] devuelve un puntero al primer entero de la segunda fila, etc. En realidad multi[n] se evalúa como un puntero a un array de enteros que componen la fila n-ésima de nuestro array de dos dimensiones. Podemos pensar que multi es un array de arrays y multi[n] es un puntero que apunta al n-ésimo elemento de este array de arrays. Aquí la palabra puntero se utiliza para representar un valor de dirección. Aunque esto es bastante común, hay que poner especial cuidado al leer estas declaraciones para distinguir entre la dirección constante de un array y un puntero variable que es un objeto de datos en sí.

Veamos ahora el siguiente programa:


--------------- Programa 9.1 --------------------------------

/* Programa 9.1 de PTRTUT10.HTM  6/13/97 */

#include <stdio.h>
#include <stdlib.h>

#define COLS 5

typedef int RowArray[COLS];
RowArray *rptr;

int main(void)
{
    int nrows = 10;
    int row, col;
    rptr = malloc(nrows * COLS * sizeof(int));
    for (row = 0; row < nrows; row++)
    {
        for (col = 0; col < COLS; col++)
        {
            rptr[row][col] = 17;
        }
    }

    return 0;
}
------------- Fin del Prog. 9.1 --------------------------------

Aquí asumimos que utilizamos un compilador ANSI y por tanto no es necesaria la conversión del puntero void devuelto por malloc(). Si utilizas un compilador antiguo como K&R necesitarás realizar la conversión así:
    rptr = (RowArray *)malloc(.... etc.
Con este enfoque, rptr cumple con todas las características de un nombre de array (excepto que rptr se puede modificar), y se puede utilizar la notación de array en el resto del programa. Esto también significa que si quieres escribir una función para modificar los elementos de un array, debes utilizar COLS como parte del parámetro formal de esa función, tal y como lo hicimos cuando vimos cómo pasar un array de dos dimensiones a una función.

MÉTODO 2:

En el MÉTODO 1 anterior, rptr es un puntero de tipo "array de una dimensión de COLS enteros". También podemos utilizar otra sintáxis para este tipo:

    int (*xptr)[COLS];

la variable xptr tendrá las mismas características que la variable rptr del MÉTODO 1 anterior, sin necesidad de utilizar typedef. Aquí, xptr es un puntero a un array de enteros y el tamaño de este array viene dado por los COLS definidos. La ubicación de los paréntesis hace que prevalezca la notación de puntero aunque pensemos que la notación de array tiene mayor precedencia, es decir, si escribimos
    int *xptr[COLS];
estaríamos definiendo a xptr como un array de punteros con un número COLS de punteros. Como vemos, esto no es lo mismo que lo anterior. De cualquier modo, los arrays de punteros se utilizan en la asignación de memoria dinámica de arrays de dos dimensiones, como veremos en los dos siguientes métodos.

MÉTODO 3:

Supongamos que no conocemos el número de elementos en cada fila en tiempo de compilación, es decir, en tiempo de ejecución debemos determinar el número de filas y el número de columnas. Una forma de hacer esto es creando un array de punteros de tipo int, después asignar espacio para cada fila y hacer que esos punteros apunten a cada fila. Veámoslo en este programa:

-------------- Programa 9.2 ------------------------------------

/* Programa 9.2 de PTRTUT10.HTM   6/13/97 */

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int nrows = 5;     /* Tanto nrows como ncols deben asignarse */
    int ncols = 10;    /* o ser leídas en tiempo de ejecución */
    int row;
    int **rowptr;
    rowptr = malloc(nrows * sizeof(int *));
    if (rowptr == NULL)
    {
        puts("\nError asignando espacio para los punteros de filas.\n");
        exit(0);
    }

    printf("\n\n\nIndice   Puntero(hex)   Puntero(dec)   Dif.(dec)");

    for (row = 0; row < nrows; row++)
    {
        rowptr[row] = malloc(ncols * sizeof(int));
        if (rowptr[row] == NULL)
        {
            printf("\nError asignando memoria para row[%d]\n",row);
            exit(0);
        }
        printf("\n%d         %p         %d", row, rowptr[row], rowptr[row]);
        if (row > 0)
        printf("              %d",(int)(rowptr[row] - rowptr[row-1]));
    }

    return 0;
}

--------------- Fin 9.2 ------------------------------------

En el código anterior rowptr es un puntero que apunta al tipo int. En este caso apunta al primer elemento de un array de punteros de tipo int. Consideremos el número de llamadas a malloc():

    Para conseguir el array de punteros      1     llamada
    Para conseguir espacio para las filas    5     llamadas
                                          -----
                     Total                   6     llamadas
En este caso ten en cuenta que puedes utilizar la notación de arrays para acceder a los elementos de un array individualmente, por ej. rowptr[row][col] = 17;, pero esto no quiere decir que los datos del "array de dos dimensiones" estén ubicados de forma contigua en memoria.

De todas formas puedes utilizar la notación de arrays como si se tratara de un bloque contiguo de memoria. Por ejemplo, puedes escribir:

    rowptr[row][col] = 176;
como si rowptr fuera el nombre de un array de dos dimensiones creado en tiempo de compilación. Obviamente, tanto row como col deben figurar dentro de los límites del array creado, al igual que un array creado en tiempo de compilación.

Si lo que quieres es un bloque contiguo de memoria dedicado a almacenar los elementos de un array, puedes hacer lo siguiente:

MÉTODO 4:

En este método asignamos un bloque de memoria para almacenar primero a todo el array. Después creamos un array de punteros para que cada puntero apunte a cada fila del primer array. De esta forma, aunque se utilice el array de punteros, el array realmente está en un bloque contiguo de memoria. El código sería como el siguiente:
----------------- Programa 9.3 ----------------------------------

/* Programa 9.3 de PTRTUT10.HTM   6/13/97 */

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int **rptr;
    int *aptr;
    int *testptr;
    int k;
    int nrows = 5;     /* Tanto nrows como ncols deben asignarse */
    int ncols = 8;     /* o ser leídas en tiempo de ejecución */
    int row, col;

    /* ahora asignamos memoria para el array */

    aptr = malloc(nrows * ncols * sizeof(int));
    if (aptr == NULL)
    {
        puts("\nError al asignar memoria para el array");
        exit(0);
    }

    /* asignamos espacio para los punteros que apuntan a las filas */

    rptr = malloc(nrows * sizeof(int *));
    if (rptr == NULL)
    {
        puts("\nError asignado memoria para los punteros");
        exit(0);
    }

    /* y ahora hacemos que los punteros 'apunten' */

    for (k = 0; k < nrows; k++)
    {
        rptr[k] = aptr + (k * ncols);
    }

    /* Ahora vemos cómo se incrementan los punteros de las filas */
    printf("\n\nIncremento de los punteros de las filas");
    printf("\n\nIndice   Puntero(hex)  Dif.(dec)");

    for (row = 0; row < nrows; row++)
    {
        printf("\n%d         %p", row, rptr[row]);
        if (row > 0)
        printf("              %d",(rptr[row] - rptr[row-1]));
    }
    printf("\n\nY ahora mostramos el array\n");
    for (row = 0; row < nrows; row++)
    {
        for (col = 0; col < ncols; col++)
        {
            rptr[row][col] = row + col;
            printf("%d ", rptr[row][col]);
        }
        putchar('\n');
    }

    puts("\n");

    /* y aquí mostramos lo que realmente es: un array de
       2 dimensiones en un bloque contiguo de memoria. */
    printf("Demostracion de que el array esta en un bloque contiguo de memoria:\n");

    testptr = aptr;
    for (row = 0; row < nrows; row++)
    {
        for (col = 0; col < ncols; col++)
        {
            printf("%d ", *(testptr++));
        }
        putchar('\n');
    }

    return 0;
}

------------- Fin Programa 9.3 ----------------

Consideremos de nuevo el número de llamadas a malloc()
    Para conseguir el array en sí         1      llamada
    Para el espacio del array de ptrs     1      llamada
                                        ----
                         Total            2      llamadas
Ahora, cada llamada a malloc() crea espacio adicional puesto que malloc() normalmente se implementa mediante el sistema operativo en forma de lista enlazada conteniendo el tamaño del bloque. Pero es más importante todavía, con arrays grandes (varios cientos de filas), utilizar un registro para liberar espacio antes de que se haga más complicado hacerlo. Esto, junto a la contigüidad del bloque de datos que permite su inicialización a ceros utilizando memset() parece ser la segunda alternativa preferida.

Como ejemplo final de arrays multidimensionales veremos la asignación dinámica de un array de tres dimensiones. Este ejemplo nos enseña una cosa más cuando hacemos este tipo de asignación. Por las razones antes citadas utilizaremos el enfoque de la segunda alternativa. Consideremos el siguiente código:


------------------- Programa 9.4 ------------------------------------

/* Programa 9.4 de PTRTUT10.HTM   6/13/97 */

#include <stdio.h>
#include <stdlib.h>
#include <stddef.h>

int X_DIM=16;
int Y_DIM=5;
int Z_DIM=3;

int main(void)
{
    char *space;
    char ***Arr3D;
    int y, z;
    ptrdiff_t diff;

    /* primero asignamos espacio para el array en sí */

    space = malloc(X_DIM * Y_DIM * Z_DIM * sizeof(char));

    /* asignamos espacio para un array de punteros, cada
       puntero apuntará eventualmente al primer elemento de un
       array de 2 dimensiones de punteros a punteros */

    Arr3D = malloc(Z_DIM * sizeof(char **));

    /* y por cada uno de ellos asignamos un puntero a un nuevo
       array asignado de punteros que apunta a filas */

    for (z = 0; z < Z_DIM; z++)
    {
        Arr3D[z] = malloc(Y_DIM * sizeof(char *));

        /* y por cada espacio en este array ponemos un puntero 
           apuntando al primer elemento de cada fila en el espacio
           del array originalmente asignado */

        for (y = 0; y < Y_DIM; y++)
        {
            Arr3D[z][y] = space + (z*(X_DIM * Y_DIM) + y*X_DIM);
        }
    }

    /* Y ahora, comprobamos cada dirección en nuestro array 3D para 
       ver si la indexación del puntero Arr3d se realiza de forma
       contigua */

    for (z = 0; z < Z_DIM; z++)
    {
        printf("La ubicacion del array %d es %p\n", z, *Arr3D[z]);
        for ( y = 0; y < Y_DIM; y++)
        {
            printf("  Array %d y Fila %d comienzan en %p", z, y, Arr3D[z][y]);
            diff = Arr3D[z][y] - space;
            printf("    diff = %d  ",diff);
            printf(" z = %d  y = %d\n", z, y);
        }
    }
    return 0;
}

------------------- Fin del Programa 9.4 --------------------------

Si has seguido este tutorial hasta aquí, no tendrás problemas en descifrar el programa anterior leyendo sus comentarios. De todas formas hay que ver un par de puntos. Comencemos con esta línea:
    Arr3D[z][y] = space + (z*(X_DIM * Y_DIM) + y*X_DIM);
Fíjate aquí que space es un puntero de tipo caracter, y es el mismo tipo que Arr3D[z][y]. Es importante saber que cuando agregamos un entero, como el obtenido evaluando la expresión (z*(X_DIM * Y_DIM) + y*X_DIM), a un puntero, el resultado es un nuevo valor puntero. Y cuando asignamos valores puntero a variables puntero, el tipo de dato del valor y el tipo de dato de la variable tiene que ser el mismo.
Continuar con el Tutorial de Punteros

Tabla de Contenidos