Punteros


Punteros y estructuras de datos dinámicas

Empecemos por unas definiciones respecto a los diferentes tipos de datos que manejan las computadoras:

  • Dato: Porción de información manejable por el computador.
  • Tipos de datos: Formados por el conjunto de espacio de representación y conjunto de operaciones asociadas.
    • Simples
      • Caracter
      • Entero
      • Real
      • Puntero
    • Estructurados o compuestos:
      • Estáticos: Su espacio en memoria es reservado en tiempo de compilación.
      • Dinámicos: Su espacio en memoria es reservado en tiempo de ejecución.
      • Contiguos: Se guardan en memoria de forma consecutiva (vectores, matrices, arrays…).
      • Enlazados: No se guardan en memoria de forma consecutiva (listas, árboles, grafos…).


Tipos estáticos

Como por ejemplo: short, int, char, long, [], [][], etc. Se almacenan de forma estática en la memoria del ordenador, destinando memoria para este tipo de variables en tiempo de compilación. La dirección de memoria asignada se puede conocer con el operador de dirección &.

Ventajas
  • Sencillos de manejar y no dan problemas en tiempo de ejecución.
Desventajas
  • No pueden crecer o menguar durante la ejecución del programa (hay que hacer estimaciones previas a la codificación).
  • Las zonas de memoria asignadas a estas variables son fijas (no son adecuadas para representar listas, árboles, etc.).


Los punteros

Es una variable de tipo simple, que almacena como valor una dirección de memoria, que suele estar ocupada por otro dato diferente. El puntero, pués, apunta o referencia a otro dato.

Se declaran mediante el operador * y el tipo de dato al que apuntará (para que el compilador sepa cómo leer los datos de las dirección apuntadas). También es posible usar punteros sin tipo definido (void *) indicando luego durante la asignación a qué tipo de dato apuntan.

Pueden usarse arrays de punteros o punteros a/dentro de estructuras.

Los punteros que no apuntan a ningún sitio deben direccionarse a NULL (0x0000 en C) y se representan mediante una raya que los tacha.

Acceso al contenido del puntero:

Se puede conocer el contenido de la variable a la que referencia un puntero mediante el operador de contenido *, es decir, se puede acceder al dato apuntado por el puntero, tanto para lectura como para escritura:

Ejemplo:

    int *punt;
    int var;
 
    punt = &var;          /* hacemos que punt apunte a var */
    *punt = 10;                /* estamos cambiando el valor de var */

Así pues:

punt     = Valor guardado dentro del puntero punt (dirección).
&punt    = dirección de memoria donde punt guarda sus valores.
*punt    = valor almacenado en la posición de memoria cuya 
           dirección se guarda en punt.


Uso de punteros


A). Para referenciar a variables simples:

Apuntado a variables de tipo simple:

int *punt;
int numero

punt = №
*punt = 123;                / *hacemos numero=123*/


B). Paso por referencia de variables simples:

Consiste en usar punteros como parámetros:

/*-- Llamar con ModificaVariable(&var, valor); --- */
void ModificaVariable( int *a, int valor )
{   
    *a = valor;
}


C). Para referenciar a variables compuestas:

Consiste en la utilización de punteros para referenciar a variables compuestas como vectores y registros. En este caso se puede acceder mediante el puntero a la totalidad de campos de la estructura:

      float vect[4] = { 1, 2, 3, 4};
      struct pieza nueva = {234, "bobina"}; 
      float *pf;
      struct pieza *psp;
 
      pf = vect;
      printf("%f = %f", *pf, pf[0]);
      psp = &nueva;
      printf("%d", psp->codigo );

NOTA: El operador flecha → sustituye a * y ()

       psp->codigo  =  (*psp).codigo


D). Paso por referencia de variables compuestas:

Para simulación de paso por referencia de variables compuestas:

void funcion1( float v[], struct fecha *actual)
{
    v[0] = v[1]+5;
    actual->mes = 5;
}
 
void main()
{
   struct fecha hoy;
   float vector[4]={1,2,3,4};
   funcion1(vector, &hoy );
}


E). Punteros que referencian a punteros:

Los punteros pueden referenciar a otros punteros. Para ello se declara la variable de tipo puntero a puntero ( * * ):

int a = 7;
int *pi;
int **ppi;
                          // entero, puntero a entero y puntero a puntero de enteros.
pi = &a;                                         // pi referencia a "a"
ppi = &pi  ;                                     // ppi referencia a pi
printf("El valor de a es %d", **pi );


F). Paso por referencia de punteros:

void main( void )
{
   int a=3, *pi;
   pi = &a;                            // pi = dirección de la variable "a".
   funcion1( pi, &pi );
}
 
void funcion1( int *p_entero, int **p_puntero )
{
  *p_entero = 5;                          // cambia el valor de "a"
    p_entero = NULL;                      // cambia el valor de "p_entero" pero no el de "pi".
   *p_puntero = NULL;                     // cambia el valor de "pi"
}


Uso de punteros en la creación de variables dinámicas

#include <alloc.h>

