22 octubre 2009

Apuntadores en C: ¿pa’ onde apunto?

apuntarLa idea de este mensaje es ayudar a que aprendas a aplicar los apuntadores de manera que sean útiles. Los apuntadores son muy útiles en C, como espero que podrás apreciar en este mensaje.  Está por demás decir que no puedo cubrir TODO lo que hay que ver con apuntadores en este mensaje.  Si luego tengo tiempo, puedo escribir una segunda o tercera parte para explicar como usar apuntadores para pasar parámetros a las funciones por referencia, ver su relación con arreglos, usar apuntadores a funciones y a arreglos de caracteres.

¿Y qué son esas ondas que suenan tan complicadotes?

En primer lugar, un apuntador es una variable que almacena una dirección de memoria. Generalmente, si está bien hecho, esa región de la memoria tiene algún dato interesante.

¿Y cómo nos les declaramos?

Se declaran al igual que cualquier variable, solo que se le antepone una asterisco. Estos son algunos ejemplos:

Codigo1

Consideraciones

Recuerda que & sirve para indicar la dirección de memoria donde se guarda el contenido de una variable. Y el * regresa el valor a la cual apunta un apuntador (valga la redundancia). Ejemplos:

Codigo2

Nota: Aquí estoy usando el printf, pero lo puedes sustituir con el cout, si lo prefieres.

Riesgo: si no lo inicializas, lo pagarás muy caro

Cuando declaras un apuntador ya apunta a algún lado (cuando declaras una variable en C simplemente reserva el lugar en la memoria, pero lo que había allí se queda). Tienes que inicializar el apuntador antes de usarlo. Fíjate en el siguiente código:

Codigo3

En el código de ejemplo 2 se muestra la manera correcta de hacerlo (bueno, de programarlo). Se inicializa yPtr a una dirección de memoria ya “apartada” por y.

Si no inicializas una variable cualquiera, a lo mucho no te da el resultado esperado, pero el resto del programa funciona. Pero con los apuntadores hay que tener mucho cuidado ya que pueden apuntar a cualquier lado en la memoria y, por lo tanto, puede alterar datos usados por diferentes variables o funciones. Es como un carro: te puede llevar a cualquier lugar (lo cual te da libertad y facilidad para viajar), pero si conduces sin fijarte por donde vas, puedes sufrir un accidente. Lo mismo sucede con los apuntadores: si no te fijas o controlas hacia donde apunta, puede ocurrir un error grave.

Haciendo algo útil con apuntadores: hostigar funciones

Podemos usar apuntadores con las funciones para hacer “cositas”. Básicamente hay 2 maneras de pasar valores a una función: por referencia y por valor.

Generalmente solo pasamos funciones por valor porque es la manera estándar de hacerlo.  En este caso, si modifico el valor del parámetro pasado por valor, solo se modifica dentro de la función y al terminar ésta, la variable es tragada por un inmenso hoyo negro que lo desplazará hacia una galaxia lejana y se pierde para siempre.

Al pasar valores por referencia, es otra onda. Pasas apuntadores como parámetros a una función y por esto, si alteras el valor contenida en una dirección de memoria (la que se le manda a la función), se altera en todos lados.

Esto se parece cuando llevan un pastel a un cumpleaños. Si se reparte un pastel a cada invitado, se asemeja a pasar por valor porque cada persona hace con su pastel lo que quiere (se lo come allí, se lo lleva a su casa, etc.). Sin embargo, si entre todos comen el mismo pastel, es como si pasaras por referencia, porque todos se reparten el mismo pastel: solo les dicen donde está el pastel, les dan el plato y se sirven. El pastel se va acabando entre todos.

Vamos viendo un ejemplo. Este código (el 4) usa una función muy simple, que eleva un número al cuadrado. De esta manera, que es la forma “ordinaria”, se pasan los parámetros por valor:

Codigo4

