martes, 15 de junio de 2010

Punteros en C

El siguiente mini-tuto nació de una ayuda de memoria y apuntes que escribí hace un tiempo sobre punteros en C. Está escrito y orientado a al lenguaje C, pero hay conceptos que son de programación en general y vienen bien para los que estén aprendiendo a programar, ya que al principio los punteros son un tema medio complicado de manejar. Si esto lo lee alguien bastante avanzado, entiendan que esto es bastante básico y simple para ayudar a entender.



/*
* PUNTEROS
*
* (Simples notas sobre que son punteros y cómo utilizarlos)
*
*/


Básicamente, la memoria de la computadora es un gran conjunto de Bytes, que se pueden representar de la siguiente manera:









Cada casillero de la imagen representa un Byte en la memoria.
Ahora bien, hay dos partes de los Bytes que nos interesan.

La primera: cada Byte tiene una dirección de memoria, que es justamente eso, una dirección para que se pueda acceder a ese byte y leer o escribir lo que hay en él. Las direcciones de memoria son números que se escriben en hexadecimal, pero para hacerlo fácil de entender, a las direcciones les puse un numero antecedido de una D. recuerden que un byte son 8 bits.

La segunda parte que nos interesa, es el contenido de cada Byte, o sea, los datos que almacena, en la imagen se ve que están vacios, pero normalmente cuando la memoria no se utiliza hay "basura" en ella, datos sin importancia, valores aleatorios, etc..
Entonces, en la imagen podemos ver la memoria separada en Bytes, con su respectivo contenido y sus direcciones (desde D1 a Dn).

Ahora, consideremos el siguiente programa.
(Pueden copiar y pegar los pedazos de código que voy poniendo e ir compilando a medida que avanzamos)

//Prog 1.

#include // incluimos stdio.h

int main(void)
{
float N = 2.1; // Declaro un float y le asigno el valor “2.1″ printf(”N vale %f \n”,N); // Imprimo el valor de N
return 0; }
Ahora, mediante el operador & (anpersand), puedo ver cuál es la dirección de memoria.
El operador &, lo que hace es devolver la DIRECCION de una variable, en lugar del contenido de la misma.

//Prog 2.

#include
// incluyo stdio.h

int main(void)
{
float N = 2.1;

printf(”N vale %.1f \n”,N); // Imprimo el CONTENIDO de la variable N

printf(”La direccion de N es %X \n”, &N); //Imprimo la DIRECCION de N


//con %X imprimo un numero en Hexadecimal, pero tambien es valido esto:
//printf(”La direccion de N es %d \n”, (unsigned int) &N);
//Fuerzo la conversion de la direccion de N a un numero entero
//para facilitar su lectura.


return 0;
}




Ahora, veamos lo que pasa en la memoria con estos dos programas.


N ocupa 4 bytes al ser del tipo float, y en esos 4 bytes esta almacenado el numero 2.1.

Si yo imprimo N obtendre -> 2.1 (el contenido)
Si yo imprimo &N obtendre -> D1 (la direccion de memoria)

Nota importante: En nuestro diagrama N ocupa 4 bytes: D1, D2, D3 y D4 ¿Entonces porué se dice que la dirección de N es solo D1?
Porque la dirección de memoria de una variable es la dirección solo del primer Byte.

La direccion de memoria no es mas que un numero entero, yo podria por ej, almacenarla en otra variable, o realizar operaciones sobre ese numero, etc.
Veamos el siguiente programa:

//Prog 3.

#include // incluimos stdio.h

int main(void)
{
float N = 2.1;
int direccion;
direccion = (unsigned int) &N; //asigno la DIRECCION de N a mi variable
// utilize UNSIGNED INT para forzar la conversion a entero
// UNSIGNED es porque una direccion de memoria NUNCA puede ser negativa
printf(”La direccion de N es %d”,direccion);
printf(”La direccion de N es %d”, (unsigned int) &N);
return 0;
}

