AnteriorPosterior

9.2. ¿Qué son los punteros?

  Curso: Fundamentos de programación en C, por Nacho Cabanes

9.2. ¿Qué son los punteros?

Un puntero no es más que una dirección de memoria. Lo que tiene de especial es que normalmente un puntero tendrá un tipo de datos asociado: por ejemplo, un “puntero a entero” será una dirección de memoria en la que habrá almacenado (o podremos almacenar) un número entero.

Vamos a ver qué símbolo usamos en C para designar los punteros:

int num; /* "num" es un número entero */
int *pos; /* "pos" es un "puntero a entero" (dirección de
   memoria en la que podremos guardar un entero) */

Es decir, pondremos un asterisco entre el tipo de datos y el nombre de la variable. Ese asterisco puede ir junto a cualquiera de ambos, también es correcto escribir

int* pos;

Esta nomenclatura ya la habíamos utilizado aun sin saber que era eso de los punteros. Por ejemplo, cuando queremos acceder a un fichero, hacemos

FILE* fichero;

Antes de entrar en más detalles, y para ver la diferencia entre trabajar con “arrays” o con punteros, vamos a hacer dos programas que pidan varios números enteros al usuario y muestren su suma. El primero empleará un “array” (una tabla, de tamaño predefinido) y el segundo empleará memoria que reservaremos durante el funcionamiento del programa.

El primero podría ser así:

/*---------------------------*/
/*  Ejemplo en C nº 71:      */
/*  C071.C                   */
/*                           */
/*  Sumar varios datos       */
/*  Version 1: con arrays    */
/*                           */
/*  Curso de C,              */
/*    Nacho Cabanes          */
/*---------------------------*/
 
#include <stdio.h>
 
int main() {
  int datos[100];  /* Preparamos espacio para 100 numeros */      
  int cuantos;     /* Preguntaremos cuantos desea introducir */
  int i;           /* Para bucles */
  long suma=0;     /* La suma, claro */
 
  do {
    printf("Cuantos numeros desea sumar? ");
    scanf("%d", &cuantos);
    if (cuantos>100)  /* Solo puede ser 100 o menos */
      printf#40;"Demasiados. Solo se puede hasta 100.");
  } while (cuantos>100);  /* Si pide demasiado, no le dejamos */
 
  /* Pedimos y almacenamos los datos */
  for (i=0; i<cuantos; i++) {
    printf("Introduzca el dato número %d: ", i+1);
    scanf("%d", &datos[i]);
  }
 
  /* Calculamos la suma */
  for (i=0; i<cuantos; i++) 
    suma += datos[i];
 
  printf("Su suma es: %ld\n", suma);
 
  return 0;
}
 

Los más avispados se pueden dar cuenta de que si sólo quiero calcular la suma, lo podría hacer a medida que leo cada dato, no necesitaría almacenar todos. Vamos a suponer que sí necesitamos guardarlos (en muchos casos será verdad, si los cálculos son más complicados). Entonces nos damos cuenta de que lo que hemos estado haciendo hasta ahora no es eficiente:

  • Si quiero sumar 1000 datos, o 500, o 101, no puedo. Nuestro límite previsto era de 100, así que no podemos trabajar con más datos.
  • Si sólo quiero sumar 3 números, desperdicio el espacio de 97 datos que no uso.
  • Y el problema sigue: si en vez de 100 números, reservamos espacio para 5000, es más difícil que nos quedemos cortos pero desperdiciamos muchísima más memoria.

La solución es reservar espacio estrictamente para lo que necesitemos, y eso es algo que podríamos hacer así:

/*---------------------------*/
/*  Ejemplo en C nº 72:      */
/*  C072.C                   */
/*                           */
/*  Sumar varios datos       */
/*  Version 2: con punteros  */
/*                           */
/*  Curso de C,              */
/*    Nacho Cabanes          */
/*---------------------------*/
 
#include <stdio.h>
#include <stdlib.h>
 
