Correspondencia entre arrays y punteros

En muchos aspectos, existe una equivalencia entre arrays y punteros. De hecho, cuando declaramos un array estamos haciendo varias cosas a la vez:

  • Declaramos un puntero del mismo tipo que los elementos del array.
  • Reservamos memoria para todos los elementos del array. Los elementos de un array se almacenan internamente en la memoria del ordenador en posiciones consecutivas.
  • Se inicializa el puntero de modo que apunte al primer elemento del array.

Las diferencias entre un array y un puntero son dos:

  • Que el identificador de un array se comporta como un puntero constante, es decir, no podemos hacer que apunte a otra dirección de memoria.
  • Que el compilador asocia, de forma automática, una zona de memoria para los elementos del array, cosa que no hace para los elementos apuntados por un puntero corriente.

Ejemplo:

int vector[10]; 
int *puntero;
 
puntero = vector; /* Equivale a puntero = &vector[0]; (1) 
                     esto se lee como "dirección del primer elemento de vector" */ 
(*puntero)++;     /* Equivale a vector[0]++; (2) */ 
puntero++;        /* puntero equivale a asignar a puntero el valor &vector[1] (3) */

¿Qué hace cada una de estas instrucciones?:

En (1) se asigna a puntero la dirección del array, o más exactamente, la dirección del primer elemento del array vector.

En (2) se incrementa el contenido de la memoria apuntada por puntero, que es vector[0].

En (3) se incrementa el puntero, esto significa que apuntará a la posición de memoria del siguiente elemento int, y no a la siguiente posición de memoria. Es decir, el puntero no se incrementará en una unidad, como tal vez sería lógico esperar, sino en la longitud de un int, ya que puntero apunta a un objeto de tipo int.

Análogamente, la operación:

puntero = puntero + 7;

No incrementará la dirección de memoria almacenada en puntero en siete posiciones, sino en 7*sizeof(int).

Otro ejemplo:

struct stComplejo { 
   float real, imaginario; 
} Complejo[10]; 
stComplejo *pComplejo;  /* Declaración de un puntero */

pComplejo = Complejo; /* Equivale a pComplejo = &Complejo[0]; */ 
pComplejo++;          /* pComplejo  == &Complejo[1] */ 

En este caso, al incrementar pComplejo avanzaremos las posiciones de memoria necesarias para apuntar al siguiente complejo del array Complejo. Es decir avanzaremos sizeof(stComplejo) bytes.

La correspondencia entre arrays y punteros también afecta al operador []. Es decir, podemos usar los corchetes con punteros, igual que los usamos con arrays. Pero incluso podemos ir más lejos, ya que es posible usar índices negativos.

Por ejemplo, las siguientes expresiones son equivalentes:

   *(puntero + 7);
   puntero[7];

De forma análoga, el siguiente ejemplo también es válido:

   int vector[10];
   int *puntero;
   
   puntero = &vector[5];
   puntero[-2] = puntero[2] = 100;

Evidentemente, nunca podremos usar un índice negativo con un array, ya que estaríamos accediendo a una zona de memoria que no pertenece al array, pero eso no tiene por qué ser cierto con punteros.

En este último ejemplo, puntero apunta al sexto elemento de vector, de modo que puntero[-2] apunta al cuarto, es decir, vector[3] y puntero[2] apunta al octavo, es decir, vector[7].

Operaciones con punteros

La aritmética de punteros es limitada, pero en muchos aspectos muy interesante; y aunque no son muchas las operaciones que se pueden hacer con los punteros, cada una tiene sus peculiaridades.

Asignación

Ya hemos visto cómo asignar a un puntero la dirección de una variable. También podemos asignar un puntero a otro, esto hará que los dos apunten a la misma dirección de memoria:

int *q, *p;
int a;
 
q = &a; /* q apunta a la dirección de a */ 
p = q;  /* p apunta al mismo sitio, es decir, 
           a la dirección de a */

Sólo hay un caso especial en la asignación de punteros, y es cuando se asigna el valor cero. Este es el único valor que se puede asignar a cualquier puntero, independientemente del tipo de objeto al que apunte.

Operaciones aritméticas

Podemos distinguir dos tipos de operaciones aritméticas con punteros. En uno de los tipos uno de los operandos es un puntero, y el otro un entero. En el otro tipo, ambos operandos son punteros.

Ya hemos visto ejemplos del primer caso. Cada unidad entera que se suma o resta al puntero hace que este apunte a la dirección del siguiente objeto o al anterior, respectivamente, del mismo tipo.

