Operacions amb punters en C
Objectius
- Aprendre com fer un recorregut de vectors amb punters en llenguatge C.
- Aprendre a utilitzar punters a punters en C.
- 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:
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:
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.
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:
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 <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:
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 <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:
Veiem que en aquest cas sí que es modifica el punter correctament.
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:
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:
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:
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:
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:
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:
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 *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:
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:
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.