Los punteros también se utilizan en la reserva dinámica de memoria, utilizándose para la creación de estructuras cuyo tamaño se decide en tiempo de ejecución (son creadas y destruidas cuando el programa las necesite), adecuado para programas donde no se pueda hacer una estimación inicial eficiente de necesidades de memoria.

El lenguaje C utiliza para la reserva dinámica de memoria una zona de espacio diferente del segmento de datos y de la pila del programa, llamada montículo (heap), para que ésta no depende de los movimientos de la pila del programa y se puedan reservar bloques de cualquier tamaño (otros lenguajes usan space-lists).

a). La función malloc():

void * malloc( size );

Reserva un bloque de memoria de tamaño "size" bytes y devuelve la dirección de comienzo de dicha zona (sin valor inicial dentro de ella). Esta dirección suele asignarse a una variable de tipo puntero previamente declarada. En caso de error o de no existir tanta memoria disponible, malloc devuelve NULL:

 float *p;
 p = (float *) malloc( sizeof(float));
 *p = 5.0;


b). La función calloc():

void * calloc( size_t n, size_t size );

Reserva un bloque de memoria para ubicar n elementos contiguos de "size" bytes cada uno (ej: un vector).

 int *p;
 p = (int *) calloc( 5, sizeof(int));
 p[3] = 2;


c). La función realloc():

void * realloc( void *pant, size_t size );

Cambia el tamaño de un área de memoria reservada con anterioridad, a un tamaño de "size" bytes contiguos. Si size es mayor que el anterior tamaño, realloc() busca una nueva zona, copia allí los datos y destruye la zona anterior. Si size es menor, truncará el bloque actual al nuevo tamaño.

 int *p, *nuevo
 p = (int *) calloc( 5, sizeof(int));
 nuevo = realloc(p, 10);


d). La función free():

void free( void *punt );

Libera la memoria asignada previamente con malloc() o calloc() al puntero punt, evitando que los bloques de memoria permanezcan ocupados hasta la salida del programa al liberar estos bloques para su posterior utilización. Tras el uso de free es recomendable anular el puntero poniéndolo a NULL. Free() no puede usarse para liberar la memoria de las variables globales o locales (están en el segmento de datos o pila, no en el montículo).

 float *p;
 p = (float *) malloc( sizeof(float));
 
 // libera la zona apuntada por p
 free(p); 


Aritmética de punteros

Aparte de las operaciones de asignación (=) y de comparación (==), sobre los punteros están definidas las operaciones aritméticas (–,-,+,++), aunque de un modo diferente a como están definidas para los otros tipos. Estas operaciones sólo pueden usarse cuando los punteros apuntan a matrices o vectores ( de cualquier tipo). Cuando incrementamos o decrementamos un puntero en una cantidad "n", el compilador da como resultado la dirección situada a n*sizeof(tipo_base_del_array) bytes de la dirección original. Es decir, al sumar 1 a un puntero, estamos accediendo al siguiente elemento del vector, como puede verse en el siguiente ejemplo:

 int *p;      
 int vector[4]={ 5,6,1,4};
 p = &vector[0];
 p++;                                    // p apunta a vector[1]
 p=p+2;                                // p apunta a vector[3]
 p=p-1;                                // p apunta a vector[2]


Punteros y arrays

Existe una gran relación entre los punteros y los arrays estáticos, ya que en realidad estos últimos son punteros encubiertos que apuntan al elemento inicial del vector. Al declarar un vector, el nombre del mismo es un puntero a vector[0]. Al apuntar a un vector podemos utilizar sobre él las operaciones del apartado anterior:

 int *p, a;
 int vector[4]={ 5,6,1,4};
 p = vector;
 p[3] = 10;                             // escribimos 10 en vector[3] (equivale a *(p+3)=10)
 p++;                                   // p apunta a vector[1]
 a = *(p+2) + *(vector+1);              // a vale 1+6=7
 p=p+2;                                 // p apunta a vector[3]
 p = vector+2;                          // p apunta a vector[2]


Vectores dinámicos

Son colecciones de datos del mismo tipo almacenados en posiciones contiguas de memoria, pero definidos mediante memoria dinámica, y que son liberados cuando no se necesitan (mediante free()).

 int *p;
 p = (int *) calloc( 5, sizeof(int));

Se puede indexar este vector de igual forma que los estáticos:


a) Mediante índice:

   p[0] = primer elemento = *p;          (ej:  p[5] = 32 )
   p[1] = segundo elemento (etc...).


b) Mediante aritmética de punteros:

   *p     = primer elemento = *p;
   *(p+1) = segundo elemento (etc...).   (ej:  *(p+5) = 32 )

Para recorrer un vector dinámico se hace de igual forma que mediante vectores estáticos:

int n = 10;
int *p;
p = (int *) calloc(n, sizeof(int));
for(i=0; i<n; i++)
   q[i] = 1;    
 
/* o bien *(q+i)=1 */

o bien:

#define n = 10;
int *p, *q;
q = p;
for(i=0; i<n; i++)
{
   *q = 1;
    q++;
}

