En esta entrada hablaremos acerca de otro de los tipos de datos derivados: los punteros (o apuntadores). A veces este tema les resulta algo complicado a los que recién comienzan pero, si uno tiene claros los conceptos del tema, utilizarlos carece de mayores complicaciones.
Vamos por partes...
¿Qué es un puntero?:
Esta es la primera pregunta que surge en este tema y la respuesta es muy sencilla: Simplemente es una variable en la que podemos guardar la dirección de memoria de otra variable.
Declaración de punteros:
Para declarar punteros debemos tener en cuenta el tipo de dato de la variable a la que nuestro puntero apuntará, es decir de la que guardaremos la dirección de memoria. Y simplemente los declararemos de la siguiente manera:
Vemos que los declaramos como a una variable normal, con el tipo de dato de la variable a la que apuntará y colocándole el identificador (nombre) de nuestra elección. La diferencia respecto de otras variables es el operador de indirección "*" que llevan delante los identificadores de los punteros.
Este operador (*) nos permite acceder al valor que contiene la dirección de memoria guardada en el puntero. O, lo que es lo mismo, el valor de la variable a la que apunta.
NOTA: Cabe recordar que un puntero, como dijimos al comienzo, es una variable por lo que también tiene su propia dirección de memoria.
Asignando una dirección a un puntero:
Una vez declarado el puntero, podemos asignarle la dirección de la variable a la que apuntará, para ello generalmente utilizaremos el operador de dirección "&".
Este operador (&) nos permite conocer la dirección de memoria de la variable a la que se aplica (no se puede usar con constantes ni expresiones ya que estas no tienen dirección) por lo que, usando este operador, la forma clásica de asignar la dirección de una variable a un puntero será:
Como vemos, declaramos una variable de tipo int y un puntero a int. Luego, gracias al &, le asignamos al puntero (sin colocar el operador de indirección) la dirección de memoria de la variable.
Ahora que sabemos lo básico acerca de los punteros, veamos un ejemplo:
En este ejemplo simplemente declaramos una variable y un puntero a esa variable int, luego mostramos en pantalla lo que contienen var, &var, p, *p y &p. Hay que notar que para mostrar direcciones de memoria utilizamos la marca de formato %p.
En este ejemplo simplemente declaramos una variable y un puntero a esa variable int, luego mostramos en pantalla lo que contienen var, &var, p, *p y &p. Hay que notar que para mostrar direcciones de memoria utilizamos la marca de formato %p.
La imagen anterior nos muestra lo que obtenemos con ese código. Analicemos los resultados:
Hay que recordar que en los punteros se guardan direcciones de memoria por lo que en ellos no están definidas todas las operaciones que podríamos realizarle a otras variables. Las que sí son la suma y la resta, pero no como las conocemos ya que en este caso si le sumamos o restamos algo al puntero en realidad lo estamos moviendo hacia atrás o adelante por otras direcciones de memoria.
Por lo que si hacemos p = p + 1 (también puede hacerse p++) estamos diciéndole al puntero p que apunte a la dirección siguiente de la que apuntaba teniendo en cuenta el tipo de dato. Esto último significaría que si fuera un tipo int, se desplazaría 4 bytes hasta la siguiente dirección.
El resultado podemos analizarlo mejor en lo que se muestra en pantalla:
Al ver estas direcciones de memoria notamos que, en efecto, p se desplazó 4 bytes hasta la siguiente dirección de memoria. Si allí también quisiéramos mostrar el contenido de p veríamos que *p tendrá el valor de a, pero luego de desplazar p a la siguiente dirección de memoria obtendríamos basura en *p ya que no inicializamos en nada el contenido de dicha dirección:
En cambio si hiciéramos *p = *p + 1 , lo que estaríamos haciendo sería sumarle 1 al contenido de la variable a la que apunta p. Veamos esto último en un ejemplo:
Y obtendríamos por pantalla lo siguiente:
En donde vemos que, en efecto se le sumó 1 al valor de a y ésta fue modificada ya que se sobreescribió el nuevo resultado en la dirección de memoria almacenada en p.
Punteros y arrays:
Hagamos un flashback y recordemos un poco acerca de los array, en especial lo que explicamos cuando nos centramos en las cadenas de caracteres . En esa entrada, al hablar del uso de scanf dijimos que el identificador de un array guarda la dirección de memoria del primer elemento del mismo, entonces podemos decir que el identificador funciona como un puntero al elemento 0 del array.
Por esto mismo, si quisiéramos apuntar al comienzo de un array con un puntero bastaría con escribir:
Que, como dijimos, sería exactamente lo mismo que esto:
Ahora veamos en un ejemplo todas las formas de mostrar el contenido del vector en un bucle for.
La que ya conocíamos, sin usar nuestro puntero, consistía en hacer referencia al índice del elemento del array en cuestión:
Esto lo mostramos para recordar un poco y ahora compararlo con las formas en que podemos hacer lo mismo usando álgebra de punteros.
Sabemos que las direcciones de memoria de los elementos de un array son contiguas, por lo que podemos acceder a cada elemento desplazando el puntero.
Teniendo en cuenta eso último, veamos un par de formas de desplazarlo.
La primera forma de obtener el contenido de los elementos del array es ir desplazando p i direcciones con álgebra de punteros (p + i) y a esto lo indireccionamos para obtener el contenido. Esto vale por lo que aclaramos de las direcciones contiguas, al comienzo p se encuentra en el elemento 0 y si lo desplazamos las direcciones que señala coinciden con las de los elementos del array.
La segunda se trata de desplazar p por medio de subíndices. Sabemos que A es un puntero al elemento 0, por lo que si p apunta al mismo lugar que A podemos movernos por las direcciones con los mismos subíndices. Es decir:
A la primera la llamamos forma indexada y a la segunda subindexada.
Hasta aquí llegamos en esta entrada, espero que les sea de utilidad. En las próximas entradas veremos algunos ejemplos y hablaremos acerca del paso de parámetros por valor y por referencia en una función.
- 10 es el valor de var y también el valor guardado en la dirección a la que apunta p, es decir *p.
- La dirección de la variable y la guardada en p es la misma (0028FF44 en este caso).
- Como habíamos aclarado antes, la dirección de memoria de p (&p) es otra (0028FF40 en el ejemplo).
Álgebra (o aritmética) de punteros:
Hay que recordar que en los punteros se guardan direcciones de memoria por lo que en ellos no están definidas todas las operaciones que podríamos realizarle a otras variables. Las que sí son la suma y la resta, pero no como las conocemos ya que en este caso si le sumamos o restamos algo al puntero en realidad lo estamos moviendo hacia atrás o adelante por otras direcciones de memoria.
Por lo que si hacemos p = p + 1 (también puede hacerse p++) estamos diciéndole al puntero p que apunte a la dirección siguiente de la que apuntaba teniendo en cuenta el tipo de dato. Esto último significaría que si fuera un tipo int, se desplazaría 4 bytes hasta la siguiente dirección.
El resultado podemos analizarlo mejor en lo que se muestra en pantalla:
Al ver estas direcciones de memoria notamos que, en efecto, p se desplazó 4 bytes hasta la siguiente dirección de memoria. Si allí también quisiéramos mostrar el contenido de p veríamos que *p tendrá el valor de a, pero luego de desplazar p a la siguiente dirección de memoria obtendríamos basura en *p ya que no inicializamos en nada el contenido de dicha dirección:
En cambio si hiciéramos *p = *p + 1 , lo que estaríamos haciendo sería sumarle 1 al contenido de la variable a la que apunta p. Veamos esto último en un ejemplo:
Y obtendríamos por pantalla lo siguiente:
En donde vemos que, en efecto se le sumó 1 al valor de a y ésta fue modificada ya que se sobreescribió el nuevo resultado en la dirección de memoria almacenada en p.
NOTA: Vimos que un puntero como es una variable puede modificarse apuntando a otra dirección de memoria. Pero hay que recordar que una dirección de memoria en sí no puede modificarse, por lo que algo como &a = p es totalmente incorrecto.
Punteros y arrays:
Hagamos un flashback y recordemos un poco acerca de los array, en especial lo que explicamos cuando nos centramos en las cadenas de caracteres . En esa entrada, al hablar del uso de scanf dijimos que el identificador de un array guarda la dirección de memoria del primer elemento del mismo, entonces podemos decir que el identificador funciona como un puntero al elemento 0 del array.
Por esto mismo, si quisiéramos apuntar al comienzo de un array con un puntero bastaría con escribir:
Que, como dijimos, sería exactamente lo mismo que esto:
Ahora veamos en un ejemplo todas las formas de mostrar el contenido del vector en un bucle for.
La que ya conocíamos, sin usar nuestro puntero, consistía en hacer referencia al índice del elemento del array en cuestión:
Esto lo mostramos para recordar un poco y ahora compararlo con las formas en que podemos hacer lo mismo usando álgebra de punteros.
Sabemos que las direcciones de memoria de los elementos de un array son contiguas, por lo que podemos acceder a cada elemento desplazando el puntero.
Teniendo en cuenta eso último, veamos un par de formas de desplazarlo.
La primera forma de obtener el contenido de los elementos del array es ir desplazando p i direcciones con álgebra de punteros (p + i) y a esto lo indireccionamos para obtener el contenido. Esto vale por lo que aclaramos de las direcciones contiguas, al comienzo p se encuentra en el elemento 0 y si lo desplazamos las direcciones que señala coinciden con las de los elementos del array.
La segunda se trata de desplazar p por medio de subíndices. Sabemos que A es un puntero al elemento 0, por lo que si p apunta al mismo lugar que A podemos movernos por las direcciones con los mismos subíndices. Es decir:
A la primera la llamamos forma indexada y a la segunda subindexada.
Hasta aquí llegamos en esta entrada, espero que les sea de utilidad. En las próximas entradas veremos algunos ejemplos y hablaremos acerca del paso de parámetros por valor y por referencia en una función.
Han pasado casi 10 años desde que publicastes este post pero igual quiero comentar porque justo estoy estudiando C con ayuda de un libro y déjame decirte que tu explicación es tan clara como la del texto. Te felicito, se nota el dominio que tienes. Seguiré leyendo tus posts relacionados para ver que más aprendo. (y)
ResponderEliminar