Operaciones con punteros en C

Última modificación por editor1 el 2018/09/17 10:37

Objetivos

  1. Aprender cómo hacer un recorrido de vectores con punteros en lenguaje C.
  2. Aprender a utilizar punteros a punteros en C.
  3. Aprender a utilizar vectores de punteros en C.

Introducción

Una vez visto qué es un puntero y sus operaciones más básicas, en esta guía veremos algunos ejemplos más del uso de los punteros en lenguaje C. También veremos dos ejemplos de punteros a punteros, así como el paso por referencia de punteros y vectores de punteros.

1. Recorrido de vectores con punteros

Hemos visto que cuando declaramos una variable de tipo vector, realmente esta variable es un puntero que apunta al inicio de este vector. Para ver las equivalencias, mostraremos un código inicial y lo iremos transformando para ir introduciendo punteros sin cambiar en ningún momento su resultado.

Definimos el vector de números enteros v con 6 posiciones y le damos valores iniciales 1,2, 3 ..., 6:

/* Variable definition */
unsigned int i;
int v[6];

for(i=0; i<6; i++) {
   *[i]=i+1;
}

Cuando utilizamos la expresión v[i], lo que hacemos es acceder al contenido del puntero v desplazado i posiciones. Por tanto, este código sería completamente equivalente a escribir:

/* Variable definition */
unsigned int i;
int v[6];

for(i=0; i<6; i++) {
   *(v+i)=i+1;
}

Recordad que cuando a un puntero le sumamos o restamos un valor, este valor no se suma directamente a la dirección que guarda el puntero, sino que es el número de unidades (bytes que ocupa el tipo al que representa el puntero) lo que se suma o resta.

A continuación tendremos en cuenta el hecho de que las posiciones de un vector en memoria se guardan consecutivas. Definiremos un vector que apunte al final de otro vector y otro al principio. Iremos desplazando el puntero desde el principio hasta que lleguemos al del final.

/* Variable definition */
unsigned int i;
int v[6];
int *pStart=NULL, *pEnd=NULL;

pStart=v;
pEnd=&(p[5]);

i=1;
while(pStart<=pEnd) {
   *pStart=i;
    i++;
    pStart++;    
}

En este caso, dado que los valores que asignamos son consecutivos, podríamos escribir:

/* Variable definition */
int v[6];
int *pStart=NULL, *pEnd=NULL;

pStart=v;
pEnd=&(p[5]);

*pStart=1;
while(pStart<pEnd) {
   *(pStart+1)=*pStart +1;
    pStart++;    
}

Lo que estamos haciendo en este último ejemplo es asignar a cada posición el valor de la anterior sumando 1. Hay que fijarse en el uso de los paréntesis en la asignación, ya que *(pStart + 1) significa «contenido del vector pStart desplazado una posición», mientras que *pStart + 1 significa «sumar 1 al contenido de pStart».

2. Punteros a punteros

Los punteros no dejan de ser un tipo de datos como cualquier otro, por lo que, al igual que podemos definir punteros a enteros o punteros a reales, también podemos definir punteros a punteros. Uno de los casos más habituales del uso de punteros a punteros es cuando se pasa un parámetro de tipo entrada/salida o salida a una acción o función que es de tipo puntero.

A continuación tenemos un ejemplo de código que llama a esta acción pasando solo el puntero:

#include <stdio.h>
#include
<stdlib.h>

void  getFloatVector(float *v, unsigned int len) {
 v=(float*)malloc(len*sizeof(float));
}

int main(int argc, char **argv) {
float *v=NULL;

 getFloatVector(v, 3);

if(v==NULL) {
  printf("Error\n");
 } else {
  printf("OK\n");
 }

if(v!=NULL) {
  free(v);
 }
return 0;
}

Este código siempre retornará error. Veamos qué pasa en realidad:

PointerRefEx1.png

Lo primero que hay que tener presente, es que aunque se llamen igual, la variable v de la función main y la variable v de la acción getFloatVector no tienen nada que ver; se encuentran en diferentes zonas de memoria y, por lo tanto, sus contenidos son independientes. Vemos que lo que pasa cuando llamamos la acción getFloatVector es que se copia el contenido de la variable v de la función main en la variable v de la acción getFloatVector, que en este caso es el valor null. Dentro de la acción asignamos al puntero la dirección que devuelve la función malloc, que es la dirección al inicio de la memoria reservada, en este ejemplo la dirección 55. Este valor queda asignado solo internamente a la acción, ya que el puntero se ha pasado por valor. Para poder pasar el puntero por referencia tendrá que ser de puntero a puntero, como en el ejemplo siguiente:

#include <stdio.h>
#include
<stdlib.h>

void  getFloatVector(float **v, unsigned int len) {
*v=(float*)malloc(len*sizeof(float));
}

int main(int argc, char **argv) {
float *v=NULL;

 getFloatVector(&v, 3);

if(v==NULL) {
  printf("Error\n");
 } else {
  printf("OK\n");
 }

if(v!=NULL) {
  free(v);
 }
return 0;
}

Fijémonos en que ahora le pasamos la dirección al puntero y el valor se asigna al contenido del puntero, que es la dirección del puntero original En el siguiente esquema se muestra qué pasa ahora:

PointerRefEx2.png

Vemos que en este caso sí que se modifica el puntero correctamente. 

En el primer caso, la función malloc ha reservado una zona de memoria que, al finalizar la ejecución de la acción getFloatVector, sigue estando reservada hasta el final de la ejecución del programa. Al haber perdido la dirección donde comenzaba esta zona de memoria es imposible liberarla y queda ocupada e inservible durante toda la ejecución. Si esto se produce dentro de un bucle, puede que nos quedemos sin memoria rápidamente. La memoria que una aplicación reserva y no libera se conoce como memory leak.

