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.2 Punteros y matrices

Nota: las indicaciones sobre subíndices contenidas en este capítulo se refieren a las matrices definidas con los tipos básicos (preconstruidos en el lenguaje), no a los tipos abstractos, en los que el operador subíndice [ ] puede tener un significado distinto.

§1 Posición de un elemento

Asumiendo que los elementos de una matriz se almacenan de forma contigua y que z es el tamaño en bytes de un elemento tipoX, el desplazamiento d en bytes respecto del comienzo, del elemento a[i] de una matriz tipoX a[n];, viene determinado por:

Desplazamiento (en bytes)  d = posición i  ·  tamaño del elemento z

Siendo el tamaño del elemento:  z = sizeof(tipoX).

Nota: recuerde que i tiene el valor 0 para el primer elemento.

Sin embargo, hemos visto que la aritmética de punteros ( 4.2.2) incluye automáticamente el tamaño del elemento. Por tanto, para manejar las matrices a través de punteros no es preciso referirse a las posiciones absolutas d de sus elementos (en bytes desde el comienzo de la matriz), sino en unidades tipoX. Es decir, a efectos de los punteros, la posición pi (respecto al comienzo) del elemento a[i] es simplemente pi = i.

Al primer elemento, a[0], de una matriz de dimensión m le corresponde la posición 0 y al último la posición m-1. El significado es que para alcanzar el primer elemento desde el principio el desplazamiento es cero, para el segundo el desplazamiento es un elemento, dos para el tercero, ... y m-1 para el último.

§2 Acceso a elementos mediante punteros

Por definición, excepto cuando es operando de los operadores de referencia & ( 4.2.3) o sizeof ( 4.9.13), el identificador de una matriz es considerado un puntero a su primer elemento [2]. Esto tiene importantes implicaciones prácticas, y es el secreto para comprender la mecánica de matrices; de punteros, y de las cadenas de caracteres en C++ (un tipo especial de matrices). Por ejemplo, cuando una matriz es pasada como argumento a una función, el identificador es considerado como un puntero al primer elemento, así que coloquialmente suele decirse: "las matrices se pasan por referencia"; por supuesto, dentro de la función llamada, el argumento aparece como una variable local tipo puntero.

Podríamos decir que el nemónico de una matriz C++ encierra una dualidad. En momentos puede ser considerado como representante de una matriz. Por ejemplo, cuando lo utilizamos con la notación de subíndices o en el operador sizeof. En otros casos adquiere la personalidad de puntero. Por ejemplo, cuando utilizamos con él el álgebra de punteros (en cierta forma, me recuerda la famosa dualidad onda-partícula de la luz que estudiamos en bachiller).

Esta dualidad hace que a efectos prácticos (salvo las excepciones ya comentadas), el identificador de una matriz sea sinónimo de la dirección de su primer elemento; de modo que si m es una matriz de elementos tipoX y pm un puntero-a-tipoX, la asignación pm = &m[0]; puede también ser expresada como pm = m;, o lo que es lo mismo, para la mayoría de los casos prácticos se puede establecer que:

 pm == &m[0] == m      §2a

Es decir:

 m == &m[0]  ↔  *m == m[0]      §2b


En el siguiente ejemplo se muestra como el nombre de la matriz puede ser tomado directamente como sinónimo de puntero al primer elemento.

int a[5] = {1,2,3,4,5};
printf("Posicion 0 = %1i\n", *a);
printf("Posicion 3 = %1i\n", *(a+3));

salida:

Posicion 0 = 1
Posicion 3 = 4


  Como vemos a continuación, es lo mismo pasar como argumento de una función:

  1. Meramente el nombre de la matriz
  2. La dirección del primer elemento
  3. Un puntero al primer elemento

Las tres alternativas producen el mismo resultado.

char a[11] = "Hola mundo",
char* ptr = &a[0];
char* s = "Hola mundo";
printf("%s\n", a);      // 1
printf("%s\n", &a[0]);  // 2
printf("%s\n", ptr);    // 3
printf("%s\n", s);      // 3

Nota: recuerde que cuando se trata de representar una cadena (%s) printf espera recibir como argumento un puntero al primer elemento de la misma (consulte el manual de su compilador respecto de esta función de la librería clásica C/C++).

Las cuatro funciones producen la misma salida, en todas ellas printf imprime desde la primera posición hasta alcanzar el primer nulo, que es interpretado como fin de cadena. El último es interpretado como no imprimible y automáticamente descartado.

Obsérvese que en la primera línea hemos hecho deliberadamente la dimensión de la matriz mayor que la cadena en 1 elemento para que el compilador incluya al final el carácter nulo ( 4.3.1). En cambio, en la definición de constantes de cadena (tercera línea) el compilador incluye por su cuenta el carácter de final ( 3.2.3f).


§2.2 En el ejemplo siguiente, además de demostrarse lo anterior, se comprueba como la función func supone que el argumento recibido es un puntero, y como tal lo trata en printf().

#include <stdio.h>
void func(char* ptr);   // prototipo
 
void main() {
   char a[5] = "AEIOU", *ptr = &a[0];
   func(a);              // Las tres
   func(ptr);           // llamadas son
   func(&a[0]);         // equivalentes
}
void func(char* arr) {  // definición
  int i;
  for (i=0; i<5; i++) {
      printf("arr[%1i] =%5.0d =%2c\n", i, *arr, *arr);
     arr++;
  }
  return;
}

Salida para las tres funciones:

arr[0] = 65 = A
arr[1] = 69 = E
arr[2] = 73 = I
arr[3] = 79 = O
arr[4] = 85 = U


§2.3  La dualidad matriz ↔ puntero también se pone de manifiesto en este sencillo ejemplo:

#include <iostream.h>

void fun(char*, int);        // L.3

int main() {                 // ============
  char* arr = new char[5];   // L.5
  fun(arr, 5);               // L.6
  delete[] arr; return 0;
}

void fun (char a[], int x) {      // L.9
  a[x] = static_cast<char>(x+65); // L.10
  cout << a[x] << endl;
}

Observe que aparentemente la declaración de fun en su prototipo de L.3 y su definición en L.9 no coinciden, a pesar de lo cual, no se obtiene ninguna protesta por parte del compilador. Así mismo, en L.5 se define arr como puntero-a-char y se pasa (L.6) a fun como argumento, aunque es aceptada por esta última como matriz de caracteres (char a[]), y utilizada como tal con la correspondiente notación de subíndices en L.10 y L.11 sin que exista tampoco protesta por parte del compilador.

Observe que en L.10 se suma 65 al valor x, con los compiladores que estamos utilizando, se trata de valor ASCII 65 + 5 == 70 ( 2.2.1a), con lo que esta línea equivale a:

a[x] = 'F';    // L.10-bis


§2.4  Es posible pasar a una función parte de una matriz, es decir, un puntero a un elemento distinto del primero. En el ejemplo anterior se podría haber puesto (ambas son equivalentes):

func(&a[2]);
func(a+2);

Compruebe el lector que en este caso sería necesario modificar la definición de la función func para garantizar que el bucle no pretenda escribir fuera del ámbito de la matriz, una posible solución seria cambiar la condición del bucle:

void func(char* arr) {   // definición

  int i;

  for (i=0; *arr != 0; i++) {

     printf("arr[%1i] =%5.0d =%2c\n", i, *arr, *arr);

    arr++;

  }

  return;

}


§ Siguiendo con la equivalencia enunciada (§2a ):

pm == &m[0] == m

Si sumamos un entero i al primer miembro, la equivalencia debe mantenerse, con lo que:

pm+i == (&m[0])+i == m+i

sabemos por la aritmética de punteros que el segundo miembro equivale a: &m[i], con lo que:

pm+i == &m[i] == m+i         §2.5a

aplicar el operador de indirección * a ambos lados:

*(pm+i) == *(&m[i]) == *(m+i)

es decir:

*(pm+i) == m[i] == *(m+i)         §2.5.b

De todas estas relaciones podemos deducir algunas consecuencias importantes:

m[i] == *(m+i)         §2.5c

En efecto; según K&R [1] "Al evaluar m[i], C la convierte inmediatamente en *(m+i); las dos formas son equivalentes". Puede probarse que también es cierto en C++ con un sencillo ejemplo:

int dia[] = {1,2,3,4,5,6,7};

pirntf("dia: = %2i\n", dia[2] );    // -> dia = 3

printf("dia: = %2i\n", *(dia+2) );  // -> dia = 3


Precisamente, el operador de elemento de matriz [ ] ( 4.9.16) se define como:

<exp1>[exp2]   ↔   *((exp1) + (exp2))

donde exp1 es un puntero y exp2 es un entero, o exp1 es un entero y exp2 es un puntero.

La expresión &m[i] == m+i  (§2.5a ) es la dirección del i-avo elemento de m.

Evidentemente (§2a ) se cumple que:

&m[0] == &m == m == m+0


§2.8
  Puesto que C++ es consistente y regular en cuanto a su tratamiento de la aritmética de punteros, de la misma forma que (por definición) se ha establecido que m[0] es equivalente a *m, y que en consecuencia m[i] es equivalente a *(m+i), también se ha establecido la relación simétrica, esto es: que si pm es un puntero al primer elemento de la matriz m, pm[0] sea equivalente a *pm, y que pm[i] sea equivalente a *(pm+i). Todo esto puede ser expresado con otras palabras:

  • Aplicar subíndices a un puntero-a-matriz equivale a aplicarle el operador de indirección ( 4.9.11a) es decir, devolver el objeto señalado:
  • pm[0] == *pm       §2.8a

  • Se puede aplicar a los punteros la notación de subíndices: (Ver ejemplo 4.9.11)
  • pm[i] == *(pm+i)       §2.8b


§
2.9 Recordemos las expresiones de quivalencia §2.5b :

*(pm+i) == m[i] == *(m+i)

Añadiendo la expresión §2.8b se obtiene:

*(pm+i) == m[i] == *(m+i) == pm[i]

La equivalencia entre el segundo y último término:

m[i] == pm[i]

puede enunciarse: una expresión de puntero con subíndices es equivalente a una matriz con subíndices.

  Puede ver un ejemplo avanzado de punteros y matrices en: "Punteros-a-miembros de clases" ( 4.2.1g1).


§3 La forma de calcular el tamaño de una matriz en tiempo de ejecución es:

sizeof ar/sizeof ar[0]   // tamaño total / tamaño del primer elemento

Puede utilizarse una directiva de preprocesador. Por ejemplo:

# define SIZEMAT(x) sizeof x/sizeof x[0]

Nota: observe que cuando, como en este caso, el identificador de una matriz se utiliza como operando de sizeof ( 4.9.13), es de las pocas veces en que se refiere a la totalidad de la matriz, y no es considerada como puntero a su primer elemento .

  Inicio.


[1] Kernighan & Ritchie 1988 pag. 99.

[2] El Dr. Stroustrup indica: TC++PL §10.4.7 "El nombre de un array puede ser usado como puntero a su elemento inicial", §5.3. "Like C, C++ doesn't distinguish between a pointer ot an individual object and a pointer to the initial element of an array". Sin embargo, desde un punto de vista estrictamente formal, se trata de una conversión estándar realizada automáticamente por el compilador ( 2.2.5).