Ads 468x60px

Perfil

lunes, 17 de octubre de 2011

Punteros


El tipo de datos más característico del C son los punteros. Un puntero contiene un valor que es la dirección en memoria de un dato de cierto tipo. Los punteros se emplean en C para muchas cosas, por ejemplo recorrer vectores, manipular estructuras creadas dinámicamente, pasar parámetros por referencia a funciones, etc.

Cuando se declara una variable, se reserva un espacio en la memoria para almacenar el valor de la variable.
Ese espacio en memoria tiene una dirección.
Un puntero es una dirección dentro de la memoria, o sea, un apuntador a donde se encuentra una variable.

7.1 Operaciones básicas

Declaración

Los punteros se declaran con un asterisco, de esta forma:

tipo * variable;

Por ejemplo:

         int* puntero;

Se dice que la variable puntero es un puntero a enteros (apunta a un entero).

Asignación

El valor que puede adquirir un puntero es, por ejemplo, la dirección de una variable.
El operador & devuelve la dirección de una variable:

         puntero = &variable;

Desreferencia de un puntero

Se puede alterar la variable a la que apunta un puntero.
Para ello se emplea el operador de desreferencia, que es el asterisco:

*puntero = 45;

En este caso, se está introduciendo un 45 en la posición de memoria a la que apunta puntero.

7.2 Ejemplo de uso


         {
            int* puntero;
            int variable;
            
            puntero = &variable;
            *puntero = 33;  /* mismo efecto que variable=33 */
         }


Varios punteros pueden apuntar a la misma variable:

int* puntero1;
int* puntero2;
int var;
 
puntero1 = &var;
puntero2 = &var;
*puntero1 = 50;           /* mismo efecto que var=50  */
var = *puntero2 + 13;     /* var=50+13 */

7.3 Otros usos

Declaración múltiple de punteros


Si en una misma declaración de variables aparecen varios punteros, hay que escribir el asterisco a la izquierda de cada uno de ellos:

         int *puntero1, var, *puntero2;

Se declaran dos punteros a enteros ( puntero1 y puntero2) y un entero ( var).

El puntero nulo


El puntero nulo es un valor especial de puntero que no apunta a ninguna parte. Su valor es cero.
En <stdio.h> se define la constante NULL para representar el puntero nulo.

7.4 Parámetros por referencia a funciones


En C todos los parámetros se pasan por valor. Esto tiene en principio dos inconvenientes:
  • No se pueden modificar variables pasadas como argumentos
  • Si se pasa como parámetro una estructura, se realiza un duplicado de ella, con lo que se pierde tiempo y memoria

Sin embargo, se puede pasar un puntero como argumento a una función. El puntero no se puede alterar, pero sí el valor al que apunta:

void incrementa_variable (int* var)
{
   (*var)++;
}
 
main()
{
   int x = 1;
   incrementa_variable (&x);       /* x pasa a valer 2 */
}

En el ejemplo anterior, habia que poner paréntesis en (*var)++ porque el operador ++ tiene más precedencia que la desreferencia (el asterisco). Entonces *var++ sería como escribir *(var++), que no sería lo que queremos.

7.5 Precauciones con los punteros

Punteros no inicializados

Si se altera el valor al que apunta un puntero no inicializado, se estará modificando cualquier posición de la memoria.

main()
{
         int* puntero;
         *puntero = 1200; /* Se machaca una zona
                                               cualquiera de la memoria */
}


Confusión de tipos

Un puntero a un tipo determinado puede apuntar a una variable de cualquier otro tipo. Aunque el compilador lo puede advertir, no es un error.
(Afortunadamente, esto no ocurre en C++).

main()
{
         int p;
         double numero;
         int* puntero;
         
         p = &numero;     /* incorrecto, 
   pero el compilador no aborta */
         *p = 33;         /* Un desastre */
}

Punteros a variables locales fuera de ámbito

Si un puntero apunta a una variable local, cuando la variable desaparezca el puntero apuntará a una zona de memoria que se estará usando para otros fines. Si se desreferencia el puntero, los resultados son imprevisibles y a menudo catastróficos.

main()
{
         int* puntero;
 
         while (...)
         {
           int local;
           puntero = &local;       
  /* ‘puntero’ apunta a una variable local */
           ...
           *puntero = 33;          /* correcto */
         }
         ...
 
/* ahora ‘puntero’ apunta a una zona de memoria
   inválida */
         puntero = 50; /* catástrofe */
}

