Disponible la nueva versión "donationware" 7.3 de OrganiZATOR
Descubre un nuevo concepto en el manejo de la información.
La mejor ayuda para sobrevivir en la moderna jungla de datos la tienes aquí.

Curso C++

[Home]  [Inicio]  [Índice]


4.3.7  Inicialización indirecta de Matrices

§1  Sinopsis:

En este epígrafe continuamos la exposición de la iniciación indirecta de matrices iniciada en el apartado anterior. En concreto, detallamos aquí el caso en que la definición no puede hacerse en una sola sentencia, lo que ocurre con las matrices que deben ser almacenadas en el montón, y cuyas dimensiones no se conocen en el momento de la declaración.

Hemos señalado que en estos casos, se define un puntero del tipo adecuado. Por ejemplo:

char** a;     // Ok. puede señalar a matriz de dos dimensiones!


A continuación hay que reservar el espacio de almacenamiento, y finalmente, inicializar los miembros de forma individualizada. Para esto se requiere un poco de práctica y conocer ciertos trucos (cuyas claves se han apuntado antes) para no perderse en el proceso:

  • El identificador de una matriz se describe como un puntero. En particular, el identificador de matrices bidimensionales cuyo espacio de almacenamiento se localice en el montón, se describe como puntero-a-puntero (lo mismo para n dimensiones 4.3.6).

  • Los elementos se almacenan por filas ( 4.3.6). Esta frase encierra el secreto de crear y destruir matrices multidimensionales sin necesidad de "verlas", dado que las de más de tres dimensiones tienen una difícil representación mental. Como veremos en los ejemplos, el proceso para crear una matriz de n dimensiones (x1, x2, ... xn) empieza por crear la primera fila; una matriz de una dimensión con x1 elementos.


§2   La creación de matrices de una dimensión ya se trató en el epígrafe dedicado al almacenamiento de matrices ( 4.3.3); el razonamiento puede hacerse extensivo a matrices de n dimensiones. Por ejemplo, consideremos declarar matrices de tres dimensiones:

void func() {
  int a1[2][4][3];           // L1: objeto de duración automática
  static int a2[2][4][3];    // L2: objeto persistente (estático)
}

En L1 tenemos un objeto a1 de duración automática alojado en la pila; en L2 tenemos un objeto estático a2 alojado en el segmento. Utilizaremos ahora el procedimiento de "C++ clásico" para crear una matriz ai de tres dimensiones en el montón. En primer lugar, recordaremos que desde ambas ópticas (puntero ↔  matriz), se tiene:


ai
: Puntero-a-puntero-a-puntero-a-int  ↔  matriz de una fila de dos elementos (la llamaremos bi), cada uno de los cuales es una matriz. Traducido a código:

int*** ai = (int***) calloc(2, sizeof(int**));  // I:

Traducido a lenguaje coloquial: ai es una variable automática  (4 bytes en la pila) de tipo puntero-a-puntero-a-puntero-a-int, señalando un espacio bi (8 bytes en el montón) capaz para albergar dos punteros-a-puntero-a-int.

La situación se ha esquematizado en la figura 1.


bi
:  Puntero-a-puntero-a-int  ↔  matriz de una fila de cuatro elementos, cada uno de ellos es una matriz (la llamaremos ci) de una dimensión. Traducido a código:

int** bi = (int**) calloc(4, sizeof(int*));    // II:

Coloquialmente: bi es una variable automática (4 bytes en la pila) de tipo puntero-a-puntero-a-int, señalando un espacio ci de 16 bytes en el montón, que es capaz para albergar 4 punteros-a-int.

ci:  Puntero-a-int  ↔  matriz de una fila de tres elementos, cada uno de los cuales: es un int. Traducido a código:

int* ci = (int*) calloc(3, sizeof(int));    // III:

En lenguaje coloquial:  ci es una variable automática (4 bytes en la pila) de tipo puntero-a-int, señalando un espacio ci de 12 bytes en el montón, capaz para albergar 3 int.


§2.1  Poniendo las sentencias anteriores en forma de una secuencia ejecutable, resultaría:

int*** ai = (int***) calloc(2, sizeof(int**));  // L1:
int i, j;
for (i = 0; i < 2; ++i) {                        // L2:
    ai[i] = (int**) calloc(4, sizeof(int*));
    for (j = 0; j < 3; ++j)                      // L3:
      ai[i][j] = (int*) calloc(3, sizeof(int));
}

Comentario:

El resultado de la ejecución de la sentencia L1 es el esquematizado en verde claro en la figura 2 (fase 1).

En este momento existe en la pila una variable automática ai tipo int*** que señala a un espacio bi de 8 bytes en el montón (suficiente para albergar 2 objetos tipo int**).  Los 4 bytes de cada mitad pueden ser referenciados mediante ai[0] y ai[1].

El bucle L2 realiza dos asignaciones muy parecidas a L1. Los 8 bytes del montón anteriores son ocupados con las direcciones de dos nuevos espacios ci (también en el montón) de 16 bytes cada uno (suficientes para albergar 4 objetos tipo int*). El resultado es el señalado en azul claro (fase 2).

Finalmente el bucle L3 crea cuatro espacios de 12 bytes cada uno (señalados en blanco), cuyas direcciones se asignan a los espacios ci anteriores (fase 3 dibujada a la derecha en la figura).

Estos espacios son capaces de albergar un total de 3 x 8 = 24 enteros. Justamente los 24 elementos de esta matriz tridimensional. Es interesante resaltar que cuando se designa un elemento, desde ai[0][0][0] hasta ai[1][3][2], el compilador proporciona automáticamente el valor almacenado en cada uno de los espacios. También que los elementos no tienen porqué ser contiguos.

Observe que el proceso de crear esta seudo-matriz de 24 enteros, ha supuesto la creación de los siguientes objetos:

  • En el montón:

    • 8 matrices de 3 int cada una (estas son las que contienen realmente la "carga útil" de la matriz).

    • 2 matrices de cuatro punteros cada una. Sus miembros contienen las direcciones de las anteriores. El tipo de cada miembro es int* (señala al primer miembro de una de las matrices anteriores).

    • 1 matriz de dos punteros. Sus miembros contienen las direcciones de las dos anteriores, por lo que el tipo de sus miembros es puntero-a-puntero-a-int (int**).

  • En la pila:

    • Un puntero que señala a un punto del montón (la matriz anterior). Realmente al primer miembro de esta matriz, por lo que su tipo es puntero-a-puntero-a-puntero-a-int (int***).

§3  Ejemplos

Como caso concreto, se muestra la definición indirecta de una matriz [2] de dos dimensiones matriz[m][n] en el montón. Se ha previsto para tres filas y cinco columnas matriz[3][5]; sus elementos son del tipo long double, pero puede ser adaptada fácilmente para aceptar entradas de usuario en tiempo de ejecución, de forma que se acepten otros tamaños y/u otros tipos.

Se presenta en dos versiones: la primera utiliza las funciones de librería clásicas de asignación y liberación de espacio (calloc y free). La segunda más moderna , utiliza las nuevas funciones de librería (new y delete). Finalmente, a título comparativo, se incluye una tercera versión que supone la definición directa utilizando el nuevo operador new[] para matrices .

§3.1  Versión C clásico

El proceso es análogo al esquematizado en la figura anterior aunque más sencillo (carece de la tercera fase), ya que la matriz a crear es de dos dimensiones. Consistirá en la creación de los siguientes objetos:

  • En el montón:

    • 3 matrices de 5 long double cada una, creadas en el bucle M4 (contienen los "datos" de la matriz).

    • 1 matriz 3 punteros (creada en M3). Cada uno de sus miembros señala al primer elemento de una de las matrices anteriores (la asignación se realiza en M5), por lo que son de tipo long double*.

  • En la pila:

    • Un puntero que señala al primer miembro de la matriz anterior, por lo que su tipo es puntero-a-puntero-a-long double (long double**).

 

#include <stdio.h>              // Versión C clásico
#include <stdlib.h>             // para free & calloc
typedef long double TIPO;       // L3:
typedef TIPO** OBJETO;
unsigned int fil = 3, col = 5;  // L5:
void des_asigna(OBJETO);        // L6: prototipo

int main(void) {                // ===============
  unsigned int i, j;
  OBJETO matriz;                // M2:
  matriz = (OBJETO) calloc(fil, sizeof(TIPO*));
  for (i = 0; i < fil; ++i)     // M4: Establecer columnas
    matriz[i] = (TIPO*) calloc(col, sizeof(TIPO));

  for (i = 0; i < fil; i++)     // M7: Iniciar elementos
    for (j = 0; j < col; j++)   // con valores arbitrarios
      matriz[i][j] = i + j;

  for (i = 0; i < fil; ++i) {   // M11: Mostrar elementos
    printf("\n");
    for (j = 0; j < col; ++j)
      printf("%5.2Lf", matriz[i][j]);
  }
  des_asigna(matriz);           // M16: Rehusar espacio
  return 0;
}

void des_asigna(OBJETO x) {     // liberar memoria
  unsigned int i;
  for (i = 0; i < fil; i++)     // F2:
    free(x[i]);                 // F3:
  free(x);                       // F4:
}

El programa produce la siguiente salida:

0.00 1.00 2.00 3.00 4.00
1.00 2.00 3.00 4.00 5.00
2.00 3.00 4.00 5.00 6.00

Comentario

El typedef de L3 establece el tipo de elementos que se guardarán en la matriz.  Actualmente son del tipo long double, 80 bits en Borland C++ ( 2.2.4), pero puede ser cambiado fácilmente.

El typedef de L4 establece una macro para definir un tipo de OBJETO que en realidad está definido como: OBJETO ↔  long double**.

La sentencia M2 declara un objeto automático matriz tipo OBJETO (long double**), puntero-a-puntero-a-long double.

M3 reserva espacio en memoria con calloc.  Este espacio es suficiente para albergar fil objetos de tipo TIPO* (long double*); tres punteros-a-long double (podemos considerar que este espacio es una matriz de tres elementos).

En este punto caben varias consideraciones adicionales:

El valor devuelto por calloc (puntero-a-void) es promovido a puntero-a-puntero-a-long double utilizando una promoción "casting" ( 4.9.9) de estilo tradicional C antes de asignarlos al puntero matriz.

En un programa para uso real, deberíamos incluir entre M3 y M5 una comprobación de que efectivamente calloc ha podido reservar la memoria solicitada.  Por ejemplo, incluyendo una sentencia que comprobara que matriz != NULL [1].

En M5 se reserva memoria para contener col objetos tipo TIPO (long double). El proceso se realiza 3 veces, una para cada fila. Al final del bucle M4 se han reservado 3 espacios para 5 long double cada uno. Las direcciones de inicio de estos bloques se asignan a cada miembro de la matriz creada en M3.

Podríamos considerar que M5 crea tres matrices de 5 elementos cada una. Este conjunto de 15 elementos constituiría la seudo-matriz m[3][5] cuyos miembros se inician con valores arbitrarios en el bucle M7.

El bucle M11 es análogo al anterior (un bucle de 5 iteraciones anidado en otro de 3), y se encarga de mostrar los valores asignados.

M16 invoca a la función encargada de rehusar el espacio previamente asignado en M3 y M5. Observe que el proceso se realiza en orden inverso al de asignación. El bucle F2 realiza la tarea inversa a la que se realizó en M4. F3 desasigna los espacios asignados en M5. A continuación F4 deshace la asignación realizada en M3.

Finalmente el objeto matriz y el resto de objetos automáticos serán destruidos en M17 al salir de main.

§3.2  Versión C++  tradicional

 