Podemos ver como imprime dos veces el mismo numero.
Entonces, las direcciones de memoria no tienen ningun misterio, solo son numeros enteros
que pueden ser almacenados en otras variables enteras.



En nuestro esquema de memoria, despues de correr el ultimo programa habria quedado asi:


La variable “direccion” es entera, ocupa 2 bytes, y tiene almacenada la direccion de N, en este caso es D1.


Ahora bien, para diferenciar una variable entera normal, de una variable
entera que se dedicara a ALMACENAR DIRECCIONES DE MEMORIA, inventaron los
PUNTEROS.
Los punteros no son mas que eso, variables enteras que almacenan un numero que es una direccion de memoria.
Los punteros se declaran utilizando el operador * (asterisco) entre el tipo y el identificador de la variable, por ejemplo:

char *ptr;

Con el operador *, lo que hago es declarar una variable como puntero.
Bien, si dije que los punteros son variables que almacenen numeros enteros
Porque dice tipo char?
Nuestro puntero todavia es un numero entero, pero el tipo char indica que
en “ptr” se almacenara una direccion de una variable que es de tipo char.
Se dice que “ptr es un puntero a char“.
Con el operador * tambien puedo ver el contenido de una posicion de la
memoria.

Entonces, para recordar:

&p –> Me devuelve la DIRECCION de P.
*p –> Me devuelve el CONTENIDO de la DIRECCION que almacena P. (Si P es un puntero)

Veamos otro programa como ejemplo de un puntero.

#include // incluimos stdio.h
int main(void)
{
float N = 2.1; // Declaro mi variable de punto flotante y le asigno 2,1.
float *ptr; // una variable ptr que almacenara una direccion de memoria
// en la que habra una variable de punto flotante.
ptr = &N; // Le asigno a ptr la direccion de memoria de N, con el &.
printf(”La direccion de N es %X\n” , ptr); // Imprimo el contenido de ptr
// recordemos que es la direccion de memoria de N.

printf(”El valor de N es %.1f\n”, *ptr); // Imprimo el CONTENIDO de la
// DIRECCION a la que apunta ptr.


return 0;
}

Entonces, vimos que un puntero es una variable que almacena un numero entero, una direccion de la memoria, y ademas vimos que un puntero “apunta” a un tipo de dato en especial.
¿Cómo puedo utilizar un puntero?
Cuando utilizo funciones en C, por default, todos los parametros se pasan
por VALOR. Esto quiere decir, que mi función nunca trabaja sobre
la variable que yo le pase, sino sobre una copia en otra direccion de
memoria.
Cuando yo quiero pasar un parametro por REFERENCIA, utilizo un puntero.

Vamos a hacer una funcion que le sume 1 a un numero.
Podemos ver que no funciona como desearia.

//Prog 5.
#include // incluimos stdio.h

void sumaUno(int Num)
{
Num += 1;
printf(”Direccion de Num: %X \n”, &Num);
printf(”Valor de Num: %d \n”, Num);
}

int main(void)
{
int N = 5;
sumaUno(N);
printf(”Direccion de N: %X \n”, &N);
printf(”Valor de N: %d \n”, N);

return 0;
}

Podemos ver claramente que Num y N tienen dos direcciones de memoria
diferentes
, por lo que las modificaciones que hago en mi funcion nunca
van a tocar lo que hay en N.
Aca es donde entran en juego los punteros, voy a escribir la misma funcion
pero pasando el parametro por REFERENCIA, utilizando un puntero.

//Prog 6.
#include // incluimos stdio.h

void sumaUno(int *Num) // <– Esto quiere decir que espero recibir una DIRECCION de memoria como parametro.
{
*Num += 1;
printf(”Direccion de Num: %X \n”, Num);
printf(”Valor de Num : %d \n”, *Num);
}