Es recomendable mantener siempre una referencia (puntero) al inicio del vector para poder volver al principio ( y liberarlo) en cualquier momento, así como una variable que almacene la cantidad de elementos del vector:


Matrices dinámicas

En las matrices dinámicas, a diferencia de las estáticas, no todos los elementos están guardados en posiciones consecutivas de memoria. La matriz dinámica se define como un vector de punteros a vectores de datos (ej: 5x3):

// asignación:
int *matriz[5];
for( i=0; i<5; i++ )              
   matriz[i] = (int *) calloc( 3, sizeof(int));
 
// liberación:
for( i=0; i<5; i++ )
   free(matriz[i]);

También puede crearse definiendo el vector de punteros dinámicamente (mejor forma):

// asignación:
int **matriz;
matriz = (int **) calloc(5,sizeof(int *));
for( i=0; i<5; i++ )
   matriz[i] = (int *) calloc( 3, sizeof(int));
 
// liberación:
for( i=0; i<5; i++ )
   free(matriz[i]);
free(matriz);

Métodos de acceso al elemento (i,j):

  • Por índices: matriz[i][j];
  • Por aritmética: *(*(matriz+i)+j);
  • Mezcla de ambos: *(matriz[i]+j), (*(matriz+i))[j];

Errores con punteros


a). Asignación correcta e incorrecta de valores:

int *p, *q;
int numero, copia;
 
numero=15;
p = &numero;                                 // correcto: p apunta a numero
p = numero;                                  // incorrecto, p apunta la dirección 15.
                                             // -> Apunta a zonas incontroladas.
*p = 5;                                      // correcto, numero ahora vale 5
p = 5;                                       // incorrecto, p apunta a la dirección 5.
 
q = (int *) malloc(sizeof(int));
*q = *p;                                     // correcto, pedimos memoria para otro entero,
                                             //  y almacenamos numero allí
p=q;                                         // correcto, p apunta a q, es decir,
                                             // a la nueva memoria.
copia = *p;
 
q = p;                                       // Incorrecto: si perdemos la referencia a q,
                                             // ya no podremos liberar la memoria asignada.


b). Punteros no inicializados o apuntando a zonas de memoria no controladas:

char *letra, mi_letra;
mi_letra = *letra;     // incorrecto:  mi_letra tendrá un valor desconocido
                       // (letra no inicializado).
 
*letra = 'A';          // incorrecto: letra no inicializado, estamos
                       // escribiendo en algun lugar de la memoria.
 
(etc...)
free(puntero);
puntero=NULL,
*puntero = 100;        // incorrecto, puntero estaba apuntando a NULL.
                       // (Punteros sueltos).

También es incorrecto, y peligroso, en los arrays y punteros que apunta a arrays, utilizar como índice valores que se salgan del rango del vector.

float vector[10];
vector[-2] =           // incorrecto, igual que vector[11], p+11, etc...


Tipos de datos estáticos vs dinámicos

Para elegir entre tipos de datos estáticos frente a dinámicos, uno debe ver si el flujo de datos puede ser predeterminado desde el principio del programa. Si es así, se usarán estructuras estáticas, y si no, dinámicas. Para ciertos algoritmos también puede ser necesario el uso de memoria dinámica (estructuras abstractas), aunque otros pueden requerir el uso de estructuras estáticas (busqueda binaria, ordenación, etc.). Hay que recordar que si nos decantamos por mem. dinámica, se debe hacer mucho uso de inicializaciones, NULL, chequeos y free().


Ejercicios


11-1 ¿Qué se escribe por pantalla cuando se ejecute lo siguiente?:

 int *a, *b;
 a = (int) malloc(sizeof(int));
 b = (int) malloc(sizeof(int));
 *a = 19;
 *b = 5;
 a = b;
 *b = 7;
 printf("%d\n", *a );          
 
(solución = 7)
 int *a, *b;
 a = (int) malloc(sizeof(int));
 b = (int) malloc(sizeof(int));
 *a = 19;
 *b = 5;
 *a = *b;
 *b = 7;
 printf("%d\n", *a );          
 
  (solucion =5)


11-2 Supongamos p un puntero a reales. ¿como harías que 23.5 sea el valor referido por el puntero?

 Solución:
 p= (float *) malloc(sizeof(float));
 *p = 23.5



11-4 Supongamos que p es un puntero a char. ¿Seria correcto hacer p = 'A'?

Solución: No. Suponiendo que p apunta al lugar correcto, de manera correcta sería *p = 'A';

11-6 ¿Qué es un puntero suelto?

Solución: Un puntero que referencia a NULL o a un sitio desconocido (puntero no inicializado).

11-9 Suponga que p apunta a NULL ¿Que ocurriría al hacer *p = 8?

Solución: Que escribiríamos en la primera posición de la mem el valor 8 (puede ser catastrófico).

11-10 ¿Que valor se guarda sobre A según este programa?

void main( void) 
{
  int v[4] = { 2,3,5,11}, a, *p;
  p = v+2;
  p--;
  a = *p + *(p+1) + *(v+1) + p[2];                                         
}
 
Solucion = 22


<Volver a la sección de Tutoriales de Programación>