Disponible la versión 6 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.1 Declaración de matrices

§1 Sintaxis

La declaración de matrices sigue la siguiente sintaxis:

tipoX etiqueta [<expr-const>]

  tipoX es el tipo de los elementos que serán almacenados en la matriz. Puede ser cualquier type-id ( 2.2) válido a excepción de void y de funciones (no pueden existir matrices de funciones, pero sí de punteros-a-función).

  etiqueta es el identificador

  <expr-const>: una expresión cuyo resultado debe ser una constante entera positiva n distinta de cero, que es el número de elementos de la matriz. Los elementos están numerados desde 0 hasta n-1.

Nota: no confundir el valor n con el hecho de que los elementos estén numerados de 0 a n-1. Por ejemplo, el elemento a[9] es el último de la matriz definida como: int a[10];. Esta "extraña" ocurrencia es herencia del C clásico y una costumbre muy extendida en la informática (empezar a contar por el cero); aunque se habitúa uno a ella rápidamente, la verdad es que resulta un poco extraña al principio [3].

Ejemplos:

int a[10];        // declara una matriz de 10 elementos enteros
char ch[10]       // ídem de 10 elementos char
char* p[10]       // ídem de 10 elementos puntero-a-carácter
struct St mst[10] // ídem de 10 elementos estructuras tipo St

Observe en esta última sentencia, que los elementos no tienen porqué ser necesariamente tipos simples (preconstruidos en el lenguaje). También pueden ser tipos abstractos ( 2.2a). Incluso su declaración puede realizarse en la misma sentencia que declara la matriz. Por ejemplo:

struc St {
   int orden;
   char* plato;
   char* descripc;
} mst[10]; 

Esta sentencia declara una matriz mst de 10 estructuras tipo St; ambas se declaran en la misma sentencia.

La exigencia de que el resultado de <expr-const> sea un valor constante, es de la mayor trascendencia para entender las limitaciones de las matrices C++. Significa que el tamaño de la matriz debe ser conocida en tiempo de compilación. Por ejemplo, no es posible algo como:

unsigned int size;
...
char matriz[size];    // Error!!

en su lugar debe hacerse:

const unsigned int size = 10;
...
char matriz[size];    // Ok.

pero entonces es preferible establecer directamente:

char matriz[10];      // Ok.

o mejor aún:

#define ASIZE 10
...
char matriz[ASIZE];   // Ok.

En general, cuando se necesitan matrices que cambien de tamaño en "runtime", suele recurrirse a crearlas en el montón mediante el operador new[] ( 4.9.20c) que sí permite definir su tamaño en función de una variable. Por ejemplo:

unsigned int size;
...
char* mptr = new char[size];    // Ok.

En este caso las matrices no son referenciadas directamente, sino a través de un puntero, y una vez creadas tampoco es posible cambiar su tamaño. El recurso utilizado cuando se necesita cambiar este, es crear otra matriz del tamaño adecuado; copiar en su caso los miembros de la antigua a la nueva; borrar la antigua (liberar el espacio asignado), y finalmente, asignar el puntero a la nueva matriz. De esta forma la ilusión del usuario es que realmente se ha cambiado el tamaño de la matriz, aunque la realidad subyacente sea muy diferente. Otro recurso, utilizado cuando la matriz "puede" crecer pero no se está muy seguro, es crearla con un tamaño inicial algo mayor que lo necesario (por ejemplo un 25%). En estos casos se dispone de cierto espacio de reserva antes que sea necesario proceder a un proceso de copia total.

No obstante lo anterior, en C-99 sí es lícita la declaración de matrices de longitud variable ("variable length arrays") que es como se conoce a las que pueden definir su tamaño en runtime.  Es decir, en este entorno están permitidas declaraciones del tipo

unsigned int size;
char matriz[size];    // Ok

Con objeto de garantizar la compatibilidad con el código C existente, algunos compiladores permiten la definición de estas matrices cuando son de naturaleza automática (definidas en el interior de una función).  Es el caso del compilador GNU gcc http://gcc.gnu.org/.  Sin embargo, es un recurso que debe evitarse, ya que el código resultante no resulta portable.


§2 Como el resto de las variables, la declaración de matrices puede estar acompañada de los correspondientes especificadores de almacenamiento ( 4.1.8), aunque por su propia naturaleza, con los actuales procesadores no tiene sentido declararlas variables de registro. Ejemplo:

extern a1[];          // L.1: Ok.!
static int a2[2];     // Ok.!
register char a3[20]; // No tiene sentido!!
auto char a4[3];      // Ok.!

Nota: por una larga tradición de C, en algunos compiladores la línea 1 equivale a extern int a1[], sin embargo en algún compilador puede dar error porque el ANSI C++ prohíbe la declaración de objetos sin la especificación de tipo.

§3 Acceso a elementos

