Introducción a la programación

Antes de explicar qué son, para qué sirven y cómo hacer uso de ellos, vamos primero a exponer unas cuantas consideraciones:

Los punteros no están disponibles en todos los lenguajes de programación, ello significa que si, por ejemplo, programais en C, podeis utilizarlos, pero si programais en Basic no.

Su uso produce unas ciertas ventajas sobre los programas, y es que aumentan la flexibilidad a la hora de, por ejemplo, declarar vectores o matrices cuyo tamaño no sea predefinido. No acaban ahí las ventajas, con estructuras de datos más complejas son el pan nuestro de cada día, pero de todo eso ya hablaremos.

Sin embargo, no todo es maravilloso, y tienen serios inconvenientes que provocan más de un quebradero de cabeza. Es muy fácil equivocarse usándolos y con ello escribir en alguna zona de memoria no aconsejable (como las reservadas por el sistema operativo), lo que provoca el consiguiente cuelgue en sistemas poco robustos que permitan escribir en cualquier parte. No hay nada más peligroso que un puntero incontrolado o sin inicializar.

Vistas las pegas (ya las descubrireis cuando el programa se os cuelgue sin motivo aparente), vamos a ver qué es eso de los punteros, pues parece que tenemos que tenerles un cierto respeto :-D

. Qué es un puntero

Intuitivamente, un puntero es una flecha que apunta a alguna parte. ¿A qué parte? Obviamente, si estamos hablando de ordenadores, apuntará a una cierta dirección de memoria. Es decir, un puntero es una representación simbólica de una dirección de memoria.

(Nota importante: voy a usar la notación de C para los punteros)

Veamos un ejemplo; supongamos que tenemos declarada la variable Variable_Misteriosa en nuestro programa. Si queremos saber cuál es la dirección de dicha variable, pondremos lo siguiente:

  &Variable_Misteriosa
  { el símbolo & precediendo a una variable es el
    operador dirección de memoria } 

y si, por ejemplo, escribimos en nuestro programa:

Mostrar_por_Pantalla(&Variable_Misteriosa)

saldrá en pantalla (por decir algo):

56743

que no es más que la celdilla de la memoria en la que se almacena el valor que nosotros identificamos con Variable_Misteriosa. Es decir, si hemos hecho previamente (suponiendo que fuera una variable entera):

Variable_Misteriosa <- 9

lo que significa es que en la dirección de memoria 56743 está almacenado el valor 9. ¿Está todo claro hasta aquí? Bien, pues sigamos :)

En el caso que he puesto como ejemplo, lo que teníamos era una *constante* puntero. Pero también podemos declarar como variables, variables de tipo puntero. Estas variables contendrán, como hemos dicho, una dirección de memoria.

Por ejemplo, si tenemos una variable puntero que se llame Mi_Puntero, y una variable normal que se llame Mi_Variable, podemos hacer cosas como esta:

Mi_Puntero <- &Mi_Variable

con lo que en Mi_Puntero tenemos almacenada la dirección de memoria de Mi_Variable (y decimos que Mi_Puntero APUNTA a Mi_Variable). Y aquí surge el primer problema. Si hacemos:

Mi_Puntero <- Mi_Variable

en Mi_Puntero está almacenada, como dirección de memoria, el valor de la variable Mi_Variable. Si luego hemos de escribir algo en la zona de memoria a la que apunta Mi_Puntero, ya la hemos liado, puesto que no sabemos qué puede haber ahí.

Resumiendo: no es lo mismo la dirección de memoria de una variable que el contenido de la variable (o, lo que es lo mismo, el contenido de esa dirección de memoria).

Seguimos. Como hemos dicho que Mi_Puntero es una variable de tipo puntero, podemos hacer, más adelante en el curso del programa, que apunte a otra variable de la misma forma.

Ya sabemos que Mi_Puntero apunta a Mi_Variable. En este caso, podemos utilizar el operador de indirección * (también es de C; no confundirlo con el operador de multiplicación) para encontrar el valor almacenado en Mi_Variable. ¿Cómo? Escribiendo (por ejemplo):

 Mi_Puntero <- &Mi_Variable
 Mostrar_por_Pantalla(*Mi_Puntero) 

