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.3 Almacenamiento de matrices

§1 Sinopsis

Puesto que los registros no son adecuados para el albergarlas, las matrices pueden alojarse en el segmento, el montón o la pila ( 2.2.6). El sitio concreto depende de las características de la matriz como variable, es decir, que sea estática o dinámica, y dentro de estas últimas que sea de naturaleza automática (almacenada en la pila) o persistente (almacenada en el montón o en el segmento).


§2  En general las matrices se declaran directamente mediante una expresión del tipo:

int m1[10];             // matriz de 10 enteros
char m2[10];            // matriz de 10 char
double m3[10];          // matriz de 10 doubles
int* m4[10];            // matriz de 10 punteros a entero
void (* m5[10])(char);  // matriz de 10 punteros a función


En estos casos, aunque no se trata propiamente de definiciones ( 4.1.2), el compilador tiene suficiente información para deducir el espacio necesario según el tipo y número de elementos (dimensión) de la matriz, de forma que puede reservar una zona de memoria contigua. En el caso anterior se reservarían los siguientes espacios ( 2.2.4):

m1 → 10 * 32 == 320  bits

m2 → 10 * 8   ==  80 bits

m3 → 10 * 64 == 640 bits

m4 → 10 * 32 == 320 bits

m5 → 10 * 32 == 320 bits


En la figura 1 se muestra un esquema de una de estas zonas, que es accedida a través del identificador, por ejemplo, m1, que hemos visto ( 4.3.2) que representa un puntero al comienzo. O mediante expresiones de subíndices, como m1[i], que son traducidos por el compilador a una dirección teniendo en cuenta la del primer elemento y el desplazamiento adicional adecuado:

m1 == &m1[0]

m1[i] == *(m1+i)


El resultado de declaraciones como las anteriores (§2 ) es una variable del tipo matriz de n elementos de tipoX con un Lvalue ( 2.1.5). Salvo indicación en contrario, o que la declaración esté fuera de cualquier bloque o función, será un objeto de tipo automático alojado en la pila.

§3 Creación de matrices persistentes

Cuando se desea crear una matriz persistente, el procedimiento anterior no es adecuado. Para crear una matriz de naturaleza no automática caben dos opciones:

  • Declararla estática, con lo que el compilador se encarga de crearla en el segmento de datos.
  • Crearla en el montón, ya que estos objetos también tienen carácter persistente. Como veremos a continuación (§4 ), esta alternativa puede realizarse de dos formas.

Resumimos la situación general mediante un sencillo ejemplo:

#include <iostream.h>
int m1[3] = {1,2,3};           // L.2:
void func();
 
int main () {                  // ========
  func();
}
 
void func() {
  static int m2[3] = {3,4,5};    // L.8:
  static int* m3 = new int[3];   // L.9:
  m3[0] = 1; m3[1] = 2; m3[2] = 3;
  int m4[3] = {1,2,3};
}

Comentario:

La matriz m1 es de naturaleza estática, ya que es global al fichero ( 2.2.6). La especificación de tipo de almacenamiento está implícita en la propia situación de la declaración en L.2, fuera de cualquier función.

La matriz m2 es estática por la utilización explícita del especificador static en L.8; el compilador se encarga de reservar espacio adecuado en el segmento de datos. La matriz conserva sus valores entre posibles invocaciones sucesivas a func.

m3 es una alternativa a la anterior. Se declara un puntero-a-int de naturaleza estática; este puntero es almacenado en el segmento, por lo que conservará su valor entre llamadas sucesivas a la función. A su vez este puntero señala a un espacio en el montón; espacio que ha sido reservado mediante el operador new, así que los valores almacenados serán también persistentes.

Nota: el ejemplo es méramente didáctico y no sería operativo. Existe una diferencia adicional importante entre las opciones L.8/L.9. En L.8 el espacio es reservado una sola vez por el compilador, y es iniciado una sola vez por el módulo de inicio. En cambio, la opción L.9 reservaría un nuevo espacio con cada sucesiva invocación a la función.

La matriz m4 es de naturaleza automática, se creará en la pila y será iniciada y destruida con cada invocación a func.


§4  Dejando aparte la declaración estática en sus dos versiones, implícita (L.2) o explícita (L.8), para crear una matriz en el montón existen dos procedimientos, según el tipo de funciones utilizadas. El que denominaremos "clásico", que utiliza las funciones calloc / free [1] y el "moderno" que utilizaría los operadores new / delete respectivamente. En ambos casos la referencia al elemento creado se realiza mediante un puntero.


§4.1  Para crear la matriz int m1[10] en el montón según el sistema moderno, se utilizaría la expresión:

int* m1;
m1 = new int[10];

La consecuencia de estas sentencias es que se declara un puntero-a-int denominado m1; esta variable se crea en la pila (es automática), y a este Lvalue se le asigna el valor devuelto por el operador new ( 4.9.20) que es la dirección de un espacio de memoria reservado en el montón.


El resultado sería la situación de memoria esquematizada en la figura 3, con la diferencia respecto al caso de las matrices estáticas anteriores (L.2 y L.8 ), de que ahora existen dos objetos: un puntero en la pila, y un espacio anónimo en el montón (accesible mediante el puntero). El inconveniente es que hay que acordarse de desasignar la zona de memoria del montón cuando resulte innecesaria [2]. Esto se realizaría con la sentencia:

delete[] m1;


§4.2 Procedimiento para crear la misma matriz según el sistema clásico:

int* m1 = (int*) calloc(10, sizeof(int));


También en este caso m1 se declara como puntero-a-int, y se le asigna el valor devuelto por la función calloc; el resultado sería idéntico al anterior. Observe que en este caso se necesita un modelado ("casting" 4.9.9) antes de la asignación a m1, porque el valor devuelto por calloc es un puntero-a-void, y no se puede asignar directamente a un puntero-a-int. La versión actualizada del modelado sería:

int* m1 = static_cast<int*>( calloc(10, sizeof(int)) );


§4.2.1  Como se comprueba a continuación, aunque se haya definido en términos de puntero-a-int, nada impide considerar a m1 como una auténtica matriz a todos los efectos (puede aplicársele la notación de subíndices):

#include <iostream.h>

int main() {            // =============
  int* m = new int[5];
  for (int i = 0; i <5 ; i++) {
    m[i] = i+10;
    cout << "m[" << i << "] == " << m[i] << endl;
  }
  delete[] m;
  return 0;
}

Salida:

m[0] == 10
m[1] == 11
m[2] == 12
m[3] == 13
m[4] == 14


§4.3  No obstante lo anterior, considere los sorpresivos resultados del siguiente programa que crea tres matrices, m1, m2 y m3 utilizando los tres métodos señalados anteriormente:

#include <iostream>
using namespace std;

void main() {         // =============
  int m1[5];
  int* m2 = new int[5];
  int* m3 = static_cast<int*>( calloc(5, sizeof(int)) );

  cout << typeid(m1).name() << " tamaño: " << sizeof(m1) << endl;
  cout << typeid(m2).name() << " tamaño: " << sizeof(m2) << endl;
  cout << typeid(m3).name() << " tamaño: " << sizeof(m3) << endl;

  m1[4] = 3; m2[4] = 3; m3[4] = 3;   // L.10:
  cout << "m1[" << 4 << "] == " << m1[4] << endl;
  cout << "m2[" << 4 << "] == " << m2[4] << endl;
  cout << "m3[" << 4 << "] == " << m3[4] << endl;

  delete[] m2;        // L.15
  free(m3);           // L.16
}

Salida:

int[5] tamaño: 20
int * tamaño: 4
int * tamaño: 4
m1[4] == 3
m2[4] == 3
m3[4] == 3

Comentario:

La primera observación es que m1 se ha creado en la pila, mientras que m2 y m3 están en el montón. Observe en L.15 y L.16 la distinta forma de recusar el espacio asignado para estas últimas.

La segunda observación es que tanto m1 como m2 y m3, se comportan como matrices; en L.10 y siguientes aceptan la notación de subíndices. Sin embargo, observe que, mientras que el operador typeid ( 4.9.14) nos informa que m1 es efectivamente una matriz del tipo y tamaño esperados, en cambio m2 y m3 siguen siendo consideradas por el compilador como tales punteros y su tamaño es el correspondiente.

En estas condiciones la pregunta es inevitable: ¿Que ha pasado con las matrices m2 y m3? ¿Como pueden almacenarse 5 enteros en 4 Bytes?.

La respuesta está en que en ambos casos se han creado dos variables tipo puntero-a-int en la pila !!. Se trata de variables automáticas del tipo indicado y tamaño 4 Bytes (como todos los punteros), que responden a los nemónicos m2 y m3.

Simultáneamente se ha reservado en el montón dos zonas de memoria de 20 Bytes; sus direcciones se han asignado a los punteros m2 y m3, con lo que deben ser accedidas siempre a través de ellos. El resultado se esquematiza en la figura 2.


Observe que aunque podemos utilizarlas como si se tratara de auténticas "matrices", para el compilador existen diferencias abismales. Mientras que m1 es una variable automática de tipo matriz-de-enteros; m2 y m3 son variables automáticas de tipo puntero-a-int. El hecho que señalen a un espacio persistente en el montón, adecuado para almacenar 5 enteros es circunstancial; incluso puede realizarse una nueva asignación sobre ellas. Por ejemplo:

int x = 5;
m2 = &x;

m2 señalaría ahora a una zona de almacenamiento en la pila. El único problema es que el bloque de memoria reservado en el montón quedaría irremediablemente perdido. La liberación de estas zonas de memoria, que se realiza en L.15 y L.16, no significa la destrucción de las variables m2 y m3 (de hecho podrían seguir siendo utilizadas para otros usos); su destrucción efectiva se realiza cuando salga de ámbito el bloque en que han sido definidas (en este caso la función main).

  Inicio.


[1] Funciones de la Librería Estándar C++ que son en realidad herencia del C clásico.

[2] Cuando se reserva espacio en el montón mediante sentencias como new o calloc, junto con el propio espacio, el compilador incluye alguna información adicional, como el tamaño del área reservada. Por esta razón puede desasignar todo el espacio con una sola instrucción, delete o free, indicando solo el punto de comienzo del área a liberar.