Existen dos formas de acceder a los elementos de una matriz: mediante subíndices y mediante punteros. En cuanto a la primera, el empleo de subíndices es la forma que podemos llamar "canónica". Por ejemplo a[i] representa el elemento i+1avo. Recuerde que la numeración empieza por 0, de forma que a[1] es en realidad el segundo elemento ( 4.9.16 Operador de elemento de matriz). El acceso a elementos mediante punteros, se detalla en el apartado siguiente ( 4.3.2). Los elementos de la matriz del primer ejemplo se referencian mediante:

a[0], a[1], a[2], a[3], a[4], a[5], a[6], a[7], a[8] y a[9]

La numeración de subíndices de una matriz de n elementos es de 0 a n-1


§4 En ciertos contextos, el primer declarador de una matriz puede no contener una <expresión> dentro de las llaves, por ejemplo:

int array[];

Este tipo de matriz, de tamaño indeterminado, resulta aceptable en situaciones donde el tamaño no es necesario para reservar espacio, pero siguen siendo necesarias las llaves para indicar que la variable es una matriz. Por ejemplo, una declaración extern de una matriz no necesita el tamaño exacto de la misma; alguna otra declaración tampoco la necesita, porque el tamaño está implícito en el declarador. Por ejemplo:

char arr[] = "AEIOU";               // declara matriz de 6 elementos [1]
char arr[] = {'A','E','I','O','U'}; // declara matriz de 5 elementos


En otros casos, como en la definición de parámetros de funciones, no es necesario especificar el tamaño de la matriz a referenciada por un puntero. Por ejemplo:

int func(char* ptr[]);

este prototipo indica que func recibe como argumento una matriz-de-punteros-a-carácter, sin especificar nada sobre su tamaño; las llaves [ ] son necesarias para avisar a la función que se trata de una matriz de punteros, ya que la expresión:

int func(char* ptr);

indicaría simplemente puntero-a-carácter.

Nota: como una extensión especial del ANSI, C++Builder también permite una matriz de tamaño indefinido como el miembro final de una estructura. Dicha matriz no incrementa el tamaño de la estructura, excepto que puede añadirse relleno para asegurar que la matriz está correctamente alineada. Estas estructuras se utilizan normalmente en asignación dinámica, y debe añadirse explícitamente el tamaño actual de la matriz al tamaño de la estructura para la asignación del espacio.

§5 Inicialización

En ocasiones la declaración puede incluir una inicialización de la matriz como en los siguientes ejemplos:

cons int an[5] = {1, 2, 3, 4, 5};   // L.1:
char ak[5] = {'a','e','i','o','u'}; // L.2:
int ax[6] = {1,2,3,4};              // L.3: == {1,2,3,4,0,0}
char c[2] = {'1','2','3'};          // L.4: Error!
char c[2] = "AEIOU";                // L.5: Error!
char ac[5] = "AEIOU";               // L.6: Ok.
char ac[6] = "AEIOU";               // L.7: == {'A','E','I','O','U','\0'}
int ai[] = {1,2,3,4,5};             // L.8:
char as[] = "AEI";                  // L.9: == {'a','e','i','\0'}


Cuando el tamaño señalado es menor que la lista de inicio se produce un error (L.4). Si el tamaño es mayor, se rellenan los espacios sobrantes con ceros (L.3).

Nota: algunos de estos comportamientos son específicos del compilador y pueden variar según el entorno. Por ejemplo, la asignación L.6 es perfectamente válida en Borland C++ 5.5 para Windows, mientras que da error en el compilador GNU Cpp para Linux; la razón es que este último considera que una cadena del tipo "AEIOU" incluye implícitamente el carácter "\0" de fin de cadena ( 4.3.4) incluso en estos casos de inicialización de matrices (lo normal es que el terminador sea considerado cuando se trata de asignación a punteros).

Como puede verse en L.8 y L.9, si se incluye una inicialización de en la misma sentencia, excepcionalmente puede omitirse el tamaño en la declaración. La razón es que el compilador puede deducir lo contando los elementos de la lista de inicio.

Las cinco expresiones que siguen son igualmente válidas y definen st como matriz de caracteres de 6 elementos (el último elemento de la primera matriz es distinto al resto):

char st[] = "AEIOU"      // define la matriz "AEIOU\0"
char st[6] = "AEIOU\"";
char st[6] = {'A','E','I','O','U','\"'};
char st[6] = {'A','E','I','O','U','"'};
char st[6] = {'A','E','I','O','U',34};

Una expresión como: char st[4]= "AEIOU"; produce un error: Too many initializers, en cambio: char st[7]= "AEIOU"; no produce error; el espacio sobrante se llena de nulos (2 posiciones en este caso).

§5.1  Un ejemplo ejecutable con una inicialización más compleja

#include <iostream>
using namespace std;

#define DIMENSION ((int) (sizeof mst / sizeof mst [0]))

struct St {
  int orden;
  char* plato;
  char* desc;
} mst [] = {
  1, "Entrada", "Sopa juliana",
  2, "Segundo", "Filete empanado",
  3, "Postre ", "Tarta al Whisky",
};