es exactamente lo mismo que hacer:

Mostrar_por_Pantalla(Mi_Variable)

Lo que hemos hecho ha sido apuntar a Mi_Variable con Mi_Puntero (es decir, Mi_Puntero contiene la dirección de memoria de Mi_Variable) y después, con el operador *, mostrar el CONTENIDO de lo que hay en la dirección de memoria que guarda Mi_Puntero.

Es decir, lo que hace el operador de indirección es, seguido de un puntero, dar el valor almacenado en la dirección de memoria a la que apunta el puntero.

Sé que todo esto es un trabalenguas de cuidado, así que releed con cuidado la primera parte del texto y seguidle bien la pista. Haced algún dibujo si eso os ayuda.

Mientras, yo prosigo con el punto siguiente:

. Declaración de punteros

He dicho antes que podemos tener variables de tipo puntero, así que, lo lógico es querer saber cómo declararlas, y a eso es a lo que vamos.

Cuando declárabamos (hace ya mucho tiempo) variables de tipo entero, poníamos:

a,b,c : ENTEROS

¿No podríamos poner para los punteros algo como:?

a,b,c: PUNTEROS

Bien, pues la respuesta es NO. ¿Y por qué no? Pues porque para declarar un puntero necesitamos saber, aparte de que va a apuntar a alguien, a qué TIPO de alguien va a apuntar, es decir, a qué tipo de variable va a apuntar. No será lo mismo apuntar a un entero que a un caracter o que a un real, pues estos tipos ocupan distintos tamaños en memoria, y eso es algo fundamental para otra cosa que veremos más adelante, la aritmética de punteros.

Para declarar una variable de tipo puntero a un tipo de dato, lo haremos como sigue:

  *Puntero1            : CARACTER
  *Puntero2            : ENTERO
  *Puntero3, *Puntero4 : REAL 

(esto en C lo haríamos así: char *Puntero1; int *Puntero2; float *Puntero3, *Puntero4 ;)

y tenemos que Puntero1 será una variable puntero que apunte a una variable de tipo caracter, Puntero2 apuntará a una variable de tipo entero y Puntero3 y Puntero4 apuntarán a variables de tipo real.

Ahora que los tenemos declarados, suponiendo que tengamos las variables Var1 de tipo caracter, Var2 de tipo entero y Var3 de tipo real, podemos inicializarlos haciendo lo que ya hemos visto:

  Puntero1 <- &Var1
  Puntero2 <- &Var2
  Puntero3 <- &Var3

y acceder a sus contenidos escribiendo:

  *Puntero1
  *Puntero2
  *Puntero3

sin embargo, si se nos ocurre usar en el programa:

*Puntero4

puede pasar de todo. ¿Por qué? Pues porque este puntero no está inicializado (lo he dejado a drede). Eso significa que, en principio, puede contener cualquier cosa. Al ser un puntero, esa ''cualquier cosa'' será interpretada como una dirección de memoria, y el contenido de ''cualquier dirección de memoria'' puede ser de lo más insólito, lo que no es muy recomendado si, por ejemplo, estamos haciendo cálculos.

Una cosa que no hay que perder de vista es que los punteros, al ser variables, tienen una posición de memoria en la que se guarda su contenido. Es decir, si hacemos:

&Puntero1

estamos accediendo a la dirección de memoria del puntero Puntero1. Y el contenido de esa dirección de memoria es la dirección de memoria de la variable a la que apunta.

. Aritmética de Punteros

Trabajando con punteros, ¿qué sentido tiene hacer:?

Puntero <- Puntero + 1

Pues eso depende de a qué tipo de variable esté apuntando la variable Puntero. Si Puntero apunta a un carácter, como un carácter ocupa 1 byte en memoria, al hacer la operación anterior, Puntero está apuntando al byte siguiente al que apuntaba antes. Si la operación que hacemos es:

Puntero <- Puntero - 1