#include <iostream.h>             // Versión C++ moderno

  typedef long double TIPO;       // L.3:
  typedef TIPO** OBJETO;
  unsigned int fil = 3, col = 5;  // L.5:
  void des_asigna(OBJETO);        // L.6: prototipo

int main(void) {                  // ===============
  unsigned int i, j;
  OBJETO matriz;                  // M2:
  matriz = new TIPO* [fil];       // M3:
  for (i = 0; i < fil; ++i)       // M4:
    matriz[i] = new TIPO [col];   // M5:

  for (i = 0; i < fil; i++)       // M7: Inciar elementos
    for (j = 0; j < col; j++)     // con valores arbitrarios
      matriz[i][j] = i + j;

  for (i = 0; i < fil; ++i) {     // M11:  Mostrar elementos
    cout << "\n";
    for (j = 0; j < col; ++j)
      cout << matriz[i][j] << " ";
  }
  des_asigna(matriz);             // M16: Rehusar espacio
  return 0;
}

void des_asigna(OBJETO x) {       // liberar memoria
  unsigned int i;
  for (i = 0; i < fil; i++)       // F2:
    delete [] x[i];               // F3:
  delete [] x;                     // F4:
}

La salida:

0 1 2 3 4
1 2 3 4 5
2 3 4 5 6

Comentario:

El proceso es exactamente análogo al del ejemplo anterior. La única diferencia está en la zona de asignar el espacio. En este caso se utiliza el operador new[] ( 4.9.20c) que devuelve un puntero del tipo adecuado, por lo que no es necesario efectuar ningún "casting" explícito en las asignaciones M3 y M5.

  Una versión muy parecida de este mismo ejemplo en 4.9.20c.

§3.3  Versión C++ moderna:

 


#include <iostream.h>           // Versión C++ moderno

typedef long double TIPO;       // L.3:
typedef long double (*OBJETO)[5];
unsigned int fil = 3, col = 5;  // L.5:
void des_asigna(OBJETO);        // L.6: prototipo

int main(void) {                // ===============
  unsigned int i, j;
  OBJETO matriz;
  matriz = new TIPO [3][5];     // M3:

  for (i = 0; i < fil; i++)     // M7: Inciar elementos
  for (j = 0; j < col; j++)     // con valores arbitrarios
  matriz[i][j] = i + j;

  for (i = 0; i < fil; ++i) {   // M11: Mostrar elementos
    cout << "\n";
    for (j = 0; j < col; ++j)
    cout << matriz[i][j] << " ";
  }
  des_asigna(matriz);           // M16: Rehusar espacio
  return 0;
}

void des_asigna(OBJETO x) {     // liberar memoria
  delete [] x;                  // F4:
}

Comentario:

La salida es exactamente igual que las dos anteriores. La secuencia M3, M4 y M5 del primer ejemplo se han reducido a una sola sentencia. Este es un caso de definición directa en el que se crea una auténtica matriz como un objeto único, y con un espacio de almacenamiento definido y contiguo en el montón. Por contra, el puntero matriz sigue siendo un objeto automático.

Por lo demás, tanto la inicialización de sus miembros (en el bucle M7) como la secuencia de mostrarlos (bucle M11), son exactamente análogos a los casos anteriores.

La función destinada a liberar la memoria asignada ha quedado reducida a una sola sentencia; la invocación al operador delete[ ] para matrices acompañado del puntero adecuado. En realidad se podría haber suprimido totalmente esta función. Observe que no es necesario indicar delete[][], ya que el compilador ha añadido información suficiente para conocer que es una matriz bidimensional y el tamaño total que deberá ser rehusado ( 4.9.20c).

  Inicio.


[1]  Es el único caso en que puede utilizarse con propiedad el "infausto" nemónico NULL.

[2]  Hablando con propiedad, no se trata de una matriz, en el sentido de que los elementos no están almacenados de forma contigua y de que en realidad el objeto matriz no es identificado como tal por el compilador ( 4.3.3). Ver una introducción en la página anterior ( 4.3.6).