El valor del entero, por lo tanto, no se interpreta como posiciones de memoria física, sino como posiciones de objetos del tipo al que apunta el puntero.

Por ejemplo, si sumamos el valor 2 a un puntero a int, y el tipo int ocupa cuatro bytes, el puntero apuntará a la dirección ocho bytes mayor a la original. Si se tratase de un puntero a char, el puntero avanzará dos posiciones de memoria.

Las restas con enteros operan de modo análogo.

Con este tipo de operaciones podemos usar los operadores de suma, resta, preincremento, postincremento, predecremento y postdecremento. Además, podemos combinar los operadores de suma y resta con los de asignación: += y -=.

En cuanto al otro tipo de operaciones aritméticas, sólo está permitida la resta, ya que la suma de punteros no tiene sentido. Si la suma o resta de un puntero y un entero da como resultado un puntero, la resta de dos punteros dará como resultado, lógicamente, un entero. Veamos un ejemplo:

int vector[10]; 
int *p, *q;
 
p = vector; /* Equivale a p = &vector[0]; */ 
q = &vector[4]; /* apuntamos al 5º elemento */ 
cout << q-p << endl;

El resultado será 4, que es la "distancia" entre ambos punteros.

Generalmente, este tipo de operaciones sólo tendrá sentido entre punteros que apunten a objetos del mismo tipo, y más frecuentemente, entre punteros que apunten a elementos del mismo array.

Comparación entre punteros

Comparar punteros puede tener sentido en la misma situación en la que lo tiene restar punteros, es decir, averiguar posiciones relativas entre punteros que apunten a elementos del mismo array. Podemos usar los operadores <, <=, >= o > para averiguar posiciones relativas entre objetos del mismo tipo apuntados por punteros.

Existe otra comparación que se realiza muy frecuente con los punteros. Para averiguar si estamos usando un puntero nulo es corriente hacer la comparación:

if(NULL != p)

Lo expuesto en el capítulo 9 sobre conversiones implícitas a bool también se aplica a punteros, de modo que podemos simplificar esta sentencia como:

if(p)

Y también:

if(NULL == p)

O simplemente:

if(!p)

Nota: No es posible comparar punteros de tipos diferentes, ni aunque ambos sean nulos.

Comentarios de los usuarios (10)

David
2010-06-10 12:32:26

Hola!

Necesitaba enviaros un comentario para deciros que tenéis una web tremenda, en un par de días he comprendido gracias a vosotros cosas que no he podido entender en todo un curso. ¡Os lo agradezco mucho, gracias!

silvestre alejandro
2010-12-02 03:58:24

Hola y felicidades por el excelente tutorial.Mi pregunta era la siguiente con respecto a la correspondecia entre arrays y punteros, si es que se aplica de igual forma cuando es un array de mas dimensiones, por ejemplo:

int mat[10][5];

mi pregunta es,¿solo existirá un puntero a mat o se creara un array de 10 punteros?