lo que hace es apuntar al byte anterior. Sin embargo, si Puntero apunta a un real, como un real ocupa en memoria 10 bytes, en este caso apunta a los 10 bytes anteriores al que estaba apuntando.

En otras palabras, cuando sumamos 1 a un puntero, no estamos diciéndole que apunte a la dirección siguiente, sino que pase a apuntar a la siguiente celdilla de memoria de acuerdo con el tipo base al que apunta.

Y si en vez de sumar (o restar) 1, sumamos (o restamos) N, avanzamos N veces lo que ocupe el tipo de variable al que estamos apuntando.

Además, también podemos sumar y restar punteros. Veamos unos ejemplos para aclararlo. Supongamos que Puntero1 y Puntero2 son punteros a enteros, y hacemos:

Puntero2 <- Puntero1 + 4

entonces, Puntero2 apunta a la posición de memoria 8 bytes posterior a la que apunta Puntero1. Y si hacemos:

Puntero2 <- Puntero1 - 1

Puntero2 apunta a la posición de memoria 2 bytes anterior a la que apunta Puntero1. También podemos hacer:

i <- Puntero2 - Puntero1

Esto normalmente se hace dentro de un mismo array, para saber cuántos elementos los separan. Notar que el resultado que da no es en bytes, sino en las mismas unidades que el tamaño del tipo del array. Este resultado debe ser asignado a una variable de tipo entero (la variable i que aparece en el ejemplo se presupone previamente declarada).

. ¿Y todo esto para qué sirve?

Hablar de punteros por hablar puede ser muy entretenido si no se tiene nada mejor que hacer, pero resulta que yo he dicho que son muy útiles y no lo he dicho gratuitamente.

Por ejemplo, ahora que sabemos lo que son los punteros, vamos a ver que están relacionados con el paso de parámetros en las funciones, con lo que hablaremos de paso de variables por valor y por referencia.

En su día hablamos de funciones y de procedimientos, y dijimos que, en el caso de las funciones, pasábamos una serie de variables que no eran modificadas, y se nos devolvía un único valor. Por contra, en los procedimientos pasábamos una serie de variables que podían ser modificadas pero, a cambio, no se nos devolvía ningún valor.

Esto es así en lenguajes como el Pascal, pero en C esto no sucede. Quiero decir, que en C no tenemos funciones y procedimientos, sino que sólo tenemos funciones. Las funciones siguen teniendo la característica de que devuelven un único valor, y que las variables que le pasamos como argumentos no pueden ser modificadas. ¿Y si necesito que se me devuelvan dos valores? ¿Cómo lo hago, si el C sólo me permite funciones?

Pues lo hago con lo que se llama ''paso de parámetros por referencia''.

En primer lugar, he de decir que en C, cuando pasamos variables a las funciones, lo que hacemos es un ''paso de parámetros por valor''. Es decir, la función recibe los valores de las variables, pero no sabe nada más de ellas.

Cuando hacemos un paso por referencia, lo que estamos pasando a la función no es el valor de la variable, sino la dirección de memoria. Y la función, al tener la dirección de memoria de la variable, ya sí puede modificarla, pudiendo, en cierto sentido, ''devolvernos'' varios valores, solucionando el problema que teníamos al no disponer de procedimientos.

Voy a poner un ejemplo de esto comentado para que se entienda la estructura y el por qué. Y para ello, voy a utilizar un ejercicio que ya propuse, el de escribir un procedimiento que intercambie el valor de dos variables. El algoritmo de este procedimiento es algo tan sencillo como esto:

  PROCEDIMIENTO Intercambia(X,Y: ENTEROS)
  variables
    auxiliar: ENTERO
 
  inicio
 
  auxiliar <- X
  X <- Y
  Y <- auxiliar
 
  fin 

y lo podemos llamar desde cualquier punto del programa principal sin más que poner

LLAMAR_A Intercambia(Un_Valor,Y_Otro_Valor)

Sin embargo, como ya he comentado, en C no tenemos procedimientos, ¿cómo salvamos este escollo?. Pues lo salvamos con una función como la siguiente:

  void Intercambia(int *x,int *y)  {
   int aux;
   aux=*x; *x=*y; *y=aux;
   } 

