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]


 Prev.

Matrices de matrices (II)

§7 Creación de matrices multidimensionales

La creación de matrices de más de una dimensión sigue pautas análogas a las unidimensionales ( 4.3.3). Existen dos aspectos a considerar que son independientes entre sí: el "donde" y el "como".  Adicionalmente existe otra cuestión que se refiere a la forma en que son iniciados los elementos de la matriz una vez que se ha definido. Es decir, cuando la matriz tiene existencia física (espacio de almacenamiento). Estas consideraciones podemos esquematizarlas como sigue:

  • Situación:  Donde se crea el almacenamiento
  • Técnica utilizada para definir la matriz (como se crea)
  • Forma de iniciar sus miembros a los valores correctos
§7.1 Situación

El primer punto "donde", se refiere a la localización de la matriz, aspecto este que tendrá influencia en la duración del objeto creado. En este sentido, la creación de matrices multidimensionales sigue pautas análogas a las de una dimensión ( 4.3.3), de forma que pueden estar situadas en la pila, el segmento o el montón. Para ello su declaración puede estar acompañada de los correspondientes especificadores de almacenamiento ( 4.1.8):

extern a1[][2];          // Ok.!
static int a2[2][3];     // Ok.!
register char a3[2][3];  // No tiene sentido!!
auto char a4[2][3];      // Ok.! generalmente innecesario


En general, las matrices declaradas dentro de un bloque o función, son objetos automáticos creados en la pila, cuya duración está limitada al ámbito de su bloque. Es el caso de las siguientes matrices:

void func() {
  char a5[2][3];        // Ok. matriz creada en la pila
  auto char a6[2][3];   // Ok. ídem (auto es superfluo)
  ...
}


Cuando se desean matrices persistentes caben dos posibilidades, ya que existen dos zonas de almacenamiento que confieren a los objetos este carácter: el segmento y el montón.

La primera opción consiste en asignar a la matriz espacio en el segmento ( 1.3.2). Este es un almacenamiento persistente utilizado por los objetos estáticos. Correspondería a matrices que fuesen estáticas porque se hubieran declarado fuera de cualquier función, o porque hubiesen sido declaradas explícitamente mediante los especificadores static ( 4.1.8c) o extern ( 4.1.8d).

Ejemplo:

static char a7[2][3];  // Ok.!


La segunda opción consiste en asignar a la matriz espacio en el montón ( 1.3.2). Esto puede hacerse con las funciones clásicas calloc / malloc, o preferiblemente con el operador new ( 4.9.20). En cualquier caso el objeto será accedido mediante un puntero:

char (*a6)[3][4];       // Ok. declara a6 como puntero-a-matriz de char de
                        // dos dimensiones [3]
a6 = new char[2][3][4]; // Ok. reserva espacio e inicia a6


Como veremos en el siguiente epígrafe, la localización de una matriz en el montón ha dado origen a dos técnicas para su creación según se utilicen los recursos del C clásico (funciones calloc / malloc) o las posibilidades del nuevo operador new[ ]. Ambas técnicas exigen que la inicialización de los miembros de la matriz se realice por el método que más adelante denominamos inicialización individual .

 §7.2 Definir una matriz

En este epígrafe nos referimos a dos técnicas para definir una matriz. Es decir, "declararla" (darle existencia semántica) e "iniciarla" (darle existencia física). En realidad este apartado se ocupa del segundo aspecto, la iniciación de la matriz, ya que existen dos formas de proporcionarle espacio de almacenamiento (una dirección de memoria). Las denominamos definición directa e indirecta; cada una requiere una forma distinta de declaración.

§7.2.1 Definición directa

La definición directa es la más evidente y lógica. Es también la que produce una matriz que podríamos denominar "real"; un objeto con un espacio de almacenamiento contiguo. Corresponde a definiciones del tipo:

char m1[2][6]= {"Ok", "Falso"};    // D1:
 
int main() {
  static int m2[3];                // D2:
  char m3[3] = {'M','R','B'};      // D3:
  char (*m4)[3] = new char[2][3];  // D4:

}


Las anteriores definiciones asignan espacio a las matrices correspondientes; m1 y m2 son creadas en el segmento. Observe que aunque D2 no sea formalmente una definición, el compilador dispone de toda la información necesaria para asignar espacio al objeto (aunque el contenido actual sea basura). m3 es creada en la pila; por su parte el puntero m4 corresponde a una matriz creada en el montón.

Aunque la definición directa exige que las dimensiones sean conocidas en el momento de la declaración-definición, la inicialización de los miembros puede efectuarse después (sería el caso de m2 y m4 en el ejemplo anterior). Por lo demás no presenta dificultades especiales.

Ver un ejemplo de creación de una matriz bidimensional en el montón utilizando el operador new[] ( 4.3.7)

§7.2.2 Definición indirecta

En la definición D4 del ejemplo anterior se han utilizado las posibilidades del operador new[] para declarar e iniciar una matriz bidimensional en el montón. Sin embargo, hasta fechas relativamente recientes este operador no existía en el lenguaje C++ [5], y desde luego tampoco en su antecesor C, lo que originó que cuando se pretendía crear matrices multidimensionales en el montón (utilizando las funciones clásicas de C calloc/malloc o el primitivo operador new) hubiera que recurrir a esta técnica especial que denominamos definición indirecta.

Hay que observar que aunque fue desarrollada para resolver el problema de matrices persistentes en el montón, puede ser utilizada con matrices en cualquier otro espacio (el segmento o la pila). También que, a pesar que el moderno operador new[] permite evitarla, en ocasiones es de utilidad merced a las ventajas que proporciona frente a la definición directa.

El proceso es algo más engorroso que el método directo, y tiene la dificultad de que no se crea realmente una matriz, sino un conglomerado de objetos (que denominamos seudo-matriz), aunque finalmente se comportan como tales. En cambio presenta la ventaja de que las dimensiones no tienen que ser conocidas de antemano. Además permitirían implementar versiones de matrices multidimensionales dinámicas con más facilidad que las matrices "verdaderas".

El procedimiento será detallado en varios ejemplos, pero adelantaremos el esquema de funcionamiento suponiendo la creación de una matriz m de caracteres y dos dimensiones, cuya declaración sería:

char m[2][3];

En este caso, m es una matriz de dos filas, m[0] y m[1], cada una de las cuales es una matriz como: {'a','b','c'}. De forma que m contiene un total de dos de estas sub-matrices de una dimensión. El sistema de definición indirecta consiste en crear ambas matrices (independientes) a las que denominamos m0 y m1:

char m0[3] = {'A','B','C'};
char m1[3] = {'a','b','c'};

A continuación se crea una matriz intermedia mi de dos dimensiones, cada uno de cuyos elementos es un puntero a carácter. Sus miembros se inician con las direcciones del primer miembro de m0 y m1:

char* mi[2] = {&m0[0], &m1[0]};

A continuación definimos un puntero pm al primer elemento de esta matriz.

char** pm = &mi[0];

Este puntero es el manejador "handler" de una pseudo-matriz de caracteres m[2][3] de dos filas y tres columnas, y puede ser utilizado como tal.

Ejemplo:

cout << "pm[0][0] = " << pm[0][0];    // -> pm[0][0] = 'A'
cout << "pm[0][1] = " << pm[0][1];   // -> pm[0][1] = 'B'
cout << "pm[0][2] = " << pm[0][2];   // -> pm[0][2] = 'C'
cout << "pm[1][0] = " << pm[1][0];   // -> pm[1][0] = 'a'
cout << "pm[1][1] = " << pm[1][1];   // -> pm[1][1] = 'b'
cout << "pm[1][2] = " << pm[1][2];   // -> pm[1][2] = 'c'


Ver en la página siguiente ejemplos detallados de definición indirecta, que incluyen la forma C clásica con calloc, y la forma C++ equivalente con new ( 4.3.7)

§7.3 Iniciación

El último aspecto se refiere a la inicialización de la matriz. Es decir, como se inicializan sus elementos una vez que la matriz dispone de espacio físico. Aquí caben dos opciones que son independientes de la zona de almacenamiento o técnica de creación utilizada:

  • Inicialización conjunta o global: Cuando es posible iniciar todos los miembros en el mismo punto de la declaración utilizando una sola sentencia .
  • Inicialización individual: Cuando la complejidad o el tipo de la matriz hace inviable la opción anterior, es necesario crearla en dos fases. En la primera se declara y se le asigna espacio, posteriormente se incializan sus miembros .
§7.3.1 Inicialización conjunta

Aunque hemos señalado que C++ no dispone de elementos incluidos en el propio lenguaje para manejar matrices como un todo, existe una curiosa excepción. Es el caso en que una matriz se puede inicializar en una sentencia en el mismo punto de su declaración. Por ejemplo, una expresión como:

char* mes[13] = { "Ilegal", "Enero", "Febrero", "Marzo", "Abril", "Mayo", "Junio", "Julio", "Agosto", "Septiembre", "Octubre", Noviembre", "Diciembre"};

es una definición (declaración e inicialización en una misma sentencia). En este punto el compilador inicia una matriz mes de punteros-a-char; crea las 13 cadenas literales (matrices); las almacena en sitios indeterminados de la pila ( 2.2.6), y rellena los elementos de la matriz previamente creada (mes) con las direcciones del comienzo de cada cadena literal [4].

Nota: el resultado hubiera sido el mismo que si se hubiese puesto mes[] sin indicar el tamaño, puesto que el compilador lo puede deducir de la propia definición ( 4.3.1). En cualquier caso, el resultado es que mes es una matriz unidimensional de 13 elementos (punteros-a-carácter).


También son permitidas expresiones como la siguiente (observe que puede omitirse la primera dimensión, no así la segunda):

static int dias[][12] = { {31,28,31,30,31,30,31,31,30,31,30,31},
                          {31,29,31,30,31,30,31,31,30,31,30,31} };

Es interesante comparar las dos definiciones siguientes:

char* a1[2] = {"Ok", "Falso"}; // L1: matriz de punteros a carácter (1 fila, 2 miembros)
char a2[][7]= {"Ok", "Falso"}; // L2: matriz de caracteres (2 filas 7 columnas)


Ya hemos visto que a1 es una matriz de dos punteros a carácter, cada uno de los cuales ocupa 4 bytes y señala a una cadena de longitud 3 y 6 caracteres respectivamente ("Ok\0" y "Falso\0"), almacenadas en sitios arbitrarios (ver figura). Las direcciones contenidas en a1[0] y a1[1], son las de los caracteres "O" y "F" respectivamente. Observe que L1 implica la creación de tres objetos distintos: la matriz y cada una de las cadenas.

En este contexto serian válidas expresiones como:

printf("%s\n", a1[0]);     // "Ok"      §7.3.1a
printf("%s\n", a1[1]);     // "Falso"


L2 define una matriz a2 de dos dimensiones; dos filas de siete elementos cada una (cadenas de 7 caracteres); las cadenas son consecutivas en memoria, ocupando una distribución como en el esquema.

O

k

\0

 

 

 

 

F

a

l

s

o

\0

 

0 1 2 3 4 5 6 7 8 9 10 11 12 13


Las direcciones señaladas por a2[0][0] y a2[1][0] son de los caracteres "O" y "F" respectivamente. Una expresión como a2[1][1] es perfectamente válida (el carácter "a" en este caso). También serían válidas expresiones como:

printf("%c\n",  a2[0][0]);     // "O"     §7.3.1b
printf("%s\n",  a2[1][0]);     // "F"


Obsérvese como (§7.3.1a ) la función printf() acepta un puntero como segundo argumento, y que en §7.3.1b , el segundo argumento también es traducido a un puntero por el compilador.

Una conclusión importante es que la sentencia L1supone la creación de tres objetos; una matriz de punteros y dos cadenas. Mientras que L2 implica un solo objeto, una matriz de caracteres. Esta última opción tiene en contra que todas las filas tienen que ser del mismo tamaño, mientras que en la opción L1 (matriz de punteros), los tamaños pueden ser distintos.

Otra diferencia radica en la forma de calcular la posición de un elemento. En a2, la posición de cualquier miembro. Por ejemplo, del elemento a2[1][4] == "o", puede ser establecida mediante la expresión: p = ( 7 * 1) + 4 == 11 (Posición de un elemento 4.3.6). Mientras que la posición del mismo elemento en a1 sería la señalada por el puntero a1[1]+4, lo que puede comprobarse ejecutando la sentencia:

printf("%c\n", *(a1[1]+4));     // -> "o"


Ver en 4.3.6a1 un ejemplo de comprobación de las consideraciones formuladas en este apartado.

§7.3.2 Inicialización individual

Hay ocasiones en que la inicialización de los miembros de la matriz no puede hacerse directamente en una sola sentencia como en el punto anterior. Esto puede deberse a circunstancias diversas. Por ejemplo, que los elementos no sean conocidos en el momento de la declaración; que se trate de una matriz muy compleja, o que sea una matriz creada en el montón (el operador new[ ] para matrices carece de iniciadores).

En estos casos, la creación e inicialización de la matriz debe hacerse en varios pasos. En el primero se define, asignándole espacio de almacenamiento en la pila, el segmento o el montón según la naturaleza de la variable. Ejemplo:

auto char a1[2][3];
static int a2[2][3];
char (*a3)[3] = new char[2][3];

Una vez la matriz tiene existencia real, la inicialización de sus miembros se efectúa individualmente. Por ejemplo:

a1[0][0] = 'A'; a1[0][1] = 'B'; a1[0][2] = 'C';
a1[1][0] = 'a'; a1[1][1] = 'b'; a1[1][2] = 'c';
a2[0][0] = 1; a2[0][1] = 2; a2[0][2] = 3;
a2[1][0] = 4; a2[1][1] = 5; a2[1][2] = 6;
a3[0][0] = 'A'; a3[0][1] = 'B'; a3[0][2] = 'C';
a3[1][0] = 'a'; a3[1][1] = 'b'; a3[1][2] = 'c';


  Inicio.


[3]  Puede resultar sorprendente que un puntero-a-matriz de dos dimensiones a6, pueda ser utilizado para señalar a una matriz de tres dimensiones:

a6 = new char[2][3][4];

La razón está en que a6 señala "al primer elemento de la matriz", y hemos señalado que en las matrices tridimensionales, el primer elemento es una matriz de dos dimensiones. Recordemos que: "Like C, C++ doesn't distinguish between a pointer ot an individual object and a pointer to the initial element of an array". Stroustrup TC++PL §10.4.7.

[4]  Suponemos que la declaración se encuentra dentro de un bloque o función.

[5]  El operador new existe desde la primera edición de C++PL, pero la versión new[ ] como operador independiente para matrices es una introducción posterior.

 Prev.