3. Vector de punteros

Al igual que con el resto de los tipos de datos, también podemos declarar vectores de punteros. Un vector de punteros es exactamente de la misma manera que un vector de cualquier otro tipo de datos, tales como enteros, reales o tuplas. Hay que tener presente que, debido a que una variable declarada como vector es un puntero, un vector de punteros será otro ejemplo de puntero a puntero. Un ejemplo que hemos estado utilizando en nuestros programas hasta ahora es un vector de cadenas de caracteres. Si nos fijamos en la declaración de la función * main:

#include <stdio.h>

int main(int argc, char **argv) {

    printf("Hello world");
   return 0;
}

Vemos que tiene dos parámetros, uno entero argc  y un puntero a puntero de caracteres argv. El parámetro argv es un vector de cadenas de caracteres, y el valor argc nos indica la longitud de este vector. Para mostrar por pantalla todos los parámetros que nos han pasado al ejecutar el programa, podemos hacer un recorrido de este vector de la forma habitual:

#include <stdio.h>

int main(int argc, char **argv) {

   unsigned int i=0;

   for(i=0; i<argc; i++) {
        printf("%s\n", argv[i]);
    }

   return 0;
}

Tened en cuenta que cada posición de este vector se muestra como una cadena de caracteres, que es también un vector de caracteres. Por lo tanto, tenemos un vector de vectores de caracteres o lo que es lo mismo, un vector de punteros a carácter y, por tanto, un puntero a puntero a carácter (char**).

3.1. Vector de vectores y matrices

Si en el ejemplo anterior quisiéramos acceder a la 3ª letra de la 4ª cadena de caracteres lo haríamos con:

printf("%c\n", argv[3][2]);

Por tanto, tal como accederíamos a la posición de una matriz. Las matrices no son nada más que un caso particular de vector de vectores, en el que todos los vectores tienen la misma longitud definida en la declaración de la matriz.

3.2. Ejemplo

Vamos a ver un ejemplo del uso de los vectores de punteros en el acceso indexado a datos. Partimos del siguiente tipo tPerson y un vector de personas vPerson:

typedef struct {
   char name[15];
   unsigned char age;
} tPerson;

tPerson vPerson[6];

Si quisiéramos mostrar todas las personas que hay en el vector, lo haríamos con el siguiente código:

unsigned int i;

for(i=0; i<6; i++) {
    printf("Name: %d\nAge: %d\n\n", vPerson[i].name, vPerson[i].age);
}

Lo que podemos hacer ahora es crear índices a este vector que nos permitan acceder a los datos de forma ordenada. Por ejemplo, creamos dos vectores vName y vAge de punteros a tPerson, de forma que cada posición de estos vectores sea un puntero que apunte a un elemento de vPerson. En el vector vName los punteros estarán ordenados por nombre, mientras que en vAge lo estarán por edad. A continuación se muestran estos vectores en un gráfico donde las flechas simbolizan «apunta a», o sea, que el vector contiene la dirección de la primera posición de la estructura tPerson correspondiente:

PointerVectorIndex.png

Los métodos de ordenación quedan fuera del objetivo de esta unidad, por lo que haremos una asignación manual de los punteros, siguiendo el ejemplo mostrado en la figura:

tPerson* vName[6];
tPerson *vAge[6];

vName[0]=&(vPerson[5]);
vName[1]=&(vPerson[0]);
vName[2]=&(vPerson[1]);
vName[3]=&(vPerson[3]);
vName[4]=&(vPerson[4]);
vName[5]=&(vPerson[2]);

vAge[0]=&(vPerson[3]);
vAge[1]=&(vPerson[1]);
vAge[2]=&(vPerson[4]);
vAge[3]=&(vPerson[0]);
vAge[4]=&(vPerson[2]);
vAge[5]=&(vPerson[5]);

Lo primero que hay que ver es que no importa dónde se pone el asterisco cuando se declaran los vectores de punteros. Lo segundo en que hay que fijarse es en que estamos utilizando el vector como si fuera un vector de cualquier tipo, solo que le asignamos direcciones.

Una vez tenemos los dos vectores de índice, si ahora queremos listar las personas ordenadas por nombre, lo podemos hacer con:

unsigned int i;

for(i=0; i<6; i++) {
    printf("Name: %d\nAge: %d\n\n", vName[i]->name, vName[i]->age);
}

Dado que ahora tenemos punteros a estructuras, utilizamos el símbolo -> para acceder a los campos de las estructuras. Lo mismo pasaría si quisiéramos mostrar las personas ordenadas por edad:

unsigned int i;

for(i=0; i<6; i++) {
    printf("Name: %d\nAge: %d\n\n", vAge[i]->name, vAge[i]->age);
}

Dado que utilizamos punteros a la información guardada en el vector vPerson, si modificamos los datos, los cambios siempre se harán a la información de vPerson, tanto si los hacemos directamente a un elemento de vPerson como a uno de los elementos de vName o vAge.

Resumen

En esta guía hemos visto diferentes usos de los punteros. También hemos visto que podemos definir punteros a punteros. Por último, hemos visto el concepto de memory leak, que consiste en que parte de la memoria que reservamos dinámicamente no se libera correctamente debido, en muchos casos, a que modificamos el puntero que la apuntaba y ya no podemos acceder a la memoria de ninguna manera.

Etiquetas:
Creado por editor1 el 2018/09/17 10:36