Operacions amb punters en C

Última modificació per editor1 el 2018/09/16 21:29

Objectius

  1. Aprendre com fer un recorregut de vectors amb punters en llenguatge C.
  2. Aprendre a utilitzar punters a punters en C.
  3. Aprendre a utilitzar vectors de punters en C.

Introducció

Una vegada vist què és un punter i les seves operacions més bàsiques, en aquesta unitat veurem alguns exemples més de l’ús dels punters en llenguatge C. També veurem dos exemples de punters a punters, així com el pas per referència de punters i vectors de punters.

1. Recorregut de vectors amb punters

Hem vist que quan declarem una variable de tipus vector, realment aquesta variable és un punter que apunta l’inici d’aquest vector. Per a veure les equivalències, mostrarem un codi inicial i l’anirem transformant per a anar introduint punters sense canviar en cap moment el seu resultat.

Definim el vector de nombres enters v amb 6 posicions i li donem valors inicials 1,2, 3 ..., 6:

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

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

Quan utilitzem l’expressió v[''i''], el que fem és accedir al contingut del punter v desplaçat i posicions. Per tant, aquest codi seria completament equivalent a escriure:

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

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

Recordeu que quan a un punter li sumem o restem un valor, aquest valor no se suma directament a l’adreça que guarda el punter, sinó que és el nombre d’unitats (bytes que ocupa el tipus al qual representa el punter) el que se suma o es resta.

A continuació tindrem en compte el fet que les posicions d’un vector en memòria es guarden consecutives. Definirem un vector que apunti el final d’un altre vector i un altre el principi. Anirem desplaçant el punter des del principi fins que arribem 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++;    
}

In aquest cas, atès que els valors que assignem són consecutius, podríem escriure:

/* 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++;    
}

El que estem fent en aquest últim exemple és assignar a cada posició el valor de l’anterior sumant 1. Cal fixar-se en l’ús dels parèntesis en l’assignació, ja que *(pStart + 1) significa «contingut del vector pStart desplaçat una posició», mentre que *pStart + 1 significa «sumar 1 al contingut de pStart».

2. Punters a punters

Els punters no deixen de ser un tipus de dades com qualsevol altre, per la qual cosa, igual que podem definir punters a enters o punters a reals, també podem definir punters a punters. Un dels casos més habituals de l’ús de punters a punters és quan es passa un paràmetre de tipus entrada/sortida o sortida a una acció o funció que és de tipus punter.

A continuació tenim un exemple de codi que crida aquesta acció passant solament el punter:

#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;
}

Aquest codi sempre retornarà error. Vegem què passa en realitat:

PointerRefEx1.png

El primer que cal tenir present és que encara que es diguin igual, la variable v de la funció main i la variable v de l’acció getFloatVector no tenen res a veure; es troben en diferents zones de memòria i, per tant, els seus continguts són independents. Veiem que el que passa quan cridem l’acció getFloatVector és que es copia el contingut de la variable v de la funció main a la variable v de l’acció getFloatVector, que en aquest cas és el valor null. Dins de l’acció assignem al punter l’adreça que retorna la funció malloc, que és l’adreça a l’inici de la memòria reservada, en aquest exemple l’adreça 55. Aquest valor queda assignat només internament a l’acció, ja que el punter s’ha passat per valor. Per a poder passar el punter per referència haurà de ser de punter a punter, com en l’exemple següent:

#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;
}

Fixem-nos que ara li passem l’adreça al punter i el valor s’assigna al contingut del punter, que és l’adreça del punter original. En el següent esquema es mostra què passa ara:

PointerRefEx2.png

Veiem que en aquest cas sí que es modifica el punter correctament. 

En el primer cas, la funció malloc ha reservat una zona de memòria que, en finalitzar l’execució de l’acció getFloatVector, segueix estant reservada fins al final de l’execució del programa. En haver perdut l’adreça on començava aquesta zona de memòria és impossible alliberar-la i queda ocupada i inservible durant tota l’execució. Si això es produeix dins d’un bucle, pot ser que ens quedem sense memòria ràpidament. La memòria que una aplicació reserva i no allibera es coneix com memory leak.

3. Vector de punters

Igual que amb la resta dels tipus de dades, també podem declarar vectors de punters. Un vector de punters és exactament de la mateixa manera que un vector de qualsevol altre tipus de dades, com ara enters, reals o tuples. Cal tenir present que, a causa que una variable declarada com a vector és un punter, un vector de punters serà un altre exemple de punter a punter. Un exemple que hem estat utilitzant en els nostres programes fins ara és un vector de cadenes de caràcters. Si ens fixem en la declaració de la funció * main:

#include <stdio.h>

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

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

Veiem que té dos paràmetres, un enter argc i un punter a punter de caràcters argv. El paràmetre argv és un vector de cadenes de caràcters, i el valor argc ens indica la longitud d’aquest vector. Per a mostrar per pantalla tots els paràmetres que ens han passat en executar el programa, podem fer un recorregut d’aquest 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;
}

Tingueu en compte que cada posició d’aquest vector es mostra com una cadena de caràcters, que és també un vector de caràcters. Per tant, tenim un vector de vectors de caràcters o, el que és el mateix, un vector de punters a caràcter i, per tant, un punter a punter a caràcter (char**).

3.1. Vector de vectors i matrius

Si en l’exemple anterior volguéssim accedir a la tercera lletra de la quarta cadena de caràcters ho faríem amb:

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

Per tant, tal com accediríem a la posició d’una matriu. Les matrius no són res més que un cas particular de vector de vectors, en el qual tots els vectors tenen la mateixa longitud definida a la declaració de la matriu.

3.2. Exemple

Anem a veure un exemple de l’ús dels vectors de punters en l’accés indexat a dades. Partim del següent tipus tPerson i un vector de persones vPerson:

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

tPerson vPerson[6];

Si volguéssim mostrar totes les persones que hi ha al vector, ho faríem amb el codi següent:

unsigned int i;

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

El que podem fer ara és crear índexs a aquest vector que ens permetin accedir a les dades de manera ordenada. Per exemple, creem dos vectors vName i vAge de punters a tPerson, de manera que cada posició d’aquests vectors sigui un punter que apunti a un element de vPerson. En el vector vName els punters estaran ordenats per nom, mentre que en vAge ho estaran per edat. A continuació es mostren aquests vectors en un gràfic on les fletxes simbolitzen «apunta a», o sigui, que el vector conté l’adreça de la primera posició de l’estructura tPerson corresponent:

PointerVectorIndex.png

Els mètodes d’ordenació queden fora de l’objectiu d’aquesta unitat, per la qual cosa farem una assignació manual dels punters, seguint l’exemple mostrat a 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]);

El primer que cal veure és que no importa on es posa l’asterisc quan es declaren els vectors de punters. El segon en què cal fixar-se és que estem utilitzant el vector com si fos un vector de qualsevol tipus, solament que li assignem adreces.

Una vegada tenim els dos vectors d’índex, si ara volem llistar les persones ordenades per nom, ho podem fer amb:

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

Atès que ara tenim punters a estructures, utilitzem el símbol -> per a accedir als camps de les estructures. El mateix passaria si volguéssim mostrar les persones ordenades per edat:

unsigned int i;

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

Atès que utilitzem punters a la informació guardada en el vector vPerson, si modifiquem les dades, els canvis sempre es faran a la informació de vPerson, tant si els fem directament a un element de vPerson com a un dels elements de vName o vAge.

Resum

En aquesta unitat hem vist diferents usos dels punters. També hem vist que podem definir punters a punters. Finalment, hem vist el concepte de memory leak, que consisteix en el fet que part de la memòria que reservem dinàmicament no s’allibera correctament perquè, en molts casos, modifiquem el punter que l’apuntava i ja no podem accedir a la memòria de cap manera.

Etiquetes:
Creat per editor1 el 2018/09/16 21:28