12. Tipuri de date derivate direct - Tipul pointer Tipurile de date derivate direct sunt tipuri de date create pe baza unor tipuri de date existente. In limbajul C, un astfel de tip, nestructurat (simplu), este tipul pointer. Pointerii reprezintă adrese ale unor zone de memorie. Ei se utilizează pentru a face referire la entităţi (variabile, funcţii) din memorie cunoscute prin adresele lor. Pentru termenul de pointer se mai folosesc şi următoarele denumiri echivalente : referinţă, reper, localizator, indicator de adresă. Noţiunea de pointer joacă un rol extrem de important în programarea în C deoarece permite efectuarea de calcule complexe folosind adrese, operaţii care, până la apariţia C-ului, erau proprii doar limbajelor de asamblare. Dintre beneficiile care s-au obţinut prin introducerea pointerilor amintim: - transferul adecvat al parametrilor de ieşire ai funcţiilor - posibilitatea alocării dinamice de memorie; - posibilitatea folosirii funcţiilor ca parametri ai unor altor funcţii; - înlocuirea expresiilor cu indici prin expresii cu pointeri. 12.1. Declaraţia de pointer şi tipul pointer Un pointer este asociat unui tip de date sau tipului void. În general dacă tip este un tip de date, un pointer identificator către tipul tip se introduce printr-o declaraţie de forma: declaratie_variabila_pointer tip * identificator ; Figura 12.1 Declaraţia de variabilă de tip pointer Tipul tip precizează în acest caz tipul datelor stocate la adresele specificate de variabila de tip pointer. Comparând forma declaraţiei anterioare cu sintaxa generală a declaraţiei unei variabile, rezultă faptul că tip* reprezintă un nou tip de date, numit tip derivat, tipul pointer către tip.
În consecinţă o variabilă de tip pointer are ca valori adrese ale unor date de tipul tip (sau ca exprimare echivalentă, pointează spre tipul tip). În cazul în care o variabilă de tip pointer conţine adresa zonei de memorie alocată unei alte variabile x (evident de tipul tip) se mai spune că ea pointează spre x. Operatorul * se numeşte operator de dereferenţiere (indirectare) şi el este opusul operatorului de referenţiere (adresă), &. Aplicat unei variabile de tip pointer, operatorul * furnizează valoarea stocată în zona de memorie a cărei adresă este conţinută în pointer. Operatorul * are aceeaşi prioritate cu operatorii unari şi se asociază de la dreapta la stânga. Reamintim faptul că operatorul adresă, &, se aplică unei variabile, furnizând adresa acesteia (adresa zonei de memorie în care ea este depozitată). Există situaţii în care dorim ca un pointer să poată fi utilizat cu tipuri diferite de date, astfel încât în declaraţia de pointer nu putem specifica explicit un tip, ca în cazurile de mai sus. De aceea se utilizează în poziţia tipului, tipul void. Specificatorul de format pentru afişarea pointerilor este %p. Pointerii către void nu pot fi utilizaţi ca atare pentru a referi conţinutul zonelor de memorie către care pointează. Este necesară în prealabil o conversie explicită de tip pentru a preciza tipul datei către care pointează un astfel de pointer. Utilizarea tipului pointer către void asigură o flexibilitate mare în utilizarea pointerilor. Cu toate acestea ea poate constitui o potenţială sursă de erori în cazul unor utilizări abuzive. Programatorul trebuie să ştie în orice moment ce fel de tip de pointer este valoarea atribuită variabilei pointer de tip pointer către void. 12.2. Transferul parametrilor de ieşire în C Aşa cum s-a arătat transferul parametrilor în C se realizează prin valoare, parametrilor formali, pentru care se rezervă separat spaţiu de memorie, li se atribuie valorile parametrilor efectivi de apel corespunzători. Când se doreşte modificarea valorii parametrului efectiv ca urmare a apelului unei funcţii, deci practic când acesta este un parametru de ieşire, trebuie să transmitem funcţiei adresa variabilei ce reprezintă parametrul efectiv, iar în corpul funcţiei să folosim operatorul de dereferenţiere.
Atunci când parametrul este nume de tablou, acest deziderat este îndeplinit, deoarece în C numele unui tablou este o adresă, respectiv adresa primului element al tabloului. Folosind deci pointeri putem obţine efectele apelului prin referinţă: parametrul formal este un pointer căruia îi corespunde la apel o adresă. Facem observaţia că transferul parametrilor rămâne în continuare prin valoare, dar ceea ce se transmite funcţiei este valoarea adresei parametrului cu care se operează. 12.3. Legătura dintre pointeri şi tablouri Aşa cum s-a arătat, în C numele unui tablou este o adresă şi anume adresa primului element al tabloului. Rezultă deci că numele unui tablou este un pointer, dar, în acest caz, un pointer constant. În consecinţă, un parametru formal ce corespunde unui parametru efectiv care este un tablou unidimensional de tipul tip poate fi declarat fie ca tablou cu elemente de tipul tip, fie ca un pointer către tipul tip. 12.4. Aritmetica pointerilor Operaţiile aritmetice permise asupra pointerilor sunt : 1. incrementarea/decrementarea 2. adunarea/scăderea unui întreg la/dintr-un pointer 3. compararea a doi pointeri 4. diferenţa a doi pointeri 12.4.1. Operaţia de incrementare/decrementare Operatorii corespunzători acestor operaţii sunt ++ respectiv -- care se pot folosi atât în notaţie prefixată cât şi în notaţie postfixată. Efectul aplicării acestor operatori asupra unor operanzi de tip pointer este creşterea/micşorarea adresei ce reprezintă valoarea operandului cu un număr de octeţi egal cu cel necesar pentru a memora o dată de tipul celei către care pointează operandul. În general, în cazul unui pointer către tip declarat cu : tip *p, incrementarea sau decrementarea presupune modificarea valorii p cu sizeof(tip) octeţi.
12.4.2. Operaţia de adunare/scădere a unui întreg la/dintr-un pointer În cazul în care p este un pointer către tipul tip şi n este un număr întreg, cum adunarea/scăderea lui n din p poate fi privită ca o adunare/scădere repetată de n ori a unei unităţi, rezultă că,în acest caz, valoarea lui p se modifică cu n*sizeof(tip) octeţi. Rezultă deci că variabilele indexate se pot înlocui cu expresii cu pointeri, ceea ce conduce la optimizarea codului. Observaţie : Operaţiile 3 şi 4 sunt garantate în ANSII C în cazul în care pointerii se menţin în limitele unui aceluiaşi tablou. Este permisă poziţionarea pointerului pe poziţia imediat următoare ultimului element al tabloului. 12.4.3. Operaţia de comparare a doi pointeri Doi pointeri care pointează spre elementele unui aceluiaşi tablou pot fi comparaţi folosind operatorii de relaţie şi de egalitate. Se poate de asemenea face comparaţia cu pointerul NULL. Astfel dacă p pointează către t[i] şi q către t[j], i şi j strict mai mici decât dimensiunea maximă a tabloului, sunt legale comparaţiile : p<q p>q p==q p<=q p>=q p!=q 12.4.4. Operaţia de scădere a doi pointeri Operaţia de diferenţă a doi pointeri rezultă imediat din cea de adunare/scădere a unui întreg dintr-un pointer. Astfel dacă p pointează către t[i] şi q către t[i+n] unde n este un întreg, diferenţa q - p este egală cu n.
Un parametru formal ce corespunde unui parametru efectiv care este un tablou unidimensional de tipul tip poate fi declarat fie ca tablou cu elemente de tipul tip, fie ca un pointer către tipul tip. Exemplu : Să se proiecteze o funcţie care să calculeze lungimea unui şir de caractere. #include <stdio.h> #include <string.h> int lung(char s[]) int i; for(i=0;s[i]!='\0';i++) ; return i; int lung_p(char *p) char *q; for(q=p;*q!='\0';q++) ; return q-p; int main(void) char a[]="acesta este un sir"; printf("i %d\tii %d\tiii %d\n",lung(a),lung_p(a),strlen(a)); return 0 ;
12.5. Alocarea dinamică de memorie Limbajul C oferă utilizatorului posibilitatea de a aloca memorie, prin comenzi specifice, în timpul execuţiei programului, într-o zonă de memorie specială numită memoria heap, rezervarea de memorie numindu-se în acest caz alocare dinamică. Cea mai utilizată funcţie în acest scop este malloc cu prototipul (în stdlib.h): void * malloc(unsigned n) ; Funcţia malloc alocă în memoria heap o zonă contiguă de n octeţi, returnând un pointer către void ce reprezintă adresa de început a zonei astfel alocate, sau pointerul NULL în cazul în care alocarea nu a fost posibilă. Zona de memorie alocată în urma apelului funcţiei malloc se eliberează cu ajutorul funcţiei free al cărei prototip este (în stdlib.h): void free(void * p) ; unde p este un pointer nenul rezultat în urma apelului funcţiei malloc. Memento : Un parametru formal ce corespunde unui parametru efectiv care este un tablou unidimensional de tipul tip poate fi declarat fie ca tablou cu elemente de tipul tip, fie ca un pointer către tipul tip Exemplul 1. Să se proiecteze şi apeleze adecvat o funcţie care citeşte un vector cu n elemente de tip double. Funcţia va avea un parametru suplimentar, numele vectorului şi va returna numărul de elemente citite corect.
#include <stdio.h> #include <stdlib.h> #define MAX 100 int pndcit(double *p, int n, const char *s) int j=0; double *q=p+n; while (p<q) printf("elementul %s[%d]= ",s,j); if (scanf("%lf",p)!=1) fflush(stdin); break; p++; j++; return j;
int main(void) int i,n,nr; double x[max],*p; printf("numarul de elemente ale vectorului x (<=%d)= ", MAX); if(scanf("%d",&n)!=1 n<=0 n>max) printf("date eronate!\n"); exit(1); printf("elementele vectorului\n"); if ((nr=pndcit(x,n,"x"))!=n) printf("atentie : S-au citit doar %d elemente\n",nr); printf("vectorul x\n"); for (i=0;i<nr;i++) printf("%10g %c",x[i],i%6==5 i==nr-1?'\n':' '); printf("vectorul x\n"); p=x; for (i=0;i<nr;i++) printf("%10g %c",*p++,i%6==5 i==nr-1?'\n':' '); double *pv; int nd; printf("nr. de elem. ale vectorului alocat dinamic, nd= "); if(scanf("%d",&nd)!=1 nd<=0) printf("date eronate!\n"); exit(1); pv=(double *)malloc(nd*sizeof(double)); if(!pv) printf("memorie insuficienta!\n"); exit(1); printf("elementele vectorului\n"); if ((nr=pndcit(pv,nd,"v"))!=nd) printf("atentie : S-au citit doar %d elemente\n",nr); printf("vectorul V\n"); for (i=0;i<nr;i++) printf("%10g %c",pv[i],i%6==5 i==nr-1?'\n':' '); printf("vectorul V\n"); p=pv; for (i=0;i<nr;i++) printf("%10g %c",*p++,i%6==5 i==nr-1?'\n':' '); free(pv); return 0;
12.6. Tablouri de pointeri Pointerii fiind tipuri de date, cu ajutorul lor se pot forma tipuri de date structurate, ca de exemplu tablouri. Tablourile de pointeri se utilizează frecvent la prelucrarea şirurilor de caractere, permiţând o tratare a acestora în mod unitar. Este mult mai avantajos să se definească un tablou de pointeri, fiecare element al tabloului fiind un pointer către un şir de caractere decât să se definească direct un tablou de şiruri de caractere. Exemplul 2. Să se scrie un program care să permită memorarea şi afişarea ulterioară a maximum 50 de cuvinte preluate de la consolă. Sfârşitul sesiunii este marcat prin acţionarea sfârşitului de fişier de la consolă.
#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAXCUV 50 char *MemSir(char *); int main(void) char *tabptr[maxcuv]; int i,j; char temp[255]; printf("introduceti cuvintele ") ; printf(" (max %d, CTRL/Z pt. a incheia)\n", MAXCUV); for (i=0;scanf("%s",temp)==1 && i<maxcuv; i++) if((tabptr[i]=memsir(temp))==0) printf("memorie insuficienta la alocarea sirului %d\n",i); break; for (j=0;j<i;j++) printf("%s\n",tabptr[j]); if((j+1)%23==0) printf("actionati o tasta pt. a continua..\n"); getch(); for(i=0;j<i;j++) free(tabptr[j]); return 0; void strcopy(char *d, char *s) while ( (*d++ = *s++)!= '\0') ; char *MemSir(char *s) char *p; if((p=(char *)malloc(strlen(s)+1))!=0) strcopy(p,s); return p; return NULL;
12.7. Pointeri către tablouri Evident un pointer va putea indica adresa de început a unei zone de memorie ce conţine un tablou. De exemplu, declaraţia unui pointer p către un tablou cu 10 elemente de tipul tip se scrie ca mai jos : tip (*p)[10]; Prezenţa parantezelor este obligatorie. Astfel de pointeri către tablouri vor avea ca unitate de operare în cazul operaţiilor cu pointeri numărul de octeţi necesari pentru memorarea tabloului (10*sizeof tip în cazul exemplului de mai sus).