Ir al contenido principal

C: Punteros...

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.


La imagen anterior nos muestra lo que obtenemos con ese código. Analicemos los resultados:

  • 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.


Comentarios

  1. 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

Publicar un comentario

Entradas populares de este blog

C: Conversiones de tipo (casting) en C...

El casting o simplemente cast  nos permite hacer una conversión explícita de un tipo de dato a otro, a criterio del programador siempre y cuando estos tipos sean compatibles. Este cast se realiza a través de un operador de conversión de tipos (type casting operator) y es un recurso a tener en cuenta ya que hay situaciones en que nos puede resultar de gran utilidad. Hacer uso de un cast es tan sencillo como poner (tipo de dato)  delante de la expresión o variable a convertir. Veamos un ejemplo: Declaramos una variable de tipo int con un identificador tan creativo como "a" y le realizamos diferentes cast a a para mostrarlo como si fuera un float, un double y un char en un printf. Lo que obtendríamos en pantalla sería lo siguiente: Donde tenemos el valor de nuestro a, a convertido en float y double (mostrándolo con 3 cifras decimales) y a convertido en char. Si vemos este último caso, al hacer la conversión de "a" a char toma a como el código ascii de

C: Ejemplos: Congruencia de Zeller (nivel básico) ...

La Congruencia de Zeller es un algoritmo que se atribuye al matemático alemán Julius Christian Johannes Zeller que vivió en el siglo XIX. Este algoritmo nos permite determinar el día de la semana que le corresponde a una fecha determinada del calendario Gregoriano. La fórmula que nosotros usaremos (con algunas modificaciones respecto de la original para poder usarla en  informática) es la siguiente: Donde h es el día de la semana (entre 0 y 6), J es año/100 (la centuria) y K es año mod 100 (el año de la centuria). Y hay que tener en cuenta que los meses de enero y febrero cuentan como el mes 13 y 14 del año anterior. Ahora que tenemos la fórmula, programemos el algoritmo en C mediante el uso de una función: Analicemos el código paso a paso: Tenemos en cuenta el caso de enero y febrero: Dijimos que estos meses corresponden a los meses 13 y 14 del año anterior por lo que los asignamos como corresponde (mes + 12 , que dará 13 para enero y 14 para febrero) y le rest

Algoritmos: Resolución de problemas y refinamientos en pseudocódigo...

En otras entradas, vimos las partes que debe tener nuestro algoritmo en pseudocódigo y las estructuras que utilizaremos para resolverlo. Ahora llega el turno de implementar todo en conjunto para dar origen a nuestra creación. Pero ¿cómo resolvemos un problema así? Para hacerlo, utilizaremos lo que llamamos refinamientos sucesivos. Este concepto consiste en dividir el problema en subproblemas más pequeños y a estos, a su vez, en otros más pequeños; y así sucesivamente hasta que la solución de los últimos sea trivial, sencillo de resolver. Luego usaremos todas las soluciones obtenidas para armar la solución de nuestro problema mayor. Este principio, tiene base en parte de la técnica divide and conquer (dependiendo de la traducción: "divide y vencerás") que es una de las muchas técnicas de resolución de algoritmos existentes. Como vemos, al dividir el problema en otros más pequeños y más fáciles de resolver, podemos pasar de un problema complicado a uno cuya solución es much