int main() {  // ==========
  for (int i = 0 ; i < DIMENSION; i++)
     cout << mst[i].orden << " " << mst[i].plato << " " << mst[i].desc << endl;
  return 0;
}

Salida

1  Entrada  Sopa juliana
2  Segundo  Filete empanado
3  Postre   Tarta al Whisky

Comentario

Observe que, en la sentencia de salida, la forma de señalar los distintos miembros de cada elemento de la matriz, es congruente con el hecho de que cada elemento es una estructura.


§5.2
  C++ no tiene operadores para manejar matrices (ni cadenas de caracteres) como una sola unidad, solo quizás en la inicialización como hemos visto. Por esta razón, aunque es posible establecer expresiones como: char ac[5]="AEIOU"; sin embargo, las sentencias (asignaciones) segunda y tercera que siguen son ilegales.

char ac[5];      // L.1: declaración.
ac[5] = "AEIOU"; // L.2: Ilegal!!
ac = "AEIOU";    // L.3: Ilegal!!


L.2 produce el error [2]: Cannot convert 'char *' to 'char'. L.3 produce: Lvalue required.

La explicación es que en L.1 se declara ac como array de 5 caracteres; en L.2 el compilador crea una matriz de 6 caracteres "AEIOU", la almacena en algún sitio, e intenta asignar la dirección del primer carácter (un puntero char*) al elemento ac[5] que es (debe ser) un char, para ello debe hacer una conversión de tipo ( 4.9.9) de char* a char, operación ilegal que es señalada por el compilador.

En L.3 las cosas ocurren lo mismo, solo que en última instancia, se intenta asignar el puntero char* al nemónico ac. Como veremos en 4.3.2, en la mayoría de los casos, ac sería interpretado automáticamente por el compilador como la dirección del primer elemento de la matriz, pero en este caso no es así, lo que nos señala el compilador es que en el lado izquierdo (receptor) de la asignación, debe haber un Lvalue ( 2.1.5) es decir, una dirección de memoria donde comenzar el almacenamiento; dirección que puede ser expresada directamente por un escalar (que represente una dirección de memoria) o una expresión que pueda traducirse a una dirección. En este caso no es lo uno ni lo otro; el compilador interpreta ac simplemente como un nemónico que no tiene Lvalue (zona de almacenamiento).

En cambio, las expresiones siguientes son válidas:

char* ac;       // L.4:
ac = "AEIOU";   // L.5: Ok!!

La explicación, es que aquí L.4 declara ac como puntero a carácter (sin iniciar); en L.5 el compilador crea la cadena de caracteres "AEIOU\0", la almacena en algún sitio como matriz de 6 caracteres constantes, y asigna al puntero ac la dirección del primer elemento de dicha matriz, lo que es perfectamente legal.


En ocasiones, una asignación como la anterior puede estar enmascarada y no ser fácilmente reconocible, con lo que podría interpretarse que la expresión L.3 es legal. Por ejemplo, el caso que sigue compila sin error:

...
void func (char ac[]) {
   ac = "AEIOU";
   ...
}

la explicación que en este caso no se produzca error en la asignación, es que el compilador considera que el argumento ac es un puntero-a-char ( 4.3.2), con lo que estaríamos en el caso L.4/L.5.

La veracidad de las afirmaciones anteriores puede ponerse de manifiesto con un sencillo programita de prueba:

#include <iostream.h>   // Prueba de asignación
#include <typeinfo.h>

void func(char ar[]) {
  ar = "AEIOU";
  cout << ar << endl;
  cout << "ar es del tipo: " << typeid(ar).name() << endl; // L.7:
}

int main(void) {        // ===============
char arr[5] = {'1','2','3','4','5'};
func(arr);
return 0;
}

Salida (ver 4.9.14, para una explicación de L.7):

AEIOU
ar es del tipo: char *

  Inicio.


[1] Respecto al porqué decimos que declara 6 elementos, y no 5 como parecería lógico, ver "Constantes literales" ( 3.2.3f).

[2] Si se compila como programa C (Borland C++ permite compilar indistintamente en uno u otro lenguaje), produce el error: Nonportable pointer conversion.

[3] Las cosas ocurren aproximadamente de la misma manera en muchos otros lenguajes de programación. Por ejemplo, en Visual Basic (un producto de Microsoft), la declaración Dim x[10] As Interger, produce una matriz de once elementos numerados del 0 al 10 (no de diez numerados del 0 al 9 como su homóloga en C++). La costumbre de empezar a contar desde cero, que ha trascendido desde C a C++ y a otros lenguajes de programación, ha llevado a algún autor a afirmar que "Los ordenadores empiezan a contar desde cero"

Refiriéndonos a estas "costumbres informáticas" y aunque no sea de aplicación a los índices de matrices C++, podemos añadir que en cuestión de índices, también es costumbre que números negativos se refieran a posiciones desde el final. Así -1 sería el último elemento, -2 el penúltimo. Etc. Seguramente el lector se encontrará con esta convención en multitud de ocasiones.