y llamamos a esta función desde el programa principal como sigue:

  int main(void) {
   int a=2,b=3;
 
   printf("\nPrimero, a=%d y b=%d\n",a,b);
   Intercambia(&a,&b);
   printf("\npero ahora, a=%d y b=%d\n",a,b);
   return 0;
   } 

Veamos cosas: para empezar, al llamar a la función Intercambia, no le hemos pasado los valores de las variables (paso por valor), sino que le hemos pasado las direcciones de las variables (paso por referencia). Así pues, los parámetros formales de la función Intercambia están recibiendo direcciones de memoria, por lo que deben declararse como punteros, cosa que hemos hecho en la cabecera de la función, indicando que recibe dos punteros.

Ahora veamos la función: en primer lugar, creo la variable auxiliar necesaria para hacer el cambio. Sin embargo, en vez de hacer:

aux <- x

hago:

aux <- *x

es decir, asigno a aux el contenido de la dirección de memoria a la que apunta x. Como, al llamar la función, hemos pasado unas direcciones de memoria, *x da como resultado el contenido de la dirección de memoria donde se guarda la variable a.

Si hubiera hecho:

aux <- x

en aux tendré almacenada la posición de memoria de la variable a y no su contenido, que es lo que yo quiero.

A continuación hacemos:

*x <- *y

es decir, al contenido de la posición de memoria a la que apunta x se le asigna el contenido de la posición de memoria a la que apunta y.

Y ya termina el intercambio con:

*y <- aux

con lo que al contenido de la posición de memoria a la que apunta y se le asigna el valor de la variable aux, que era el contenido de la posición de memoria a la que apuntaba x originalmente, en otras palabras, el valor de la variable a.

Si tras leer con calma este punto un par de veces os perdeis, me dejais una nota con las líneas que os parezcan más oscuras, porque esto es un buen trabalenguas mental O:)

La otra gran utilidad es la reserva dinámica de memoria. Si quereis, puedo dar aquí unos pequeños esbozos, pero eso ya quedaría para otro capítulo. Creo que con 400 líneas sobre punteros, para empezar, ya teneis bastante O:)

COMPARTE ESTE ARTÍCULO

ENVIAR A UN AMIGO
COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN GOOGLE +
SIGUIENTE ARTÍCULO

HAY 2 COMENTARIOS
  • Anónimo dijo:

    Muy pero Muy interesante el articulo, solo que tengo una critica. Explican todo muy bien, pero donde se supone que tenemos que hacer los ejercisios, en que programa!!! en un notepad?? en un Word¡?¡ en un Exel?? en Dreamweaver?, en una hoja?? Aguardo su respuesta!! Gracias.

  • [email protected] dijo:

    En fortran !Ordenacionde vectores por el metodo de la Burbuja Program Ordena_burbuja integer,dimension(100)::x integer::i,j,k,n integer::temp print*,"Ingrese la cantidad de datos al vector" read*,n print*,"Ingrese datos al vector" read*,(x(i),i=1,n) print* !usando el algoritmo de ordenacion por la burbuja Do i=1,n-1 Do j=1,n-1 if (x(j)>x(j+1)) then temp=x(j) x(j)=x(j+1) x(j+1)=temp end if end do end do Print*,"Vector ordenado" Print*,(x(k),k=1,n) end program ordena_burbuja !Ordenacionde vectores por el metodo de seleccion Program Ordena integer,dimension(100)::x integer::i,j,min,n integer::temp print*,"Ingrese la cantidad de datos al vector" read*,n print*,"Ingrese datos al vector" read*,(x(i),i=1,n) print* !usando el algoritmo de ordenacion por seleccion Do i=1,n-1 min=i Do j=i+1,n if (x(j)<x(min)) then min=j end if temp=x(i) x(i)=x(min) x(min)=temp end do end do Print*,"Vector ordenado" Print*,(x(i),i=1,n) end program ordena atte William

Conéctate o Regístrate para dejar tu comentario.