7.6 Aritmética de punteros

Un puntero apunta a una dirección de memoria.
El lenguaje C permite sumar o restar cantidades enteras al puntero, para que apunte a una dirección diferente: aritmética de punteros .
Consideremos un puntero a enteros:

         int* ptr;
ptr apuntará a cierta dirección de memoria:








Pero también tendrán sentido las expresiones ptr+1, ptr+2, etc. La expresión ptr+k es un puntero que apunta a la dirección de ptr sumándole k veces el espacio ocupado por un elemento del tipo al que apunta (en este caso un int):




















Ejemplo de aritmética de punteros


int vector [100]; /* un vector de enteros */
int *ptr;                          /* un puntero a enteros */
...
ptr = &vector[0]; /* ptr apunta al principio del vector */
*ptr = 33;                /* igual que vector[0] = 33 */
*(ptr+1) = 44;            /* igual que vector[1] = 44 */
*(ptr+2) = 90;            /* igual que vector[2] = 90 */


La expresión que se suma al puntero ha de ser entera y no tiene por qué ser constante. Obsérvese que ptr es lo mismo que ptr+0.

La expresión sumada NO es el número de bytes que se suman a la dirección, es el número de elementos del tipo al que apunta el puntero:

/* Supongamos que un "char" ocupa 1 byte */
/* y que un "double" ocupa 8 bytes */
 
char* ptrchar;
double* ptrdouble;
 
...
 
*(ptrchar+3) = 33; /* la dirección es ptrchar + 3 bytes */
 
*(ptrdouble+3) = 33.0;    
                                   /* la dirección es ptrdouble + 24 bytes,
   ya que cada double ocupa 8 bytes */

El compilador "sabe" cómo calcular la dirección según el tipo.

Aritmética de punteros (más)


A un puntero se le puede aplicar cualquier clase de operación de suma o resta (incluyendo los incrementos y decrementos).

/* Rellenar de unos los elementos del 10 al 20 */
 
int* ptr;                          /* el puntero */
int vector [100]; /* el vector */
int i;                    /* variable contadora */
 
ptr = &vector[0]; /* ptr apunta al origen del vector */
ptr+=10;                  /* ptr apunta a vector[10] */
 
for ( i=0; i<=10; i++ )
{
  *ptr = 1;      /*       asigna 1 a la posición de memoria apuntada                            por "ptr" */
  ptr++;         /*       ptr pasa al siguiente elemento */
}

7.7 Punteros y vectores


Si ptr es un puntero, la expresión

ptr[k]

es equivalente a

*(ptr+k)

con lo que se puede trabajar con un puntero como si se tratara de un vector:

int* ptr;
int vector [100];
 
ptr = &vector[10];
 
for ( i=0; i<=10; i++ )
  ptr[i] = 1;    /* equivalente a *(ptr+i) = 1 */


Un vector es en realidad un puntero constante. El nombre de un vector se puede utilizar como un puntero, al que se le puede aplicar la aritmética de punteros (salvo que no se puede alterar).

Por ello la expresión

ptr = vector;

es equivalente a

ptr = &vector[0];

7.8 Paso de vectores como parámetros a funciones


La aritmética de punteros permite trabajar con vectores pasados como parámetros a funciones en forma de punteros:


/*       Rellena de ceros los "n_elem" 
         primeros elementos de "vector"
*/
 
void rellena_de_ceros ( int n_elem, int* vector )
{
  int i;
  for ( i=0; i<n_elem; i++ )
         *(vector++) = 0;    /* operador de post-incremento */
}
 
main()
{
  int ejemplo [300];
  int otro_vector [100];
 
  /* pasa la dirección del vector "ejemplo" */
  rellena_de_ceros ( 300, ejemplo );
 
  /* rellena los elems. del 150 al 199 */
  rellena_de_ceros ( 50, otro_vector+150 );
}

7.9 Punteros y estructuras


Un puntero puede apuntar a una estructura y acceder a sus campos:

struct Dato
{
  int campo1, campo2;
  char campo3 [30];
};
 
struct Dato x;
struct Dato *ptr;
...
ptr = &x;
(*ptr).campo1 = 33;
strcpy ( (*ptr).campo3, "hola" );

El operador ->


Para hacer menos incómodo el trabajo con punteros a estructuras, el C tiene el operador flecha -> que se utiliza de esta forma:

ptr->campo

que es equivalente a escribir

(*ptr).campo

