Un pointer este o variabila care contine adresa unei alte variabile. Pointerii
sint foarte mult utilizati in programe scrise in C, pe de
o parte pentru ca uneori sint unicul mijloc de a exprima un calcul, iar
pe de alta parte pentru ca ofera posibilitatea scrierii unui program mai compact
si mai eficient decit ar putea fi obtinut prin alte cai.
9.1. Pointeri si adrese
Deoarece un pointer contine adresa unui obiect, cu ajutorul lui putem avea
acces, in mod indirect, la acea variabila (obiect). h4q12qk
Sa presupunem ca x este o variabila de tip intreg si px un pointer la
aceasta variabila. Atunci aplicind operatorul unar & lui x, instructiunea: px = &x; atribuie variabilei px adresa variabilei x; in acest fel spunem ca px
indica (pointeaza) spre x.
Invers, daca px contine adresa variabilei x, atunci instructiunea: y = *px; atribuie variabilei y continutul locatiei pe care o indica px.
Evident toate variabilele care sint implicate in aceste instructiuni
trebuie declarate. Aceste declaratii sint: int x,y; int *px;
Declaratiile variabilelor x si y sint deja cunoscute. Declaratia pointerului
px este o noutate. A doua declaratie indica faptul ca o combinatie de forma
*px este un intreg, iar variabila px care apare in contextul *px
este echivalenta cu un pointer la o variabila de tip intreg. In
locul tipului intreg poate aparea oricare dintre tipurile admise in
limbaj si se refera la obiectele pe care le indica px.
Pointerii pot aparea si in expresii, ca de exemplu in expresia urmatoare: y = *px + 1; unde variabilei y i se atribuie continutul variabilei x plus 1.
Instructiunea: d = sqrt((double)*px); are ca efect convertirea continutului variabilei x pe care o indica px in
tip double si apoi depunerea radacinii patrate a valorii astfel convertite in
variabila d.
Referiri la pointeri pot aparea de asemenea si in partea stinga
a atribuirilor. Daca, de exemplu, px indica spre x, atunci:
*px = 0; atribuie variabilei x valoarea zero, iar:
*px += 1; incrementeaza continutul variabilei x cu 1, ca si in expresia:
(*px)++;
In acest ultim exemplu parantezele sint obligatorii deoarece, in
lipsa lor, expresia ar incrementa pe px in loc de continutul variabilei
pe care o indica (operatorii unari *, ++ au aceeasi precedenta si sint
evaluati de la dreapta spre stinga).
9.2 Pointeri si argumente de functii
Deoarece in limbajul C transmiterea argumentelor la functii se face
„prin valoare” (si nu prin referinta), functia apelata nu are posibilitatea
de a altera o variabila din functia apelanta. Problema care se pune este cum
procedam daca totusi dorim sa schimbam un argument?
De exemplu, o rutina de sortare poate schimba intre ele doua elemente
care nu respecta ordinea dorita, cu ajutorul unei functii swap. Fie functia
swap definita astfel:
swap(int x, int y) A /* gresit */ int temp; temp = x; x = y; y = temp;
S
Functia swap apelata prin swap(a,b) nu va realiza actiunea dorita deoarece
ea nu poate afecta argumentele a si b din rutina apelanta.
Exista insa o posibilitate de a obtine efectul dorit, daca functia apelanta
transmite ca argumente pointeri la valorile ce se doresc interschimbate. Atunci
in functia apelanta apelul va fi: swap(&a,&b); iar forma corecta a lui swap este:
swap(int *px, int *py) A/* interschimba *px si *py */ int temp; temp = *px;
*px = *py;
*py = temp;
S
9.3. Pointeri si masive
In limbajul C exista o strinsa legatura intre pointeri si
masive. Orice operatie care poate fi realizata prin indicarea masivului poate
fi de asemenea facuta prin pointeri, care, in plus, conduce si la o accelerare
a operatiei. Declaratia: int aa10i; defineste un masiv de dimensiune 10, care reprezinta un bloc de 10 obiecte consecutive
numite aa0i, ... aa9i. Notatia aaii reprezinta al i-lea element al masivului
sau elementul din pozitia i?1, incepind cu primul element. Daca
pa este un pointer la un intreg declarat sub forma: int *pa; atunci atribuirea: pa = &aa0i;
incarca variabila pa cu adresa primului element al masivului a.
Atribuirea: x = *pa; copiaza continutul lui aa0i in x.
Daca pa indica un element particular al unui masiv a, atunci prin definitie
pa+i indica un element cu i pozitii dupa elementul pe care il indica pa,
dupa cum pa-i indica un element cu i pozitii inainte de cel pe care indica
pa. Astfel, daca variabila pa indica pe aa0i atunci *(pa+i) se refera la continutul
lui aaii.
Aceste observatii sint adevarate indiferent de tipul variabilelor din
masivul a.
Intreaga aritmetica cu pointeri are in vedere faptul ca expresia
pa+i inseamna de fapt inmultirea lui i cu lungimea elementului pe
care il indica pa si adunarea apoi la pa, obtinindu-se astfel adresa
elementului de indice i al masivului.
Corespondenta dintre indexarea intr-un masiv si aritmetica de pointeri
este foarte strinsa. De fapt, o referire la un masiv este convertita de
compilator intr-un pointer la inceputul masivului. Efectul este
ca un nume de masiv este o expresie pointer, deoarece numele unui masiv este
identic cu numele elementului de indice zero din masiv.
Atribuirea: pa = &aa0i; este identica cu: pa = a;
De asemenea, expresiile aaii si *(a+i) sint identice. Aplicind operatorul
& la ambele parti obtinem &aaii identic cu a+i. Pe de alta parte, daca
pa este un pointer, expresiile pot folosi acest pointer ca un indice: paaii
este identic cu *(pa+i). Pe scurt orice expresie de masiv si indice poate fi
scrisa ca un pointer si un deplasament si invers, chiar in aceeasi instructiune.
Exista insa o singura diferenta intre un nume de masiv si un pointer
la inceputul masivului. Un pointer este o variabila, deci pa = a si pa++
sint instructiuni corecte. Dar un nume de masiv este o constanta si deci
constructii de forma a = pa, a++ sau p = &a sint ilegale.
Cind se transmite unei functii un nume de masiv, ceea ce se transmite
de fapt este adresa primului element al masivului. Asadar, un nume de masiv,
argument al unei functii, este in realitate un pointer, adica o variabila
care contine o adresa. Fie de exemplu functia strlen care calculeaza lungimea
sirului s:
strlen(char *s) A /* returneaza lungimea sirului */ int n; for (n=0; *s!='\0'; s++) n++; return n;
S
Incrementarea lui s este legala deoarece s este o variabila pointer. s++ nu
afecteaza sirul de caractere din functia care apeleaza pe strlen, ci numai copia
adresei sirului din functia strlen.
Este posibil sa se transmita unei functii, ca argument, numai o parte a unui
masiv, printr-un pointer la inceputul sub-masivului respectiv. De exemplu,
daca a este un masiv, atunci:
f(&aa2i) f(a+2)
transmit functiei f adresa elementului aa2i, deoarece &aa2i si a+2 sint
expresii pointer care, ambele, se refera la al treilea element al masivului
a. In cadrul functiei f argumentul se poate declara astfel: f(int arrai) A S sau f(int *arr) A S
Declaratiile int arrai si int *arr sint echivalente, optiunea pentru una
din aceste forme depinzind de modul in care vor fi scrise expresiile
in interiorul functiei.
9.4. Aritmetica de adrese
Daca p este un pointer, atunci p += i incrementeaza pe p pentru a indica cu
i elemente dupa elementul pe care il indica in prealabil p. Aceasta
constructie si altele similare sint cele mai simple si comune formule
ale aritmeticii de adrese, care constituie o caracteristica puternica a limbajului
C. Sa ilustram citeva din proprietatile aritmeticii de adrese scriind
un alocator rudimentar de memorie. Fie rutina alloc(n) care returneaza un pointer
p la o zona de n caractere consecutive care vor fi folosite de rutina apelanta
pentru memorarea unui sir de caractere. Fie rutina free(p) care elibereaza o
zona incepind cu adresa indicata de pointerul p pentru a putea fi
refolosita mai tirziu. Zona de memorie folosita de rutinele alloc si free
este o stiva functionind pe principiul ultimul intrat ? primul iesit,
iar apelul la free trebuie facut in ordine inversa cu apelul la alloc.
Sa consideram ca functia alloc va gestiona stiva ca pe elementele unui masiv
pe care il vom numi allocbuf. Vom mai folosi un pointer la urmatorul element
liber din masiv, pe care-l vom numi allocp. Cind se apeleaza rutina alloc
pentru n caractere, se verifica daca exista suficient spatiu liber in
masivul allocbuf. Daca da, alloc va returna valoarea curenta a lui allocp, adica
adresa de inceput a blocului cerut, dupa care va incrementa pe allocp
cu n pentru a indica urmatoarea zona libera. free(p) actualizeaza allocp cu
valoarea p, daca p indica in interiorul lui allocbuf.
#define NULL 0
/* valoarea pointerului pentru semnalizarea erorii */
#define ALLOCSIZE 1000
/* dimensiunea spatiului disponibil */ static char allocbuf aALLOCSIZEi;
/* memoria pentru alloc */ static char *allocp = allocbuf;
/* urmatoarea pozitie libera */
char *alloc(int n) A
/* returneaza pointer la n caractere */ if (allocp+n<=allocbuf+ALLOCSIZE) A allocp += n; /* dimensiunea satisfacuta */ return allocp-n; /* vechea valoare */
S else return NULL; /* nu este spatiu suficient */
S
free(char *p) A /* elibereaza memoria indicata de p */ if (p>=allocbuf && p<allocbuf+ALLOCSIZE) allocp = p;
S
Testul if (allocp+n<=allocbuf+ALLOCSIZE) verifica daca exista spatiu suficient pentru satisfacerea cererii de alocare
a n caractere. Daca cererea poate fi satisfacuta, alloc revine cu un pointer
la zona de n caractere consecutive. Daca nu, alloc trebuie sa semnaleze lipsa
de spatiu pe care o face returnind valoarea constantei simbolice NULL.
Limbajul C garanteaza ca nici un pointer care indica corect o data nu va contine
zero, prin urmare o revenire cu valoarea zero poate fi folosita pentru semnalarea
unui eveniment anormal (in cazul nostru, lipsa de spatiu). Atribuirea
valorii zero unui pointer este deci un caz special.
Observam de asemenea ca variabilele allocbuf si allocp sint declarate
static cu scopul ca ele sa fie locale numai fisierului sursa care contine functiile
alloc si free.
Exemplul de mai sus demonstreaza citeva din facilitatile aritmeticii de
adrese (pointeri). In primul rind, pointerii pot fi comparati in
anumite situatii. Daca p si q sint pointeri la membri unui acelasi masiv,
atunci relatiile <, <=, >, >=, ==, != sint valide. Relatia
p<q, de exemplu, este adevarata daca p indica un element mai apropiat de
inceputul masivului decit elementul indicat de pointerul q. Compararile
intre pointeri pot duce insa la rezultate imprevizibile, daca ei
se refera la elemente apartinind la masive diferite.
Se observa ca pointerii si intregii pot fi adunati sau scazuti. Constructia
de forma: p+n
inseamna adresa celui de-al n-lea element dupa cel indicat de p, indiferent
de tipul elementului pe care il indica p. Compilatorul C aliniaza valoarea
lui n conform dimensiunii elementelor pe care le indica p, dimensiunea fiind
determinata din declaratia lui p (scara de aliniere este 1 pentru char, 2 pentru
int etc).
Daca p si q indica elemente ale aceluiasi masiv, p-q este numarul elementelor
dintre cele pe care le indica p si q. Sa scriem o alta versiune a functiei strlen
folosind aceasta ultima observatie:
strlen(char *s) A /* returneaza lungimea unui sir */ char *p; p = s;
while (*p != '\0') p++; return p-s;
S
In acest exemplu s ramine constant cu adresa de inceput
a sirului, in timp ce p avanseaza la urmatorul caracter de fiecare data.
Diferenta p-s dintre adresa ultimului element al sirului si adresa primului
element al sirului indica numarul de elemente.
In afara de operatiile binare mentionate (adunarea sau scaderea pointerilor
cu intregi si scaderea sau compararea a doi pointeri), celelalte operatii
cu pointeri sint ilegale. Nu este permisa adunarea, inmultirea,
impartirea sau deplasarea pointerilor, dupa cum nici adunarea lor cu constante
de tip double sau float.
Sint admise de asemenea incrementarile si decrementarile precum si alte
combinatii ca de exemplu *++p si *--p.
9.5. Pointeri la caracter si functii
O constanta sir, de exemplu:
"Buna dimineata" este un masiv de caractere, care in reprezentarea interna este terminat
cu caracterul '\0', astfel incit programul poate depista sfirsitul
lui. Lungimea acestui sir in memorie este astfel cu 1 mai mare decit
numarul de caractere ce apar efectiv intre ghilimelele de inceput
si sfirsit de sir.
Cea mai frecventa aparitie a unei constante sir este ca argument la functii,
caz in care accesul la ea se realizeaza prin intermediul unui pointer.
In exemplul: printf("Buna dimineata\n"); functia printf primeste de fapt un pointer la masivul de caractere.
In prelucrarea unui sir de caractere sint implicati numai pointeri,
limbajul C neoferind nici un operator care sa trateze sirul de caractere ca
o unitate de informatie.
Vom prezenta citeva aspecte legate de pointeri si masive analizind
doua exemple. Sa consideram pentru inceput functia strcpy(s,t) care copiaza
sirul t peste sirul s. O prima versiune a programului ar fi urmatoarea:
strcpy(char sai, char tai) A/* copiaza t peste s */ int t; i = 0;
while ((saii=taii) != '\0') i++;
S
O a doua versiune cu ajutorul pointerilor este urmatoarea:
strcpy(char *s, char *t) A
/* versiune cu pointeri */
while ((*s++=*t++) != '\0') ;
S
Aceasta versiune cu pointeri modifica prin incrementare pe s si t in
partea de test. Valoarea lui *t++ este caracterul indicat de pointerul t, inainte
de incrementare. Notatia postfix ++ asigura ca t va fi modificat dupa depunerea
continutului indicat de el, la vechea pozitie a lui s, dupa care si s se incrementeaza.
Efectul este ca se copiaza caracterele sirului t in sirul s pina
la caracterul terminal '\0' inclusiv.
Am mai putea face o observatie legata de redundanta compararii cu caracterul
'\0', redundanta care rezulta din structura instructiunii while.
Si atunci forma cea mai prescurtata a functiei strcpy(s,t) este:
strcpy(char *s, char *t) A
while (*s++ = *t++) ;
S
Sa consideram, ca al doilea exemplu, functia strcmp(s,t) care compara caracterele
sirurilor s si t si returneaza o valoare negativa, zero sau pozitiva, dupa cum
sirul s este lexicografic mai mic, egal sau mai mare ca sirul t. Valoarea returnata
se obtine prin scaderea caracterelor primei pozitii in care s si t difera.
O prima versiune a functiei strcmp(s,t) este urmatoarea:
strcmp(char s, char t) A/* compara sirurile s si t */ int i; i = 0;
while (saii==taii) if (sai++i=='\0') return 0; return saii-taii;
S
Versiunea cu pointeri a aceleiasi functii este:
strcmp(char *s, char *t) A for (; *s==*t; s++,t++) if (*s=='\0') return 0; return *s-*t;
S
In final prezentam functia strsav care copiaza un sir dat prin argumentul
ei intr-o zona obtinuta printr-un apel la functia alloc. Ea returneaza
un pointer la sirul copiat sau NULL, daca nu mai exista suficient spatiu pentru
memorarea sirului.
char *strsav(char *s) A /* copiaza sirul s */ char *p; p = alloc(strlen(s)+1); if (p!=NULL) strcpy(p,s); return p;
S
9.6. Masive multidimensionale
Limbajul C ofera facilitatea utilizarii masivelor multidimensionale, desi
in practica ele sint folosite mai putin decit masivele de
pointeri.
Sa consideram problema conversiei datei, de la zi din luna, la zi din an si
invers, tinind cont de faptul ca anul poate sa fie bisect sau nu. Definim
doua functii care sa realizeze cele doua conversii.
Functia day_of_year converteste ziua si luna in ziua anului si functia
month_day converteste ziua anului in luna si zi.
Ambele functii au nevoie de aceeasi informatie si anume un tabel cu numarul
zilelor din fiecare luna. Deoarece numarul zilelor din luna difera pentru anii
bisecti de cele pentru anii nebisecti este mai usor sa consideram un tabel bidimensional
in care prima linie sa corespunda numarului de zile ale lunilor pentru
anii nebisecti, iar a doua linie sa corespunda numarului de zile pentru anii
bisecti. In felul acesta nu trebuie sa tinem o evidenta in timpul
calculului a ceea ce se intimpla cu luna februarie. Atunci masivul
bidimensional care contine informatiile pentru cele doua functii este urmatorul:
static int day_taba2ia13i = A
A0,31,28,31,30,31,30,31,31,30,31,30,31S,
A0,31,29,31,30,31,30,31,31,30,31,30,31S
S;
Masivul day_tab trebuie sa fie declarat extern pentru a putea fi folosit de
ambele functii.
In limbajul C, prin definitie, un masiv cu doua dimensiuni este in
realitate un masiv cu o dimensiune ale carui elemente sint masive. De
aceea indicii se scriu sub forma aiiaji in loc de ai,ji, cum se procedeaza
in cele mai multe limbaje. Un masiv bidimensional poate fi tratat in
acelasi fel ca si in celelalte limbaje, in sensul ca elementele
sint memorate pe linie, adica indicele cel mai din dreapta variaza cel
mai rapid.
Un masiv se initializeaza cu ajutorul unei liste de initializatori inchisi
intre acolade; fiecare linie a unui masiv bidimensional se initializeaza
cu ajutorul unei subliste de initializatori. In cazul exemplului nostru,
masivul day_tab incepe cu o coloana zero, pentru ca numerele lunilor sa
fie intre 1 si 12 si nu intre 0 si 11, aceasta pentru a nu face
modificari in calculul indicilor.
Si atunci functiile care realizeaza conversiile cerute de exemplul nostru sint:
day_of_year (int year, int month, int day)
A /* ziua anului din luna si zi */ int i, leap; leap = (year%4==0) && (year%100!=0) ||
(year%400==0); for (i=1; i<month; i++) day += day_tabaleapiaii; return day;
S
Deoarece variabila leap poate lua ca valori numai zero sau unu dupa cum expresia:
(year%4==0) && (year%100!=0) ||
(year%400==0)
este falsa sau adevarata, ea poate fi folosita ca indice de linie in
tabelul day_tab care are doar doua linii in exemplul nostru.
month_day(int year, int yearday, int *pmonth, int *pday) A int i,leap; leap = (year%4==0) && (year%100!=0) ||
(year%400==0); for (i=1; yearday>day_tabaleapiaii; i++) yearday -= day_tabaleapiaii;
*pmonth = i;
*pday = yearday;
S
Deoarece aceasta ultima functie returneaza doua valori, argumentele luna si
zi vor fi pointeri.
Exemplu: month_day(1984,61,&m,&d) va incarca pe m cu 3, iar pe
d cu 1 (adica 1 martie).
Daca un masiv bidimensional trebuie transmis unei functii, declaratia argumentelor
functiei trebuie sa includa dimensiunea coloanei. Dimensiunea liniei nu este
necesar sa apara in mod obligatoriu, deoarece ceea ce se transmite de
fapt este un pointer la masive de cite 13 intregi, in cazul
exemplului nostru. Astfel, daca masivul day_tab trebuie transmis unei functii
f, atunci declaratia lui f poate fi: f(int (*day_tab)a13i) unde declaratia (*day_tab)a13i) indica faptul ca argumentul lui f este un pointer
la un masiv de 13 intregi.
In general deci, un masiv d-dimensional aaiiaji...api de rangul i*j*...*p
este un masiv d?1 - dimensional de rangul j*k*...*p ale carui elemente, fiecare,
sint masive d?2 - dimensionale de rang k*...*p ale carui elemente, fiecare,
sint masive d?3 - dimensionale s.a.m.d. Oricare dintre expresiile aaii,
aaiiaji..., aaiiaji... api pot aparea in expresii. Prima are tipul masiv,
ultima are tipul int, de exemplu, daca masivul este de tipul int. Vom mai reveni
asupra acestei probleme cu detalii.
9.7. Masive de pointeri si pointeri la pointeri
Deoarece pointerii sint variabile, are sens notiunea de masiv de pointeri.
Vom ilustra modul de lucru cu masive de pointeri pe un exemplu.
Sa scriem un program care sa sorteze lexicografic liniile de lungimi diferite
ale unui text, linii care spre deosebire de intregi nu pot fi comparate
sau schimbate printr-o singura operatie.
Daca memoram liniile textului una dupa alta intr-un masiv lung de caractere
(gestionat de functia alloc), atunci fiecare linie poate fi accesibila cu ajutorul
unui pointer la primul ei caracter. Pointerii tuturor liniilor, la rindul
lor, pot fi memorati sub forma unui masiv. Atunci doua linii de text pot fi
comparate transmitind pointerii lor functiei strcmp. Daca doua linii care
nu respecta ordinea trebuie sa fie schimbate, se schimba doar pointerii lor
din masivul de pointeri si nu textul efectiv al liniilor.
Procesul de sortare il vom realiza in trei pasi:
1) se citesc toate liniile textului de la intrare;
2) se sorteaza liniile in ordine lexicografica;
3) se tiparesc liniile sortate in noua ordine.
Vom scrie programul prin functiile sale, fiecare functie realizind unul
din cei trei pasi de mai sus. O rutina principala va controla cele trei functii.
Ea are urmatorul cod:
#define LINES 100 /* nr maxim de linii de sortat */ main() A /* sorteaza liniile de la intrare */ char *lineptraLINESi; /* pointeri la linii */ int nlines; /* nr linii intrare citite */ if ((nlines=readlines(lineptr,LINES))>=0)
A sort(lineptr,nlines);
writelines(lineptr,nlines);
S else printf
("Intrarea prea mare pentru sort\n");
S
Cele 3 functii care realizeaza intregul proces sint: readlines,
sort si writelines.
Rutina de intrare readlines trebuie sa memoreze caracterele fiecarei linii
si sa construiasca un masiv de pointeri la liniile citite. Trebuie, de asemenea,
sa numere liniile din textul de la intrare, deoarece aceasta informatie este
necesara in procesul de sortare si de imprimare. Intrucit
functia de intrare poate prelucra numai un numar finit de linii de intrare,
ea poate returna un numar ilegal, cum ar fi ?1, spre a semnala ca numarul liniilor
de intrare este prea mare pentru capacitatea de care dispune.
Atunci functia readlines care citeste liniile textului de la intrare este urmatoarea:
#define MAXLEN 1000
#define NULL 0
#define EOF -1 readlines(char *lineptrai, int maxlines) A
/* citeste liniile */ int len,nlines; char *p,*alloc(),lineaMAXLENi; nlines = 0;
while ((len=getline(line,MAXLEN))>0) if (nlines>=maxlines) return -1; else if ((p=alloc(len))==NULL) return -1; else A linealen-1i = '\0'; strcpy(p,line); lineptranlines++i = p;
S return nlines;
S
Instructiunea linealen-1i = '\0'; sterge caracterul ?LF? de la sfirsitul
fiecarei linii ca sa nu afecteze ordinea in care sint sortate liniile
si depune in locul lui caracterul '\0' ca marca de sfirsit de sir.
Rutina care tipareste liniile in noua lor ordine este writelines si
are urmatorul cod:
writelines(char *lineptrai, int nlines) A
/* scrie liniile sortate */ int i; for (i=0; i<nlines; i++) printf("%s\n",lineptraii);
S
Declaratia noua care apare in aceste programe este: char *lineptraLINESi; care indica faptul ca lineptr este un masiv de LINES elemente, fiecare element
al masivului fiind un pointer la un caracter. Astfel lineptraii este un pointer
la un caracter, iar *lineptraii permite accesul la caracterul respectiv.
Deoarece lineptr este el insusi un masiv, care se transmite ca argument
functiei writelines, el va fi tratat ca un pointer (vezi sectiunea 9.3) si atunci
functia writelines mai poate fi scrisa si astfel:
writelines(char *lineptrai, int nlines) A
while (--nlines>=0) printf("%s\n",*lineptr++);
S
In functia printf, lineptr indica initial prima linie de imprimat; fiecare
incrementare avanseaza pe *lineptr la urmatoarea linie de imprimat, in
timp ce nlines se micsoreaza dupa fiecare tiparire a unei linii cu 1.
Functia care realizeaza sortarea efectiva a liniilor se bazeaza pe algoritmul
de injumatatire si are urmatorul cod:
#define NULL 0
#define LINES 100 /* nr maxim de linii de sortat */ sort(char *vai, int n) A
/* sorteaza sirurile v0, v1, ... vn-1 in ordine crescatoare */ int gap,i,j; char *temp; for (gap=n/2; gap>0; gap/=2) for (i=gap; i<n; i++) for (j=i-gap; j>=0; j-=gap) A if (strcmp(vaji,vaj+gapi)<=0) break; temp = vaji; vaji = vaj+gapi; vaj+gapi = temp;
S
S
Deoarece fiecare element al masivului v (care este de fapt masivul lineptr)
este un pointer la primul caracter al unei linii, variabila temp va fi si ea
un pointer la un caracter, deci operatiile de atribuire din ciclu dupa variabila
j sint admise si ele realizeaza reinversarea pointerilor la linii daca
ele nu sint in ordinea ceruta.
Sa retinem deci urmatoarele lucruri legate de masive si pointeri. De cite
ori apare intr-o expresie un identificator de tip masiv el este convertit
intr-un pointer la primul element al masivului. Prin definitie, operatorul
de indexare ai este interpretat astfel incit E1aE2i este identic
cu *((E1)+(E2)). Daca E1 este un masiv, iar E2 un intreg, atunci E1aE2i
se refera la elementul de indice E2 al masivului E1.
O regula corespunzatoare se aplica si masivelor multi-dimensionale. Daca E1
este un masiv d-dimensional, de rangul i*j*...*k, atunci ori de cite ori
e1 apare intr-o expresie, e1 va fi convertit intr-un pointer la
un masiv d?1 - dimensional de rangul j*...*k, ale carui elemente sint
masive. Daca operatorul * se aplica acestui pointer, rezultatul este masivul
d?1 - dimensional, care se va converti imediat intr-un pointer la un masiv
d?2 - dimensional s.a.m.d. Rationamentul se poate aplica in mod inductiv
pina cind, in final, ca urmare a aplicarii operatorului *
se obtine ca rezultat un intreg, de exemplu, daca masivul a fost declarat
de tipul int.
Sa consideram, de exemplu, masivul: int xa3ia5i; x este un masiv de intregi, de rangul 3*5. Cind x apare intr-o
expresie, el este convertit intr-un pointer la (primul din cele trei)
masive de 5 intregi.
In expresia xaii, care este echivalenta cu expresia *(x+i), x este convertit
intr-un pointer la un masiv, ale carui elemente sint la rindul
lor masive de 5 elemente; apoi i se converteste la tipul x, adica indicele i
se inmulteste cu lungimea elementului pe care il indica x (adica
5 intregi) si apoi rezultatele se aduna. Se aplica operatorul * pentru
obtinerea masivului i (de 5 intregi) care la rindul lui este convertit
intr-un pointer la primul intreg din cei cinci.
Se observa deci ca primul indice din declaratia unui masiv nu joaca rol in
calculul adresei.
9.8. Initializarea masivelor si masivelor de pointeri
Initializatorul unei variabile declarate masiv consta dintr-o lista de initializatori
separati prin virgula si inchisi intre acolade, corespunzatori tuturor
elementelor masivului. Ei sint scrisi in ordinea crescatoare a indicilor
masivului. Daca masivul contine sub-masive atunci regula se aplica recursiv
membrilor masivului. Daca in lista de initializare exista mai putini initializatori
decit elementele masivului, restul elementelor neinitializate se initializeaza
cu zero. Nu se admite initializarea unui masiv de clasa cu automatic.
Acoladele A si S se pot omite in urmatoarele situatii:
-; daca initializatorul incepe cu o acolada stinga (A), atunci
lista de initializatori, separati prin virgula, va initializa elementele masivului;
nu se accepta sa existe mai multi initializatori decit numarul elementelor
masivului;
-; daca insa initializatorul nu incepe cu acolada stinga
(A), atunci se iau din lista de initializatori atitia initializatori citi
corespund numarului de elemente ale masivului, restul initializatorilor vor
initializa urmatorul membru al masivului, care are ca parte (sub-masiv) masivul
deja initializat.
Un masiv de caractere poate fi initializat cu un sir, caz in care caracterele
succesive ale sirului initializeaza elementele masivului.
Exemple:
1) int xai = A1,3,5S;
Aceasta declaratie defineste si initializeaza pe x ca un masiv unidimensional
cu trei elemente, in ciuda faptului ca nu s-a specificat dimensiunea masivului.
Prezenta initializatorilor inchisi intre acolade determina dimensiunea
masivului.
2) Declaratia int ya4ia3i=A
A1,3,5S,
A2,4,6S,
A3,5,7S,
S; este o initializare complet inchisa intre acolade. Valorile 1,3,5
initializeaza prima linie a masivului ya0i si anume pe ya0ia0i, ya0ia1i, ya0ia2i.
In mod analog urmatoarele doua linii initializeaza pe ya1i si ya2i. Deoarece
initializatorii sint mai putini decit numarul elementelor masivului,
linia ya3i se va initializa cu zero, respectiv elementele ya3ia0i, ya3ia1i,
ya3ia2i vor avea valorile zero.
3) Acelasi efect se poate obtine din declaratia: int ya4ia3i = A1,3,5,2,4,6,3,5,7S; unde initializatorul masivului y incepe cu acolada stinga in
timp ce initializatorul pentru masivul ya0i nu, fapt pentru care primii trei
initializatori sint folositi pentru initializarea lui ya0i, restul initializatorilor
fiind folositi pentru initializarea masivelor ya1i si respectiv ya2i.
4) Declaratia: int ya4ia3i = A
A1S,A2,S,A3,S,A4S
S; initializeaza masivul ya0i cu (1,0,0), masivul ya1i cu (2,0,0), masivul ya2i
cu (3,0,0) si masivul ya4i cu (4,0,0).
5) Declaratia: static char msgai = "Eroare de sintaxa"; initializeaza elementele masivului de caractere msg cu caracterele succesive
ale sirului dat.
In ceea ce priveste initializarea unui masiv de pointeri sa consideram
urmatorul exemplu.
Fie functia month_name care returneaza un pointer la un sir de caractere care
indica numele unei luni a anului. Functia data contine un masiv de siruri de
caractere si returneaza un pointer la un astfel de sir, cind ea este apelata.
Codul functiei este urmatorul:
char *month_name(int n) A
/* returneaza numele lunii a n-a */ static char *nameai = A
"luna ilegala", "ianuarie",
"februarie", "martie", "aprilie",
"mai", "iunie", "iulie", "august",
"septembrie", "octombrie", "noiembrie",
"decembrie"
S return ((n<1) || (n>12)) ? namea0i : nameani ;
S
In acest exemplu, name este un masiv de pointeri la caracter, al carui
initializator este o lista de siruri de caractere. Compilatorul aloca o zona
de memorie pentru memorarea acestor siruri si genereaza cite un pointer
la fiecare din ele pe care apoi ii introduce in masivul name. Deci
nameaii va contine un pointer la sirul de caractere avind indice i al
initializatorului. Dimensiunea masivului name nu este necesar a fi specificata
deoarece compilatorul o calculeaza numarind initializatorii furnizati
si o completeaza in declaratia masivului.
9.9. Masive de pointeri si masive multidimensionale
Adesea se creeaza confuzii in ceea ce priveste diferenta dintre un masiv
bidimensional si un masiv de pointeri. Fie date declaratiile:
int aa10ia10i; int *ba10i;
In aceasta declaratie a este un masiv de intregi caruia i se aloca
spatiu pentru toate cele 100 de elemente, iar calculul indicilor se face in
mod obisnuit pentru a avea acces la oricare element al masivului.
Pentru masivul b, declaratia aloca spatiu numai pentru zece pointeri, fiecare
trebuind sa fie incarcat cu adresa unui masiv de intregi.
Presupunind ca fiecare pointer indica un masiv de zece elemente inseamna
ca ar trebui alocate inca o suta de locatii de memorie pentru elementele
masivelor.
In aceasta acceptiune, folosirea masivelor a si b poate fi similara in
sensul ca aa5ia5i si ba5ia5i, de exemplu, se refera ambele la unul si acelasi
intreg (daca fiecare element baii este initializat cu adresa masivului
aaii).
Astfel, masivul de pointeri utilizeaza mai mult spatiu de memorie decit
masivele bidimensionale si pot cere un pas de initializare explicit. Dar masivele
de pointeri prezinta doua avantaje, si anume: accesul la un element se face
cu adresare indirecta, prin intermediul unui pointer, in loc de procedura
obisnuita folosind inmultirea si apoi adunarea, iar al doilea avantaj
consta in aceea ca dimensiunea masivelor pointate poate fi variabila.
Acest lucru inseamna ca un element al masivului de pointeri b poate indica
un masiv de zece elemente, altul un masiv de doua elemente si altul de exemplu
poate sa nu indice nici un masiv.
Cu toate ca problema prezentata in acest paragraf am descris-o in
termenii intregilor, ea este cel mai frecvent utilizata in memorarea
sirurilor de caractere de lungimi diferite (ca in functia month_name prezentata
mai sus).
9.10. Argumentele unei linii de comanda
In sistemul de calcul care admite limbajul C trebuie sa existe posibilitatea
ca in momentul executiei unui program scris in acest limbaj sa i
se transmita acestuia argumente sau parametri prin linia de comanda. Cind
un program este lansat in executie si functia main este apelata, apelul
va contine doua argumente. Primul argument (numit conventional argc) reprezinta
numarul de argumente din linia de comanda care a lansat programul. Al doilea
argument (argv) este un pointer la un masiv de pointeri la siruri de caractere
care contin argumentele, cite unul pe sir.
Sa ilustram acest mod dinamic de comunicare intre utilizator si programul
sau printr-un exemplu.
Fie programul numit pri care dorim sa imprime la terminal argumentele lui luate
din linia de comanda, imprimarea facindu-se pe o linie, iar argumentele
imprimate sa fie separate prin spatii.
Comanda: pri succes colegi va avea ca rezultat imprimarea la terminal a textului succes colegi
Prin conventie, argva0i este un pointer la numele pri al programului apelat,
astfel ca argc, care specifica numarul de argumente din linia de comanda este
cel putin 1.
In exemplul nostru, argc este 3, iar argva0i, argva1i si argva2i sint
pointeri la "pri", "succes" si respectiv "colegi".
Primul argument real este argva1i iar ultimul este argvaargc-1i. Daca argc este
1, inseamna ca linia de comanda nu are nici un argument dupa numele programului.
Atunci programul pri are urmatorul cod:
main(int argc, char *argvai) A
/* tipareste argumentele */ int i; for (i=1; i<argc; i++) printf("%s%%c",argvaii,(i<argc-1)?
' ':'\n');
S
Deoarece argv este un pointer la un masiv de pointeri, exista mai multe posibilitati
de a scrie acest program. Sa mai scriem doua versiuni ale acestui program.
main(int argc, char *argvai) A
/* versiunea a doua */
while (--argc>0) printf("%s%c",*++argv,(argv>1)?
' ':'\n');
S
Deoarece argv este un pointer la un masiv de pointeri, incrementindu-l,
(++argv), el va pointa la argva1i in loc de argva0i. Fiecare incrementare
succesiva pozitioneaza pe argv la urmatorul argument, iar *argv este pointerul
la argumentul sirului respectiv. In acelasi timp argc este decrementat
pina devine zero, moment in care nu mai sint argumente de
imprimat.
Alternativ:
main(int argc, char *argva i) A
/* versiunea a treia */
while (--argc>0) printf((argc>1)? "%s ":"%s\n",*++argv);
S
Aceasta versiune arata ca argumentul functiei printf poate fi o expresie ca
oricare alta, cu toate ca acest mod de utilizare nu este foarte frecvent.
Ca un al doilea exemplu, sa reconsideram programul din sectiunea 7.5, care imprima
fiecare linie a unui text care contine un sir specificat de caractere (schema).
Dorim acum ca aceasta schema sa poata fi modificata dinamic, de la executie
la executie. Pentru aceasta o specificam printr-un argument in linia de
comanda.
Si atunci programul care cauta schema data de primul argument al liniei de comanda
este:
#define MAXLINE 1000 main(int argc, char *argva i) A
/* gaseste schema din primul argument */ char lineaMAXLINEi; if (argc!=2) printf("Linia de comanda eronata\n"); else
while (getline(line,MAXLINE)>0) if (index(line,argva1i)>=0) printf("%s",line);
S
unde linia de comanda este de exemplu: "find limbaj" in care
"find" este numele programului, iar "limbaj" este schema
cautata. Rezultatul va fi imprimarea tuturor liniilor textului de intrare care
contin cuvintul "limbaj".
Sa elaboram acum modelul de baza, legat de linia de comanda si argumentele ei.
Sa presupunem ca dorim sa introducem in linia de comanda doua argumente
optionale: unul care sa tipareasca toate liniile cu exceptia acelora care contin
schema, si al doilea care sa preceada fiecare linie tiparita cu numarul ei de
linie.
O conventie pentru programele scrise in limbajul C este ca argumentele
dintr-o linie de comanda care incep cu un semn '-' sa introduca un parametru
optional. Daca alegem, de exemplu, -x pentru a indica „cu exceptia”
si -n pentru a cere „numararea liniilor”, atunci comanda: find -x -n la avind intrarea:
la miezul stinselor lumini s-ajung victorios, la temelii, la radacini, la maduva, la os.
va produce tiparirea liniei a doua, precedata de numarul ei, deoarece aceasta
linie nu contine schema "la".
Argumentele optionale sint permise in orice ordine in linia
de comanda. Analizarea si prelucrarea argumentelor unei linii de comanda trebuie
efectuata in functia principala main, initializind in mod
corespunzator anumite variabile. Celelalte functii ale programului nu vor mai
tine evidenta acestor argumente.
Este mai comod pentru utilizator daca argumentele optionale sint concatenate,
ca in comanda: find -xn la
Caracterele 'x' respectiv 'n' indica doar absenta sau prezenta acestor optiuni
(switch) si nu sint tratate din punct de vedere al valorii lor.
Fie programul care cauta schema "la" in liniile de la intrare
si le tipareste pe acelea, care nu contin schema, precedate de numarul lor de
linie. Programul trateaza corect, atit prima forma a liniei de comanda
cit si a doua.
#define MAXLINE 1000
main(int argc, char *argvai) A
/* cauta schema */ char lineaMAXLINEi, *s; long line0; int except, number; line0 = 0; number = 0;
while (--argc>0 && (*++argv)a0i=='-') for (s=argva0i+1; *s!='\0'; s++) switch(*s) A case 'x': except = 1; break; case 'n': number = 1; break; default: printf
("find: optiune ilegala %c\n",
*s); argc = 0; break;
S if (argc!=1) printf
("Nu exista argumente sau schema\n"); else
while (getline(line,MAXLINE)>0) A line0++; if ((index(line,*argv)>=0)!=except)
A if (number) printf("%d:",line0); printf("%s",line);
S
S
S
Daca nu exista erori in linia de comanda, atunci la sfirsitul
primului ciclu while argc trebuie sa fie 1, iar *argv contine adresa schemei.
*++argv este un pointer la un sir argument, iar (*++argv)a0i este primul caracter
al sirului. In aceasta ultima expresie parantezele sint necesare
deoarece fara ele expresia inseamna *++(argva0i) ceea ce este cu totul
altceva (si gresit): al doilea caracter din numele programului. O alternativa
corecta pentru (*++argva0i) este **++argv.
9.11. Pointeri la functii
In limbajul C o functie nu este o variabila, dar putem defini un pointer
la o functie, care apoi poate fi prelucrat, transmis unor alte functii, introdus
intr-un masiv si asa mai departe. Relativ la o functie se pot face doar
doua operatii: apelul ei si considerarea adresei ei. Daca numele unei functii
apare intr-o expresie, fara a fi urmat imediat de o paranteza stinga,
deci nu pe pozitia unui apel la ea, atunci se genereaza un pointer la aceasta
functie. Pentru a transmite o functie unei alte functii, ca argument, se poate
proceda in felul urmator: int f(); g(f); unde functia f este un argument pentru functia g. Definitia functiei g va fi: g(int(*funcpt) ()) A
(*funcpt)();
S
Functia f trebuie declarata explicit in rutina apelanta (int f();), deoarece
aparitia ei in g(f) nu a fost urmata de paranteza stinga ’(’.
In expresia g(f) f nu apare pe pozitia de apel de functie. In acest
caz, pentru argumentul functiei g se genereaza un pointer la functia f. Deci
g apeleaza functia f printr-un pointer la ea.
Declaratiile din functia g trebuie studiate cu grija. int (*funcpt)(); spune ca funcpt este un pointer la o functie care returneaza un intreg.
Primul set de paranteze este necesar, deoarece fara el int *funcpt();
inseamna ca funcpt este o functie care returneaza un pointer la un intreg,
ceea ce este cu totul diferit fata de sensul primei expresii. Folosirea lui
funcpt in expresia:
(*funcpt)(); indica faptul ca funcpt este un pointer la o functie, *funcpt este functia,
iar (*funcpt)() este apelul functiei.
O forma echivalenta simplificata de apel este urmatoarea: funcpt();
Ca un exemplu, sa consideram procedura de sortare a liniilor de la intrare,
descrisa in sectiunea 9.7, dar modificata in sensul ca daca argumentul
optional -n apare in linia de comanda, atunci liniile se vor sorta nu
lexicografic ci numeric, liniile continind grupe de numere.
O sortare consta adesea din trei parti: o comparare care determina ordinea oricarei
perechi de elemente, un schimb care inverseaza ordinea elementelor implicate
si un algoritm de sortare care face compararile si inversarile pina cind
elementele sint aduse in ordinea ceruta. Algoritmul de sortare este
independent de operatiile de comparare si inversare, astfel incit
transmitind diferite functii de comparare si inversare functiei de sortare,
elementele de intrare se pot aranja dupa diferite criterii.
Compararea lexicografica a doua linii se realizeaza prin functiile strcmp si
swap. Mai avem nevoie de o rutina numcmp care sa compare doua linii pe baza
valorilor numerice si care sa returneze aceiasi indicatori ca si rutina strcmp.
Declaram aceste trei functii in functia principala main, iar pointerii
la aceste functii ii transmitem ca argumente functiei sort, care la rindul
ei va apela aceste functii prin intermediul pointerilor respectivi.
Functia principala main va avea atunci urmatorul cod:
#define LINES 100 /* nr maxim de linii de sortat */
main (int argc, char *argvai) A char *lineptraLINESi; /* pointeri la linii text */ int nlines; /* numar de linii citite */ int strcmp(), numcmp(); /* functii de comparare */ int swap (); /* functia de inversare */ int numeric; numeric = 0; /* 1 daca sort numeric */ if (argc>1 && argva1ia0i=='-' && argva1ia1i=='n') numeric = 1; if ((nlines=readlines(lineptr,LINES))>=0)
A if (numeric) sort(lineptr,nlines,numcmp,swap); else sort(lineptr,nlines,strcmp,swap);
writelines (lineptr,nlines);
S else printf
("Nr de linii de intrare prea mare\n");
S
In apelul functiei sort, argumentele strcmp, numcmp si swap sint
adresele functiilor respective. Deoarece ele au fost declarate functii care
returneaza un intreg, operatorul ’&’ nu este necesar sa
preceada numele functiilor, compilatorul fiind cel care gestioneaza transmiterea
adreselor functiilor.
Functia sort care aranjeaza liniile in ordinea crescatoare se va modifica
astfel:
sort(char *vai, int n, int (*comp)(), int (*exch)()) A /* sorteaza v0, v1, ... , vn?1 */ int gap,i,j; for (gap=n/2; gap>0; gap/=2) for (i=gap; i<n; i++) for (j=i-gap; j>=0; j-=gap) A if (comp(vaji,vaj+gapi)<=0) break; exch(v+j,v+j+gap);
S
S
Sa studiem declaratiile din aceasta functie. int(*comp)(), (*exch)(); indica faptul ca comp si exch sint pointeri la functii care returneaza
un intreg (primul set de paranteze este necesar). if (comp(vaji,vaj+gapi)<=0)
inseamna apelul functiei comp (adica strcmp sau numcmp), deoarece comp
este un pointer la functie, *comp este functia, iar comp(vaji,vaj+gapi) este apelul functiei. exch(v+j,v+j+gap) este apelul functiei swap, de inversare a doua linii, inversare care realizeaza
interschimbarea adreselor liniilor implicate (vezi sectiunea 9.2). Functia numcmp
este urmatoarea:
numcmp(char *s1, char *s2) A
/* compara s1 si s2 numeric */ double atof(),v1,v2; v1 = atof(s1); v2 = atof(s2); if (v1<v2) return -1; else if (v1>v2) return 1; else return 0;
S
Pentru ca programul nostru sa fie complet sa mai prezentam si codul functiei
swap, care schimba intre ei pointerii a doua linii.
swap(char *pxai, char *pyai) A char *temp; temp = *px;
*px = *py;
*py = temp;
S