por que segun yo se y como vos tambien lo explicaste se reservara direcciones de memoria consecutivas para almacenar esa matriz, ahora para para acceder a mat[1][1] se utiliza la matriz de 10 punteros o solo se hacen calculos a partir del unico puntero que tenemos( para mat[1][3] ==> a la direccion de (mat*(1*sizeof(int))) + (3*sizeof(int)) ==> (mat*(fila*sizeof(int))) + (columna*sizeof(int))

espero su respuesta, de antemano gracias!!

Steven R. Davidson
2010-12-27 15:27:22

Hola Silvestre Alejandro,

Hay que tener presente la siguiente diferencia: un array ES una dirección de memoria, mientras que un puntero es una variable que sirve para almacenar direcciones de memoria.

En cuanto al array que expones de ejemplo, técnicamente todos los elementos (de tipo \'int\') son almacenados en un bloque de memoria cuyas direcciones son contiguas. El array, \'mat\', es una dirección de memoria del primer elemento; o dicho de otra manera, es la dirección de la memoria del comienzo de tal bloque.

Esto es mejor verlo con un vector descriptor para ver lo que sucede en la memoria:

Dirección

de Memoria Nombre Tipo Valor Misceláneo

------------------------------------------------------

0x44FFAA00 ---- int -5013 mat := 0x44FFAA00

0x44FFAA04 ---- int 10382

0x44FFAA08 ---- int -1

0x44FFAA0B ---- int 82931

0x44FFAA10 ---- int 90

0x44FFAA14 ---- int 14890 mat[1][0]

0x44FFAA18 ---- int 15

0x44FFAA1B ---- int -58433

0x44FFAA20 ---- int 88800

0x44FFAA24 ---- int -599312

...

0x44FFAAB4 ---- int 10032 mat[9][0]

0x44FFAAB8 ---- int 299661

0x44FFAABB ---- int -333021

0x44FFAAC0 ---- int 994656

0x44FFAAC4 ---- int -668311

...

Los valores que he puesto son de ejemplo, pero lo que quiero que veas es que todos éstos se almacenan contiguamente en memoria. También observa que \'mat\' no es una variable como las demás sino que ES la dirección de memoria 0x44FFAA00, la cual almacena el primer elemento: mat[0][0].

Si esto fuese un array de 10 punteros,

int *ptrMat[10];

entonces veríamos la diferencia con el array bidimensional usando otro vector descriptor:

Dirección

de Memoria Nombre Tipo Valor Misceláneo

------------------------------------------------------------

0x44FFAA00 ---- int * 0x44FFBB00 ptrMat := 0x44FFAA00

0x44FFAA04 ---- int * 0xAA22CC00

0x44FFAA08 ---- int * 0x66DD7700

...

0x44FFAA20 ---- int * 0x44AAEE00

0x44FFAA24 ---- int * 0xFFDD9900

...

0x44AAEE00 ---- int * 2203177 ptrMat[8][0]

...

0x44FFBB00 ---- int * -400221 ptrMat[0][0]

...

0x66DD7700 ---- int * 6944012 ptrMat[2][0]

...

0xAA22CC00 ---- int * -503 ptrMat[1][0]

...

0xFFDD9900 ---- int * 9908 ptrMat[9][0]

...

Observamos que tenemos un array de 10 elementos que es \'ptrMat\' comenzando en la dirección de memoria 0x44FFAA00. Como los elementos son punteros, se almacenan direcciones de memoria. Cada una de estas direcciones de memoria almacenadas indicará el comienzo de un bloque de memoria el cual contendrá número enteros.

La otra posibilidad es que tengamos un doble puntero para representar una matriz. Por ejemplo,

int **pptr;

Para representar tal matriz, habría que adjudicar memoria dinámicamente y asignársela a cada puntero. Un posible vector descriptor pudiere ser el siguiente:

Dirección

de Memoria Nombre Tipo Valor Misceláneo

---------------------------------------------------

0x44FFAA00 pptr int ** 0x55AAEE00

...

0x55AAEE00 ---- int * 0xEE00FF00 pptr[0]

0x55AAEE08 ---- int * 0x66DD7700 pptr[1]

...

0x66DD7700 ---- int -559032 pptr[1][0]

...

0xEE00FF00 ---- int 11201 pptr[0][0]

...

Espero que esto aclare las dudas.

Steven

José
2011-01-27 04:16:35

La senetencia puntero[7] no tiene efecto, según el compilador GCC en windows x86 con Code::Blocks:

void main()

{

int vector[3] = {2,4,8};

int *pElem;

pElem = vector;

int i = 2;

//Aca el compilador em tira un warning: statement has no effect. De hecho no hace nada

pElem[i];

(*pElem) += 20;

cout << "Valor de vector["<<i<<"] = "<< vector[i]<<endl<<endl;

//Pero esta forma si funciona:

*(pElem + i);

(*pElem) += 20;

cout << "Valor de vector["<<i<<"] = "<< vector[i]<<endl<<endl;

return 0;

}

Salvador Pozo
2011-01-27 08:51:59

Para José:

Efectivamente, una sentencia como esa no hace nada, porque no tiene nada que hacer. No hay asignaciones, ni llamadas a función, en definitiva, nada cambia.

pElem[i];

Hay muchas sentencias válidas que no hacen nada, y que un compilador puede suprimir, es decir, que no generan código. Por ejemplo:

i;

32+43;

Tu sentencia "pElem[i];" equivale a "8;". Es una sentencia válida, pero no aporta nada.

Lo mismo vale para la sentencia "*(pElem + i);", que es equivalente.

En el primer caso sospecho que esperabas que el valor de pElem cambiara por usar un índice. Pero los índices no modifican el valor del vector, sólo permiten acceder a direcciones de memoria mediante un puntero y un desplazamiento, de modo que el valor del puntero se mantiene.

En el primer caso la sentencia "(*pElem) += 20;" tiene efecto, pero sobre el primer elemento del vector, ya que pElem apunta a ese elemento. De modo que vector[0] vale 22.

En el segundo caso tampoco pasa nada con el elemento 2, ya que la sentencia "(*pElem) += 20" hace referencia al primer elemento del vector, que pasará a contener el valor 42.

Hasta pronto.

Oscar
2011-09-12 20:59:41

Hola antes que nada, quiero dar una gran felicitación por este tutorial es excelente.

Con respecto a el siguiente código, por que no puedo tener acceso a la variable float real mediante el puntero, para modificar su valor.

struct struc{

float real, imaginario;

} Complejo[10];

struc *pComplejo;

pComplejo = Complejo; /* Equivale a pComplejo =&Complejo[0];*/

pComplejo.real=3.1416;

pComplejo++; /* pComplejo == &Complejo[1] */

pComplejo.real=4.1416;

cout<<endl<<endl<<Complejo[0].real;

cout<<endl<<Complejo[1].real;

Gracias.

Steven R. Davidson
2011-09-12 21:58:51

Hola Óscar,

El problema es que usaste el operador de acceso a miembro que es el punto. Esto es correcto para variables de tipo 'struct', pero no es válido para acceder a un miembro a través de un puntero. Recuerda que debemos hacer dos accesos: uno a la variable de la estructura (en memoria) usando el operador * de indirección como explicamos en este capítulo ( http://c.conclase.net/curso/index.php?cap=012#PUNT_Diferencias ) y otro al miembro que queramos de tal estructura. En tu caso, esto sería así,

(*pComplejo).real = 3.1416;

Necesitamos los paréntesis, porque el operador . se evalúa antes que el operador *. Para forzar que la operación de indirección se realice antes que la de acceso a miembro, usamos paréntesis; esto es casi como en matemáticas, los paréntesis se hacen primero.

Esto es bastante engorroso, por lo que C++ proporciona otro operador que hace el trabajo de estos dos operadores conjuntamente, que es la "flecha": -> Esto es,

pComplejo->real = 3.1416;

Recuerda que C++ es un lenguaje FUERTEMENTE tipificado. Cada entidad (valor, variable, función, etc.) DEBE tener un tipo conocido. Esto también significa que se mantienen la sintaxis y semántica acerca de cada tipo de cada entidad involucrada.

En el siguiente apartado ( http://c.conclase.net/curso/index.php?cap=012c#PUNT_Estructuras ) hablamos de la manera de acceder a miembros de una estructura a través de un puntero.

Espero haber aclarado las dudas.

Steven

Antonio
2013-01-06 03:31:31

Hola de nuevo a todos.

Mi pregunta es solo por mera curiosidad...

Al realizar un ejercicio de este capitulo me he topado con la siguiente cuestión, al hacer un bucle dentro de una función he usado la sigiente expresión:

int mifuncion(char *cadena)
{
    while(NULL != *cadena) 
    {
        ....
    }
    return valorderetorno;
}

y el compilador con Code::Blocks (GCC desde Linux) me da el siguiente aviso:

se usó NULL en la aritmética

he probado cambiando "NULL" por "0L"(cero L), y por "0" (cero), en ambos casos compila sin dar ningún aviso; Tengo entendido que "0L" es el valor asignado a "NULL" por lo que vería lógico que me diese también un aviso.

Por tanto:

¿Que diferencia a NULL de 0L y 0?

¿y ya de paso, que significa la letra L?

Salvador Pozo
2013-01-06 13:25:19

Hola Antonio:

El compilador define NULL en C++ como un puntero, de modo que cuando comparas con *cadena, que es un carácter, el compilador se queja, ya que la conversión implícita de puntero a carácter puede que no sea lo que intentas hacer.

Deberías comparar con cero, o con el carácter nulo ('\000').

La diferencia es, pues, que NULL es un puntero, y 0 ó 0L son constantes enteras.

La 'L' significa que la constante es long. Eso se explica en el capítulo 7, dedicado a la notación.

Antonio
2013-01-07 17:22:43

Muchas gracias Salvador. :D

Me imaginaba que seria algo similar.

Con respecto a la notación, la verdad es que no la estoy usando, supongo que para programas pequeños como los ejercicios, la diferencia es mínima, pero para funciones que tenga pensado reutilizar en programas mayores vale la pena.

voy a repasar los ejercicios que ya tengo hechos, para optimizarlos, y procurare su uso como norma en los próximos...