Así, el ejemplo anterior quedaría de esta forma:

ptr = &x;
ptr->campo1 = 33;
strcpy ( ptr->campo3, "hola" );


7.10 Memoria dinámica: malloc y free


Los variables y vectores en C ocupan un tamaño prefijado, no pueden variarlo durante la ejecución del programa.
Por medio de punteros se puede reservar o liberar memoria dinámicamente, es decir, según se necesite. Para ello existen varias funciones estándares, de la biblioteca <stdlib.h>

La función malloc sirve para solicitar un bloque de memoria del tamaño suministrado como parámetro. Devuelve un puntero a la zona de memoria concedida:

void* malloc ( unsigned numero_de_bytes );

El tamaño se especifica en bytes. Se garantiza que la zona de memoria concedida no está ocupada por ninguna otra variable ni otra zona devuelta por malloc.
Si malloc es incapaz de conceder el bloque (p.ej. no hay memoria suficiente), devuelve un puntero nulo.

Punteros void*

La función malloc devuelve un puntero inespecífico, que no apunta a un tipo de datos determinado. En C, estos punteros sin tipo se declaran como void*
Muchas funciones que devuelven direcciones de memoria utilizan los punteros void*. Un puntero void* puede convertirse a cualquier otra clase de puntero:

         char* ptr = (char*)malloc(1000);

Operador sizeof

El problema de malloc es conocer cuántos bytes se quieren reservar. Si se quiere reservar una zona para diez enteros, habrá que multiplicar diez por el tamaño de un entero.
El tamaño en bytes de un elemento de tipo T se obtiene con la expresión

sizeof ( T)

El tamaño de un char siempre es 1 (uno).

Función free

Cuando una zona de memoria reservada con malloc ya no se necesita, puede ser liberada mediante la función free.

void free (void* ptr);

ptr es un puntero de cualquier tipo que apunta a un área de memoria reservada previamente con malloc.
Si ptr apunta a una zona de memoria indebida, los efectos pueden ser desastrosos, igual que si se libera dos veces la misma zona.


Ejemplo de uso de malloc, free y sizeof


#include <stdlib.h>
 
int* ptr;        /* puntero a enteros */
int* ptr2;       /* otro puntero */
 
...
 
/* reserva hueco para 300 enteros */
ptr = (int*)malloc ( 300*sizeof(int) );
...
  ptr[33] = 15;           /* trabaja con el área de memoria */
 
  rellena_de_ceros (10,ptr);       /* otro ejemplo */
 
  ptr2 = ptr + 15;                 /* asignación a otro puntero */
 
/* finalmente, libera la zona de memoria */
free(ptr);       

Obsérvese que hay que convertir el puntero void* devuelto por malloc a un int*, para que no haya incompatibilidad de tipos.

7.11 Precauciones con la memoria dinámica


Si una zona de memoria reservada con malloc se pierde, no se puede recuperar ni se libera automáticamente (no se hace recolección de basura ). El programador es el responsable de liberar las áreas de memoria cuando sea debido.
Si una zona de memoria liberada con free estaba siendo apuntada por otros punteros, esos otros punteros apuntarán a una zona ahora incorrecta.


7.12 Otras funciones de manejo de memoria dinámica


void* calloc ( unsigned nbytes );

Como malloc, pero rellena de ceros la zona de memoria.


char* strdup ( char* cadena );

Crea un duplicado de la cadena. Se reservan strlen(cadena)+1 bytes.

7.13 Punteros a funciones

Un puntero puede apuntar a código. La declaración es similar a esta:

void (*ptr_fun) (int,int);

En este caso se está declarando una variable, ptr_fun, que apunta a una función “void” que admite dos parámetros enteros.
Para tomar la dirección de una función, se escribe su nombre sin paréntesis ni parámetros.
Para llamar a una función apuntada por un puntero, se usa el nombre del puntero como si fuera una función.

int suma (int a, int b)
{
  return a+b;
}
 
int resta (int a, int b)
{
  return a-b;
}
 
// declaramos un puntero a funciones con dos parámetros
// enteros que devuelven un entero
int (*funcion) (int,int);
 
 
{
  ...
  funcion = suma;    // ‘funcion’ apunta a ‘suma’
  x = funcion(4,3);  // x=suma(4,3)
  funcion = resta;   // ‘funcion’ apunta a ‘resta’
  x = funcion(4,3);  // x=resta(4,3)
}

0 comentarios: