LABORATORIO 09: PUNTEROS


9.1 Introducción
9.2 ¿Cómo funcionan los punteros?
9.3 Utilización de punteros
9.4 Restricciones en el uso de punteros
9.5 Indirección Múltiple
9.6 Funciones de asignación dinámica, malloc() y free()
9.7 Utilización de punteros con constantes de cadena

9.1 Introducción

Un puntero es un tipo de variable usada para almacenar la direccion en memoria de otra variable, en lugar de un dato convencional. Mediante la variable de tipo puntero accedemos a esa otra variable, almacenada en la direccion de memoria que señala el puntero.

Es decir, el valor de la variable de tipo puntero es una direccion de memoria. Se dice que el puntero apunta o se¤ala a la variable almacenada en la direccion de memoria que contiene el puntero. Lo que nos interesa es el dato contenido en esa variable apuntada. No hay que confundir la variable apuntada con el puntero.

Para entender qué es un puntero veremos primero cómo se almacenan los datos en un ordenador.

La memoria de un ordenador está compuesta por unidades básicas llamadas bits. Cada bit sólo puede tomar dos valores, normalmente denominados alto y bajo, ó 1 y 0. Pero trabajar con bits no es práctico, y por eso se agrupan.

Cada grupo de 8 bits forma un byte u octeto. En realidad el microprocesador, y por lo tanto nuestro programa, sólo puede manejar directamente bytes o grupos de dos o cuatro bytes. Para acceder a los bits hay que acceder antes a los bytes. Cada byte tiene una dirección, llamada normalmente dirección de memoria.

La unidad de información básica es la palabra, dependiendo del tipo de microprocesador una palabra puede estar compuesta por dos, cuatro, ocho o dieciséis bytes. Hablaremos en estos casos de plataformas de 16, 32, 64 ó 128 bits. Se habla indistintamente de direcciones de memoria, aunque las palabras sean de distinta longitud. Cada dirección de memoria contiene siempre un byte. Lo que sucederá cuando las palabras sean de 32 bits es que accederemos a posiciones de memoria que serán múltiplos de 4.

Todo esto sucede en el interior de la máquina. Podemos saber qué tipo de plataforma estamos usando averiguando el tamaño del tipo int, y para ello hay que usar el operador "sizeof()", por ejemplo:

printf("Plataforma de %d bits", 8*sizeof(int));

 

9.2 ¿Cómo funcionan los punteros?

Un puntero es un tipo especial de variable que contiene una dirección de memoria. Por ejemplo, si una variable llamada p contiene la dirección de otra variable llamada q, entonces se dice que p apunta a q. Por supuesto, a partir de esa dirección de memoria puede haber cualquier tipo de objeto: un char, un int, un float, un array, una estructura, una función u otro puntero. Seremos nosotros los responsables de decidir ese contenido.

Podemos considerar la memoria del ordenador como una gran tabla, de modo que podemos acceder a cada celda de memoria a través de un índice. Podemos considerar que la primera posición de la tabla es la 0 y esa posición de memoria contiene un valor. Sí tenemos una variable Indice con un valor de 0, se puede usar esa variable para acceder a la posición 0 de la memoria.

 

9.3 Uso de punteros

Para declarar una variable de puntero en lenguaje C/C++, se utiliza esta forma general:

tipo  *nombre_de_variable;

Aquí, tipo es el tipo base del puntero. El tipo de base especifica el tipo del objeto al que puede apuntar el puntero. Observe que un asterisco precede al nombre de la variable. Esto le indica a la computadora que se está creando una variable puntero. Por ejemplo, la siguiente sentencia crea un apuntador a un entero:

int * p

C contiene dos operadores de puntero especiales: * y &. El operador & devuelve la dirección de la variable a la que precede. El operador * devuelve el valor almacenado en la dirección de memoria a la que precede (el operador de puntero * no guarda ninguna relación con el operador de multiplicación).

Actividad 1. Implemente el siguiente código en lenguaje C:

#include <stdio.h>

main()
{
    int *p, q; 
    q = 100;
    p = &q;
    printf("%d",*p);//1.1 que valor imprime esta instrucción?. Explique.
    printf("%d",q);//1.2 que valor imprime esta instrucción?. Explique.
    printf("%d",p);//1.3 que valor imprime esta instrucción?. Explique

}

Es posible utilizar el operador * al lado izquierdo de una sentencia de asignación para asignar un nuevo valor a una variable utilizando un puntero a ella. Por ejemplo este programa asigna indirectamente un valor a q utilizando el puntero q:

Actividad 2. Implemente el siguiente código en lenguaje C:

#include <stdio.h>

main()
{
    int *p, q; 
    p = &q;
    *p = 199;
    printf("%d",*p);//2.1 que valor imprime esta instrucción?. Explique.
    printf("%d",q);//2.2 que valor imprime esta instrucción?. Explique.
    printf("%d",p);//2.3 que valor imprime esta instrucción?. Explique.

}

En los dos programas anteriores no hay razón para utilizar punteros. Sin embargo, en temas posteriores, entenderá porque son importantes los punteros. Los punteros se utilizan comunmente para implementar estructuras de datos como listas enlazadas y árboles binarios.

9.4 Restricciones en el uso de punteros.

El compilador de C utiliza el tipo base del puntero para determinar cúantos bytes debe copiar cuando se hace una asignación indirecta, o cuántos bytes comparar cuando se hace una comparación indirecta. Por eso, es muy importante que se utilice el tipo base apropiado para un puntero. No se debe utilizar un puntero de un tipo para apuntar a una variable de otro tipo, dado que se puede presentar sobreescritura de datos en memoria y por consiguiente pérdida de información, lo cual puede conllevar efectos negativos sobre el ordenador.

Si intenta utilizar un puntero antes de asignarle la dirección de una variable, probablemente se producirá un fallo en el programa. Recuerde que la declaración de una variable puntero simplemente crea una variable capaz de contener una dirección de memoria.

Además de los operadores * y &, solamente hay otros cuatro operadores que se pueden aplicar a las variables puntero: los operadores aritméticos +, ++, - y --. Lo que es más, solamente se pueden sumar o restar cantidades enteras. La aritmética de punteros difiere de la aritmética normal, en que las operaciones dependen del tipo base del puntero. Por ejemplo, suponiendo que un puntero a entero llamado p contiene la dirección 200, después de ejecutar la sentencia:

p++;

p tendrá el valor 202, suponiendo que los enteros tienen una representación de 2 bytes.

Es posible sumar o restar cualquier cantidad entera a una variable puntero. Las demás operaciones aritméticas no se pueden realizar sobre variables de tipo puntero.

Actividad 3. Implemente el siguiente código en lenguaje C:

#include <stdio.h>

main()
{
    char *cp, ch; 
    int *ip, i;
    float *fp, f;
    double *dp, d;     
    cp = &ch;
    ip = &i;
    fp = &f;
    dp = &d;
    printf("%p %p %p %p ", cp, ip, fp, dp);
    cp++;
    ip++;
    fp++;
    dp++;
    printf("%p %p %p %p ", cp, ip, fp, dp);
    //3.1 Cuántos bytes tiene el tipo base char?
    //3.2 Cuántos bytes tiene el tipo base int?
    //3.3 Cuántos bytes tiene el tipo base float?
    //3.4 Cuántos bytes tiene el tipo base double?

}

9.5 Indirección Múltiple

Se puede hacer que un puntero apunte a otro puntero que apunte a un valor de destino. Esta situación se denomina indirección múltiple o punteros a punteros. Una variable que es puntero a puntero tiene que declararse como tal. Esto se hace colocando un * adicional en frente del nombre de la variable. Por ejemplo, la siguiente declaración inicial indica al compilador que ptr es un puntero a puntero de tipo float:

float **ptr;

Es importante entender que ptr no es puntero a flotante, sino un puntero a un puntero flotante.

9.6 Funciones de asignación dinámica, malloc() y free()

Los punteros proporcionan el soporte necesario para el potente sistema de asignación dinámica de memoria de C. La asignación dinámica es la forma en la que un programa puede obtener memoria mientras se está ejecutando.

El centro del sistema de asignación dinámica está compuesto por las funciones (existentes en la biblioteca stdlib.h) malloc(), que asigna memoria; y free() que la devuelve.

Tras una llamada fructífera, malloc() devuelve un puntero, el primer byte de memoria dispuesta. Si no hay suficiente memoria libre para satisfacer la petición de malloc(), se da un fallo de asignación y devuelve un nulo. El fragmento de código que sigue asigna 1000 bytes de memoria:
char *p;
p = (char *) malloc(1000);

Después de la asignación, p apunta al primero de los 1000 bytes de la memoria libre. El siguiente ejemplo dispone espacio para 50 enteros. Obsérvese el uso de sizeof para asegurar la portabilidad: p apunta al primero de los 1000 bytes de la memoria libre. El siguiente ejemplo dispone espacio para 50 enteros. Obsérvese el uso de sizeof para asegurar la portabilidad:
int *p;
p= (int *) malloc(50*sizeof(int));

La función free() es la opuesta de malloc() porque devuelve al sistema la memoria previamente asignada. Una vez que la memoria ha sido liberada, puede ser reutilizada en una posterior llamada a malloc(). El prototipo de la función free() es:
void free (void *p);
free(p);

9.7 Utilización de punteros con constantes de cadena

C permite que se utilicen constantes de cadena encerradas entre dobles comillas en un programa. Cuando el compilador encuentra una cadena así, la almacena en la tabla de cadenas del programa y genera un puntero a la cadena. El siguiente programa es un ejemplo del uso de un apuntador a carácter para almacenar una cadena:

#include <stdio.h>

main()

   char *p;
   p "uno dos tres";//En este caso la p
   printf(p);
}

 

Actividad 4. Muestre como se puede hacer más eficiente este programa:

#include <stdio.h>

main()

   char str[80];
   int i, espacios;
   printf("Introduzca una cadena");
   gets(str);
   espacios = 0;
   for (i=0; str[i]; i++)
      if (str[i]==' ') espacios++;
   printf("Número de espacios: %d", espacios);
}


INGENIERO NESTOR DIAZ
FIET - UNICAUCA
2004