El siguiente ejemplo (código 5) pasa la dirección de memoria donde está almacenado el valor. Nótese que la función la declaro como tipo void porque ya no necesito que devuelva nada (porque altero lo que está almacenado en la dirección de memoria directamente).

Codigo5

La función cuadrado_ref(int *x) recibe la dirección de una variable entera (o sea, un apuntador entero), lo almacena de forma local como x y no devuelve un valor. Podríamos decir que el valor que se manda a la función es una “copia” de la dirección de memoria.

Hasta donde vamos, la utilidad del uso de apuntadores en funciones nos permite devolver más de un valor a la función que los llamó (porque cada función solo puede regresar un valor ya que solo puede tener una instrucción return). Y, además, nos permite mandar una cadena de texto a una función (sería imposible hacer esto con los parámetros “normales”, ya que el tipo de datos cadena no existe y tengo que mandar un arreglo de char), como vemos en el código 6, escrito a continuación.

Codigo6

Ahora si, ya con algunas bases, vamos haciendo cosas verdaderamente interesantes.

Reservar memoria de manera dinámica

Al leer este título, probablemente te estarás preguntando qué onda con eso, qué significa, que me fumé… Piensa lo que quieras, pero lo importante es que sepas que hay problemas en algunos programas si no sabes de antemano con cuántos datos vas a estar trabajando.

Supongamos que estás haciendo un programa en C que almacena los datos de los libros que tiene una biblioteca. Como un buen programa, lo diseñas pensando que no sabes de antemano el número de libros que tienen almacenados. A lo mejor es la biblioteca de la primaria federal no. 2, o a lo mejor es la biblioteca principal de la UNAM. Recuerda que un buen programa no debe restringirse a problemas del programador – al usuario le vale madres tus broncas, solo quieren un programa que funcione. Además no puedes estar contratado de por vida en un lugar que use tu programa. Tus aplicaciones deben poder adaptarse a diferentes condiciones o restricciones del lugar donde quieres vender y/o implementar tu solución.

Vamos a comenzar apartando memoria para usar cadenas de manera dinámica. Supongamos que no sabemos hasta la hora de ejecución el tamaño de un arreglo. Esto es una bronca porque si declaras un arreglo de caracteres (o sea, una cadena o “string”) de antemano, a lo mejor reservas memoria de más que no vas a usar o necesitas (si eres de Jalisco, tal vez “ocupes”) más caracteres de los que declaraste.

Fíjate en el código 7. Le pide al usuario el tamaño máximo que puede tener una cadena y reserva el espacio en la memoria. Después le pide al usuario que le dé un valor a la cadena que reservó.

Codigo7

El malloc es una función que aparta memoria. Recibe como parámetro el número de bytes que se van a reservar y devuelve una dirección de memoria (que nosotros asignamos a un apuntador). Se usa la función sizeof que devuelve el número de bytes que se requieren para almacenar un valor. En este ejemplo no era del todo necesario (sabemos de antemano que un char ocupa (bien empleado el término) un byte, pero lo puse porque es muy útil cuando se reserva memoria para usar estructuras (como una lista simplemente ligada, como hablaremos más adelante). El free en este caso no se trata de una relación relajada entre novios, sino que se trata de una función que libera la memoria apartada con malloc.

Lo primero que vas a encontrar en los libros de C++ es que es de mala educación usar el malloc y el free, los cuales son parte de la librería ANSI C (el C estándar que es acatado por todos los compiladores de C, menos el DevC). Sostienen que son “vicios” de programación y por eso el C++ propone el uso de otras funciones para reservar memoria de manera dinámica. Estas son el new y el delete. Pero de estos hablaremos después, ya que quiero centrarme solo en el uso de C estándar en este documento. En otro momento hablaremos de C++ y el uso de new y delete (que son muy buenos para trabajar con clases y objetos).

Bibliografía



Publicar un comentario
Related Posts Plugin for WordPress, Blogger...