int main(void)
{
int N = 5;
sumaUno(&N); // <— Pueden ver como paso la DIRECCION de N.
printf(”Direccion de N: %X \n”, &N);
printf(”Valor de N: %d \n”, N);

return 0;
}

Ahora mi programa funciona correctamente. Las modificaciones que hice fueron:

- Ahora el parametro Num, es un “puntero a entero” int *Num
Lo que quiere decir que en esta variable ya no se guarda el numero que quiero modificar, lo que se guarda realmente es la DIRECCION de memoria en donde se encuentra el numero que quiero modificar.

-Ahora para trabajar asignarle algo a Num debo seguir tratandolo como un
puntero, por eso pongo “*Num += 1;“. Recuerden que con esto le digo a la PC “toma el numero entero almacenado en la direccion de Num y sumale 1“

-Cuando imprimo la direccion de memoria de N no uso mas &, porque ahora
Num por si solo es una direccion de memoria (la de N), si utilizase el &
lo que haria es imprimir la direccion de memoria de Num.

-Cuando llamo a mi funcion sumaUno, utilizo & “sumaUno(&N);” esto quiere
decir que le estoy pasando a mi funcion la DIRECCION de N, para que trabaje
sobre ese espacio de memoria.



A continuacion voy a poner un esquema de la memoria de los dos programas
para que se pueda ver la diferencia.






Esto es el programa sin utilizar puntero...













Se copia el valor de N en NUM, y luego se le suma 1 a NUM, lo que nos da:













En la segunda version del programa, nuestro esquema es asi:













En Num esta almacenado “1″ que es la direccion de N.
Y ahora mi funcion le suma 1 a lo que se encuentra en la direccion de Num.














Espero que hasta acá haya quedado claro lo que es un puntero y cómo funciona.
Recuerdo que cuando nos enseñaron esto todos nos quedamos medio asi O.o
Pero es un concepto realmente importante, sobretodo en un lenguaje cómo C.



ARITMETICA DE PUNTEROS:
Ptr + n ==> (n * sizeof(tipoPtr))

Consideremos el siguiente codigo;
int *ptr;
*ptr = 5;


La primera linea declara una variable que guardara una direccion de memoria en la que habra un numero entero.

Y en la segunda linea, tomamos LO QUE HAY en la direccion de ptr y le asignamos 5, mediante el operador *.

Bien, una cosa que podemos hacer con los punteros es incrementarlos o decrementarlos. Pero me refiero a incrementar o decrementar la DIRECCION de memoria, no el valor al que apuntan.
Entonces, yo puedo incrementar un puntero al siguiente elemento del MISMO
TIPO
que se encuentre en la memoria.
Cuando yo incremento un puntero, por ejemplo con ptr++, no incremento 1 byte
a ptr, sino que incremento según el tipo de dato al que apunte ptr.
Si fuese un int lo incrementaria en 2 bytes, si fuese un float en 4 bytes. Veamos un
ejemplo.
Supongamos que tenemos esta porcion de memoria, donde ptr es un puntero que apunta a la dirección D3, y ejecutamos el siguiente código

int *ptr = 15;

Esto modificará el numero que había en D3 y pondrá un 15 en su lugar.

















Luego, ejecutamos la siguiente linea de código

ptr++;

Al incrementar un puntero, lo que hago es desplazarme en la memoria tantos bytes cómo tipo de dato sea el puntero por la cantidad que incrementé.
En este caso incremento el puntero en 1, entonces me desplazo 2 Bytes, ya que es int.














Entonces, ptr solía apuntar a una direccion en la que habia un numero 15.
Luego de hacer ptr++, cambio para apuntar hacia una direccion en la que
se almacenaba un 48.


Bueno, esto es todo por ahora.
Espero que hayan encontrado bien explicada la guía.
Espero que a alguien le sea de utilidad
Me despido, cualquier tipo de comentarios y criticas son bienvenidas.

No hay comentarios: