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.
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.
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.
-------------- 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 llamadasEn 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:
----------------- 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 llamadasAhora, 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.