int main() {
  int* datos;      /* Necesitaremos espacio para varios numeros */      
  int cuantos;     /* Preguntaremos cuantos desea introducir */
  int i;           /* Para bucles */
  long suma=0;     /* La suma, claro */
  do {
    printf("Cuantos numeros desea sumar? ");
    scanf("%d", &cuantos);
    datos = (int *) malloc (cuantos * sizeof(int));
    if (datos == NULL)  /* Si no hay espacio, avisamos */
      printf("No caben tantos datos en memoria.");
  } while (datos == NULL);  /* Si pide demasiado, no le dejamos */
 
  /* Pedimos y almacenamos los datos */
  for (i=0; i<cuantos; i++) {
    printf("Introduzca el dato número %d: ", i+1);
    scanf("%d", datos+i);
  }
 
  /* Calculamos la suma */
  for (i=0; i<cuantos; i++) 
    suma += *(datos+i);
 
  printf("Su suma es: %ld\n", suma);
  free(datos);
 
  return 0;
}
 

Este fuente es más difícil de leer, pero a cambio es mucho más eficiente: funciona perfectamente si sólo queremos sumar 5 números, pero también si necesitamos sumar 120.000 (y si caben tantos números en la memoria disponible de nuestro equipo, claro).

Vamos a ver las diferencias:

En primer lugar, lo que antes era int datos[100] que quiere decir “a partir de la posición de memoria que llamaré datos, querré espacio para a guardar 100 números enteros”, se ha convertido en int* datos que quiere decir “a partir de la posición de memoria que llamaré datos voy a guardar varios números enteros (pero aún no sé cuantos)”.

Luego reservamos el espacio exacto que necesitamos, haciendo datos = (int *) malloc (cuantos * sizeof(int)); Esta orden suena complicada, así que vamos a verla por partes:

  • “malloc” es la orden que usaremos para reservar memoria cuando la necesitemos (es la abreviatura de las palabra “memory” y “allocate”).
  • Como parámetro, le indicamos cuanto espacio queremos reservar. Para 100 números enteros, sería “100*sizeof(int)”, es decir, 100 veces el tamaño de un entero. En nuestro caso, no son 100 números, sino el valor de la variable “cuantos”. Por eso hacemos “malloc (cuantos*sizeof(int))”.
  • Para terminar, ese es el espacio que queremos reservar para nuestra variable “datos”. Y esa variable es de tipo “int *” (un puntero a datos que serán números enteros). Para que todo vaya bien, debemos “convertir” el resultado de “malloc” al tipo de datos correcto, y lo hacemos forzando una conversión como vimos en el apartado 2.4 (operador “molde”), con lo que nuestra orden está completa:
    datos = (int *) malloc (cuantos * sizeof(int));
  • Si “malloc” nos devuelve NULL como resultado (un “puntero nulo”), quiere decir que no ha encontrado ninguna posición de memoria en la que nos pudiera reservar todo el espacio que le habíamos solicitado.
  • Para usar “malloc” deberemos incluir “stdlib.h” al principio de nuestro fuente.

La forma de guardar los datos que teclea el usuario también es distinta. Cuando trabajábamos con un “array”, hacíamos scanf("%d", &datos[i]) (“el dato número i”), pero con punteros usaremos scanf("%d", datos+i) (en la posición datos + i). Ahora ya no necesitamos el símbolo “ampersand” (&). Este símbolo se usa para indicarle a C en qué posición de memoria debe almacenar un dato. Por ejemplo, float x; es una variable que podremos usar para guardar un número real. Si lo hacemos con la orden “scanf”, esta orden no espera que le digamos en qué variable deber guardar el dato, sino en qué posición de memoria. Por eso hacemos scanf("%f", &x); En el caso que nos encontramos ahora, int* datos ya se refiere a una posición de memoria (un puntero), por lo que no necesitamos & para usar “scanf”.

Finalmente, la forma de acceder a los datos también cambia. Antes leíamos el primer dato como datos[0], el segundo como datos[1], el tercero como datos[2] y así sucesivamente. Ahora usaremos el asterisco (*) para indicar que queremos saber el valor que hay almacenado en una cierta posición: el primer dato será *datos, el segundo *(datos+1), el tercero será *(datos+2) y así en adelante. Por eso, donde antes hacíamos suma += datos[i]; ahora usamos suma += *(datos+i);

También aparece otra orden nueva: free. Hasta ahora, teníamos la memoria reservada estáticamente, lo que supone que la usábamos (o la desperdiciábamos) durante todo el tiempo que nuestro programa estuviera funcionando. Pero ahora, igual que reservamos memoria justo en el momento en que la necesitamos, y justo en la cantidad que necesitamos, también podemos volver a dejar disponible esa memoria cuando hayamos terminado de usarla. De eso se encarga la orden “free”, a la que le debemos indicar qué puntero es el que queremos liberar.

Actualizado el: 27-07-2014 14:09

AnteriorPosterior