Noi te platim pentru timpul acordat , o postare , like , vizualizare subiect , sau creare subiect , insemna castig.
Suntem comunitatea unica din Romania care plateste pentru efortul depus . Ideile ,opiniile tale conteaza si sunt platite doar la noi.

Pointeri şi masive

Pointeri şi masive

Solus

Membru
Staff member
Fondator BitArena
Moderator
Utilizator
Joined
Jul 6, 2018
Messages
523
Reaction score
94
Points
26
Location
Bucuresti
Website
www.bitarena.eu

Reputation:

Un pointer este o variabilă care conţine adresa unei alte variabile. Pointerii sînt foarte mult utilizaţi în programe scrise în C, pe de o parte pentru că uneori sînt unicul mijloc de a exprima un calcul, iar pe de altă parte pentru că oferă posibilitatea scrierii unui program mai compact şi mai eficient decît ar putea fi obţinut prin alte căi.

Pointeri şi adrese

Deoarece un pointer conţine adresa unui obiect, cu ajutorul lui putem avea acces, în mod indirect, la acea variabilă (obiect).
Să presupunem că x este o variabilă de tip întreg şi px un pointer la această variabilă. Atunci aplicînd operatorul unar & lui x, instrucţiunea:
px = &x;
atribuie variabilei px adresa variabilei x; în acest fel spunem că px indică (pointează) spre x.
Invers, dacă px conţine adresa variabilei x, atunci instrucţiunea:
y = *px;
atribuie variabilei y conţinutul locaţiei pe care o indică px.
Evident toate variabilele care sînt implicate în aceste instrucţiuni trebuie declarate. Aceste declaraţii sînt:
int x,y;
int *px;
Declaraţiile variabilelor x şi y sînt deja cunoscute. Declaraţia pointerului px este o noutate. A doua declaraţie indică faptul că o combinaţie de forma *px este un întreg, iar variabila px care apare în contextul *pxeste echivalentă cu un pointer la o variabilă de tip întreg. În locul tipului întreg poate apărea oricare dintre tipurile admise în limbaj şi se referă la obiectele pe care le indică px.
Pointerii pot apărea şi în expresii, ca de exemplu în expresia următoare:
y = *px + 1;
unde variabilei y i se atribuie conţinutul variabilei x plus 1.
Instrucţiunea:
d = sqrt((double)*px);
are ca efect convertirea conţinutului variabilei x pe care o indică px în tip double şi apoi depunerea rădăcinii pătrate a valorii astfel convertite în variabila d.
Referiri la pointeri pot apărea de asemenea şi în partea stîngă a atribuirilor. Dacă, de exemplu, px indică spre x, atunci:
*px = 0;
atribuie variabilei x valoarea zero, iar:
*px += 1;
incrementează conţinutul variabilei x cu 1, ca şi în expresia:
(*px)++;
În acest ultim exemplu parantezele sînt obligatorii deoarece, în lipsa lor, expresia ar incrementa pe px în loc de conţinutul variabilei pe care o indică (operatorii unari *, ++ au aceeaşi precedenţă şi sînt evaluaţi de la dreapta spre stînga).

Pointeri şi argumente de funcţii

Deoarece în limbajul C transmiterea argumentelor la funcţii se face „prin valoare” (şi nu prin referinţă), funcţia apelată nu are posibilitatea de a altera o variabilă din funcţia apelantă. Problema care se pune este cum procedăm dacă totuşi dorim să schimbăm un argument?
De exemplu, o rutină de sortare poate schimba între ele două elemente care nu respectă ordinea dorită, cu ajutorul unei funcţii swap. Fie funcţia swap definită astfel:

swap(int x, int y) { /* greşit */
int temp;
temp = x;
x = y;
y = temp;
}

Funcţia swap apelată prin swap(a,b) nu va realiza acţiunea dorită deoarece ea nu poate afecta argumentele a şi b din rutina apelantă.
Există însă o posibilitate de a obţine efectul dorit, dacă funcţia apelantă transmite ca argumente pointeri la valorile ce se doresc interschimbate. Atunci în funcţia apelantă apelul va fi:
swap(&a,&b);
iar forma corectă a lui swap este:

swap(int *px, int *py) {/* interschimbă *px şi *py */
int temp;
temp = *px;
*px = *py;
*py = temp;
}

Pointeri şi masive

În limbajul C există o strînsă legătură între pointeri şi masive. Orice operaţie care poate fi realizată prin indicarea masivului poate fi de asemenea făcută prin pointeri, care, în plus, conduce şi la o accelerare a operaţiei. Declaraţia:
int a[10];
defineşte un masiv de dimensiune 10, care reprezintă un bloc de 10 obiecte consecutive numite a[0], ... a[9]. Notaţia a reprezintă al i-lea element al masivului sau elementul din poziţia i+1, începînd cu primul element. Dacă pa este un pointer la un întreg declarat sub forma:
int *pa;
atunci atribuirea:
pa = &a[0];
încarcă variabila pa cu adresa primului element al masivului a.
Atribuirea:
x = *pa;
copiază conţinutul lui a[0] în x.
Dacă pa indică un element particular al unui masiv a, atunci prin definiţie pa+i indică un element cu i poziţii după elementul pe care îl indică pa, după cum pa-i indică un element cu i poziţii înainte de cel pe care indică pa. Astfel, dacă variabila pa indică pe a[0] atunci *(pa+i) se referă la conţinutul lui a.
Aceste observaţii sînt adevărate indiferent de tipul variabilelor din masivul a.
Întreaga aritmetică cu pointeri are în vedere faptul că expresia pa+i înseamnă de fapt înmulţirea lui i cu lungimea elementului pe care îl indică pa şi adunarea apoi la pa, obţinîndu-se astfel adresa elementului de indice i al masivului.
Corespondenţa dintre indexarea într-un masiv şi aritmetica de pointeri este foarte strînsă. De fapt, o referire la un masiv este convertită de compilator într-un pointer la începutul masivului. Efectul este că un nume de masiv este o expresie pointer, deoarece numele unui masiv este identic cu numele elementului de indice zero din masiv.
Atribuirea:
pa = &a[0];
este identică cu:
pa = a;
De asemenea, expresiile a şi *(a+i) sînt identice. Aplicînd operatorul & la ambele părţi obţinem &a identic cu a+i. Pe de altă parte, dacă pa este un pointer, expresiile pot folosi acest pointer ca un indice: pa este identic cu *(pa+i). Pe scurt orice expresie de masiv şi indice poate fi scrisă ca un pointer şi un deplasament şi invers, chiar în aceeaşi instrucţiune.
Există însă o singură diferenţă între un nume de masiv şi un pointer la începutul masivului. Un pointer este o variabilă, deci pa = a şi pa++ sînt instrucţiuni corecte. Dar un nume de masiv este o constantă şi deci construcţii de forma a = pa, a++ sau p = &a sînt ilegale.
Cînd se transmite unei funcţii un nume de masiv, ceea ce se transmite de fapt este adresa primului element al masivului. Aşadar, un nume de masiv, argument al unei funcţii, este în realitate un pointer, adică o variabilă care conţine o adresă. Fie de exemplu funcţia strlen care calculează lungimea şirului s:


strlen(char *s) { /* returnează lungimea şirului */
int n;
for (n=0; *s!='\0'; s++)
n++;
return n;
}


Incrementarea lui s este legală deoarece s este o variabilă pointer. s++ nu afectează şirul de caractere din funcţia care apelează pe strlen, ci numai copia adresei şirului din funcţia strlen.
Este posibil să se transmită unei funcţii, ca argument, numai o parte a unui masiv, printr-un pointer la începutul sub-masivului respectiv. De exemplu, dacă a este un masiv, atunci:


f(&a[2])
f(a+2)


transmit funcţiei f adresa elementului a[2], deoarece &a[2] şi a+2 sînt expresii pointer care, ambele, se referă la al treilea element al masivului a. În cadrul funcţiei f argumentul se poate declara astfel:
f(int arr[]) { }
sau
f(int *arr) { }
Declaraţiile int arr[] şi int *arr sînt echivalente, opţiunea pentru una din aceste forme depinzînd de modul în care vor fi scrise expresiile în interiorul funcţiei.


Aritmetica de adrese

Dacă p este un pointer, atunci p += i incrementează pe p pentru a indica cu i elemente după elementul pe care îl indică în prealabil p. Această construcţie şi altele similare sînt cele mai simple şi comune formule ale aritmeticii de adrese, care constituie o caracteristică puternică a limbajului C. Să ilustrăm cîteva din proprietăţile aritmeticii de adrese scriind un alocator rudimentar de memorie. Fie rutina alloc(n) care returnează un pointer p la o zonă de n caractere consecutive care vor fi folosite de rutina apelantă pentru memorarea unui şir de caractere. Fie rutina free(p) care eliberează o zonă începînd cu adresa indicată de pointerul ppentru a putea fi refolosită mai tîrziu. Zona de memorie folosită de rutinele alloc şi free este o stivă funcţionînd pe principiul ultimul intrat - primul ieşit, iar apelul la free trebuie făcut în ordine inversă cu apelul la alloc. Să considerăm că funcţia alloc va gestiona stiva ca pe elementele unui masiv pe care îl vom numi allocbuf. Vom mai folosi un pointer la următorul element liber din masiv, pe care-l vom numi allocp. Cînd se apelează rutina alloc pentru n caractere, se verifică dacă există suficient spaţiu liber în masivul allocbuf. Dacă da, alloc va returna valoarea curentă a lui allocp, adică adresa de început a blocului cerut, după care va incrementa pe allocp cu n pentru a indica următoarea zonă liberă. free(p) actualizează allocp cu valoarea p, dacă p indică în interiorul lui allocbuf.

#define NULL 0
/* valoarea pointerului pentru semnalizarea erorii */
#define ALLOCSIZE 1000
/* dimensiunea spaţiului disponibil */
static char allocbuf [ALLOCSIZE];
/* memoria pentru alloc */
static char *allocp = allocbuf;
/* următoarea poziţie liberă */


char *alloc(int n) {
/* returnează pointer la n caractere */
if (allocp+n<=allocbuf+ALLOCSIZE) {
allocp += n; /* dimensiunea satisfăcută */
return allocp-n; /* vechea valoare */
}
else
return NULL; /* nu este spaţiu suficient */
}


free(char *p) { /* eliberează memoria indicată de p */
if (p>=allocbuf && p<allocbuf+ALLOCSIZE)
allocp = p;
}


Testul if (allocp+n<=allocbuf+ALLOCSIZE)
verifică dacă există spaţiu suficient pentru satisfacerea cererii de alocare a n caractere. Dacă cererea poate fi satisfăcută, alloc revine cu un pointer la zona de n caractere consecutive. Dacă nu, alloc trebuie să semnaleze lipsa de spaţiu pe care o face returnînd valoarea constantei simbolice NULL. Limbajul C garantează că nici un pointer care indică corect o dată nu va conţine zero, prin urmare o revenire cu valoarea zero poate fi folosită pentru semnalarea unui eveniment anormal (în cazul nostru, lipsa de spaţiu). Atribuirea valorii zero unui pointer este deci un caz special.
Observăm de asemenea că variabilele allocbuf şi allocp sînt declarate static cu scopul ca ele să fie locale numai fişierului sursă care conţine funcţiile alloc şi free.
Exemplul de mai sus demonstrează cîteva din facilităţile aritmeticii de adrese (pointeri). În primul rînd, pointerii pot fi comparaţi în anumite situaţii. Dacă p şi q sînt pointeri la membri unui acelaşi masiv, atunci relaţiile <, <=, >, >=, ==, != sînt valide. Relaţia p<q, de exemplu, este adevărată dacă p indică un element mai apropiat de începutul masivului decît elementul indicat de pointerul q. Comparările între pointeri pot duce însă la rezultate imprevizibile, dacă ei se referă la elemente aparţinînd la masive diferite.
Se observă că pointerii şi întregii pot fi adunaţi sau scăzuţi. Construcţia de forma:
p+n
înseamnă adresa celui de-al n-lea element după cel indicat de p, indiferent de tipul elementului pe care îl indică p. Compilatorul C aliniază valoarea lui n conform dimensiunii elementelor pe care le indică p, dimensiunea fiind determinată din declaraţia lui p (scara de aliniere este 1 pentru char, 2 pentru int etc).
Dacă p şi q indică elemente ale aceluiaşi masiv, p-q este numărul elementelor dintre cele pe care le indică p şi q. Să scriem o altă versiune a funcţiei strlen folosind această ultimă observaţie:


strlen(char *s) { /* returnează lungimea unui şir */
char *p;
p = s;
while (*p != '\0')
p++;
return p-s;
}


În acest exemplu s rămîne constant cu adresa de început a şirului, în timp ce p avansează la următorul caracter de fiecare dată. Diferenţa p-s dintre adresa ultimului element al şirului şi adresa primului element al şirului indică numărul de elemente.
În afară de operaţiile binare menţionate (adunarea sau scăderea pointerilor cu întregi şi scăderea sau compararea a doi pointeri), celelalte operaţii cu pointeri sînt ilegale. Nu este permisă adunarea, înmulţirea, împărţirea sau deplasarea pointerilor, după cum nici adunarea lor cu constante de tip double sau float.
Sînt admise de asemenea incrementările şi decrementările precum şi alte combinaţii ca de exemplu *++p şi *--p.


Pointeri la caracter şi funcţii

O constantă şir, de exemplu:
"Buna dimineata"
este un masiv de caractere, care în reprezentarea internă este terminat cu caracterul '\0', astfel încît programul poate depista sfîrşitul lui. Lungimea acestui şir în memorie este astfel cu 1 mai mare decît numărul de caractere ce apar efectiv între ghilimelele de început şi sfîrşit de şir.
Cea mai frecventă apariţie a unei constante şir este ca argument la funcţii, caz în care accesul la ea se realizează prin intermediul unui pointer.
În exemplul:
printf("Buna dimineata\n");
funcţia printf primeşte de fapt un pointer la masivul de caractere.
În prelucrarea unui şir de caractere sînt implicaţi numai pointeri, limbajul C neoferind nici un operator care să trateze şirul de caractere ca o unitate de informaţie.
Vom prezenta cîteva aspecte legate de pointeri şi masive analizînd două exemple. Să considerăm pentru început funcţia strcpy(s,t) care copiază şirul t peste şirul s. O primă versiune a programului ar fi următoarea:


strcpy(char s[], char t[]) {/* copiază t peste s */
int t;
i = 0;
while ((s=t) != '\0')
i++;
}


O a doua versiune cu ajutorul pointerilor este următoarea:

strcpy(char *s, char *t) {
/* versiune cu pointeri */
while ((*s++=*t++) != '\0') ;
}


Această versiune cu pointeri modifică prin incrementare pe s şi t în partea de test. Valoarea lui *t++ este caracterul indicat de pointerul t, înainte de incrementare. Notaţia postfix ++ asigură că t va fi modificat după depunerea conţinutului indicat de el, la vechea poziţie a lui s, după care şi s se incrementează. Efectul este că se copiază caracterele şirului t în şirul s pînă la caracterul terminal '\0' inclusiv.
Am mai putea face o observaţie legată de redundanţa comparării cu caracterul '\0', redundanţă care rezultă din structura instrucţiunii while.
Şi atunci forma cea mai prescurtată a funcţiei strcpy(s,t) este:


strcpy(char *s, char *t) {
while (*s++ = *t++) ;
}


Să considerăm, ca al doilea exemplu, funcţia strcmp(s,t) care compară caracterele şirurilor s şi t şi returnează o valoare negativă, zero sau pozitivă, după cum şirul s este lexicografic mai mic, egal sau mai mare ca şirul t. Valoarea returnată se obţine prin scăderea caracterelor primei poziţii în care s şi t diferă.
O primă versiune a funcţiei strcmp(s,t) este următoarea:


strcmp(char s, char t) {/* compară şirurile s şi t */
int i;
i = 0;
while (s==t)
if (s[i++]=='\0')
return 0;
return s-t;
}


Versiunea cu pointeri a aceleiaşi funcţii este:

strcmp(char *s, char *t) {
for (; *s==*t; s++,t++)
if (*s=='\0')
return 0;
return *s-*t;
}


În final prezentăm funcţia strsav care copiază un şir dat prin argumentul ei într-o zonă obţinută printr-un apel la funcţia alloc. Ea returnează un pointer la şirul copiat sau NULL, dacă nu mai există suficient spaţiu pentru memorarea şirului.

char *strsav(char *s) { /* copiază şirul s */
char *p;
p = alloc(strlen(s)+1);
if (p!=NULL)
strcpy(p,s);
return p;
}


Masive multidimensionale

Limbajul C oferă facilitatea utilizării masivelor multidimensionale, deşi în practică ele sînt folosite mai puţin decît masivele de pointeri.
Să considerăm problema conversiei datei, de la zi din lună, la zi din an şi invers, ţinînd cont de faptul că anul poate să fie bisect sau nu. Definim două funcţii care să realizeze cele două conversii.
Funcţia day_of_year converteşte ziua şi luna în ziua anului şi funcţia month_day converteşte ziua anului în lună şi zi.
Ambele funcţii au nevoie de aceeaşi informaţie şi anume un tabel cu numărul zilelor din fiecare lună. Deoarece numărul zilelor din lună diferă pentru anii bisecţi de cele pentru anii nebisecţi este mai uşor să considerăm un tabel bidimensional în care prima linie să corespundă numărului de zile ale lunilor pentru anii nebisecţi, iar a doua linie să corespundă numărului de zile pentru anii bisecţi. În felul acesta nu trebuie să ţinem o evidenţă în timpul calculului a ceea ce se întîmplă cu luna februarie. Atunci masivul bidimensional care conţine informaţiile pentru cele două funcţii este următorul:


static int day_tab[2][13] = {
{0,31,28,31,30,31,30,31,31,30,31,30,31},
{0,31,29,31,30,31,30,31,31,30,31,30,31}
};


Masivul day_tab trebuie să fie declarat extern pentru a putea fi folosit de ambele funcţii.
În limbajul C, prin definiţie, un masiv cu două dimensiuni este în realitate un masiv cu o dimensiune ale cărui elemente sînt masive. De aceea indicii se scriu sub forma [j] în loc de [i,j], cum se procedează în cele mai multe limbaje. Un masiv bidimensional poate fi tratat în acelaşi fel ca şi în celelalte limbaje, în sensul că elementele sînt memorate pe linie, adică indicele cel mai din dreapta variază cel mai rapid.
Un masiv se iniţializează cu ajutorul unei liste de iniţializatori închişi între acolade; fiecare linie a unui masiv bidimensional se iniţializează cu ajutorul unei subliste de iniţializatori. În cazul exemplului nostru, masivul day_tab începe cu o coloană zero, pentru ca numerele lunilor să fie între 1 şi 12 şi nu între 0 şi 11, aceasta pentru a nu face modificări în calculul indicilor.
Şi atunci funcţiile care realizează conversiile cerute de exemplul nostru sînt:


day_of_year (int year, int month, int day)
{ /* ziua anului din lună şi zi */
int i, leap;
leap = (year%4==0) && (year%100!=0) ||
(year%400==0);
for (i=1; i<month; i++)
day += day_tab[leap];
return day;
}


Deoarece variabila leap poate lua ca valori numai zero sau unu după cum expresia:

(year%4==0) && (year%100!=0) ||
(year%400==0)


este falsă sau adevărată, ea poate fi folosită ca indice de linie în tabelul day_tab care are doar două linii în exemplul nostru.

month_day(int year, int yearday,
int *pmonth, int *pday) {
int i,leap;
leap = (year%4==0) && (year%100!=0) ||
(year%400==0);
for (i=1; yearday>day_tab[leap]; i++)
yearday -= day_tab[leap];
*pmonth = i;
*pday = yearday;
}


Deoarece această ultimă funcţie returnează două valori, argumentele lună şi zi vor fi pointeri.
Exemplu: month_day(1984,61,&m,&d) va încărca pe m cu 3, iar pe d cu 1 (adică 1 martie).
Dacă un masiv bidimensional trebuie transmis unei funcţii, declaraţia argumentelor funcţiei trebuie să includă dimensiunea coloanei. Dimensiunea liniei nu este necesar să apară în mod obligatoriu, deoarece ceea ce se transmite de fapt este un pointer la masive de cîte 13 întregi, în cazul exemplului nostru. Astfel, dacă masivul day_tab trebuie transmis unei funcţii f, atunci declaraţia lui f poate fi:
f(int (*day_tab)[13])
unde declaraţia (*day_tab)[13]) indică faptul că argumentul lui f este un pointer la un masiv de 13 întregi.
În general deci, un masiv d-dimensional a[j]...[p] de rangul i*j*...*p este un masiv d-1 - dimensional de rangul j*k*...*p ale cărui elemente, fiecare, sînt masive d-2 - dimensionale de rang k*...*p ale cărui elemente, fiecare, sînt masive d-3 - dimensionale ş.a.m.d. Oricare dintre expresiile a, a[j]..., a[j]... [p] pot apărea în expresii. Prima are tipul masiv, ultima are tipul int, de exemplu, dacă masivul este de tipul int. Vom mai reveni asupra acestei probleme cu detalii.

Masive de pointeri şi pointeri la pointeri


Deoarece pointerii sînt variabile, are sens noţiunea de masiv de pointeri. Vom ilustra modul de lucru cu masive de pointeri pe un exemplu.
Să scriem un program care să sorteze lexicografic liniile de lungimi diferite ale unui text, linii care spre deosebire de întregi nu pot fi comparate sau schimbate printr-o singură operaţie.
Dacă memorăm liniile textului una după alta într-un masiv lung de caractere (gestionat de funcţia alloc), atunci fiecare linie poate fi accesibilă cu ajutorul unui pointer la primul ei caracter. Pointerii tuturor liniilor, la rîndul lor, pot fi memoraţi sub forma unui masiv. Atunci două linii de text pot fi comparate transmiţînd pointerii lor funcţiei strcmp. Dacă două linii care nu respectă ordinea trebuie să fie schimbate, se schimbă doar pointerii lor din masivul de pointeri şi nu textul efectiv al liniilor.


Procesul de sortare îl vom realiza în trei paşi:
1) se citesc toate liniile textului de la intrare;
2) se sortează liniile în ordine lexicografică;
3) se tipăresc liniile sortate în noua ordine.


Vom scrie programul prin funcţiile sale, fiecare funcţie realizînd unul din cei trei paşi de mai sus. O rutină principală va controla cele trei funcţii. Ea are următorul cod:

#define LINES 100 /* nr maxim de linii de sortat */
main() { /* sortează liniile de la intrare */
char *lineptr[LINES]; /* pointeri la linii */
int nlines; /* nr linii intrare citite */
if ((nlines=readlines(lineptr,LINES))>=0)
{
sort(lineptr,nlines);
writelines(lineptr,nlines);
}
else printf
("Intrarea prea mare pentru sort\n");
}


Cele 3 funcţii care realizează întregul proces sînt: readlines, sort şi writelines.

Rutina de intrare readlines trebuie să memoreze caracterele fiecărei linii şi să construiască un masiv de pointeri la liniile citite. Trebuie, de asemenea, să numere liniile din textul de la intrare, deoarece această informaţie este necesară în procesul de sortare şi de imprimare. Întrucît funcţia de intrare poate prelucra numai un număr finit de linii de intrare, ea poate returna un număr ilegal, cum ar fi -1, spre a semnala că numărul liniilor de intrare este prea mare pentru capacitatea de care dispune.
Atunci funcţia readlines care citeşte liniile textului de la intrare este următoarea:


#define MAXLEN 1000
#define NULL 0
#define EOF -1
readlines(char *lineptr[], int maxlines) {
/* citeşte liniile */
int len,nlines;
char *p,*alloc(),line[MAXLEN];
nlines = 0;
while ((len=getline(line,MAXLEN))>0)
if (nlines>=maxlines)
return -1;
else if ((p=alloc(len))==NULL)
return -1;
else {
line[len-1] = '\0';
strcpy(p,line);
lineptr[nlines++] = p;
}
return nlines;
}


Instrucţiunea line[len-1] = '\0'; şterge caracterul <LF> de la sfîrşitul fiecărei linii ca să nu afecteze ordinea în care sînt sortate liniile şi depune în locul lui caracterul '\0' ca marcă de sfîrşit de şir.

Rutina care tipăreşte liniile în noua lor ordine este writelines şi are următorul cod:

writelines(char *lineptr[], int nlines) {
/* scrie liniile sortate */
int i;
for (i=0; i<nlines; i++)
printf("%s\n",lineptr);
}


Declaraţia nouă care apare în aceste programe este:
char *lineptr[LINES];
care indică faptul că lineptr este un masiv de LINES elemente, fiecare element al masivului fiind un pointer la un caracter. Astfel lineptr este un pointer la un caracter, iar *lineptr permite accesul la caracterul respectiv.
Deoarece lineptr este el însuşi un masiv, care se transmite ca argument funcţiei writelines, el va fi tratat ca un pointer (vezi secţiunea 9.3) şi atunci funcţia writelines mai poate fi scrisă şi astfel:


writelines(char *lineptr[], int nlines) {
while (--nlines>=0)
printf("%s\n",*lineptr++);
}


În funcţia printf, lineptr indică iniţial prima linie de imprimat; fiecare incrementare avansează pe *lineptr la următoarea linie de imprimat, în timp ce nlines se micşorează după fiecare tipărire a unei linii cu 1.

Funcţia care realizează sortarea efectivă a liniilor se bazează pe algoritmul de înjumătăţire şi are următorul cod:

#define NULL 0
#define LINES 100 /* nr maxim de linii de sortat */
sort(char *v[], int n) {
/* sortează şirurile v0, v1, ... vn-1 în ordine crescătoare */
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) {
if (strcmp(v[j],v[j+gap])<=0)
break;
temp = v[j];
v[j] = v[j+gap];
v[j+gap] = temp;
}
}


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 şi ea un pointer la un caracter, deci operaţiile de atribuire din ciclu după variabila j sînt admise şi ele realizează reinversarea pointerilor la linii dacă ele nu sînt în ordinea cerută.

Să reţinem deci următoarele lucruri legate de masive şi pointeri. De cîte ori apare într-o expresie un identificator de tip masiv el este convertit într-un pointer la primul element al masivului. Prin definiţie, operatorul de indexare [] este interpretat astfel încît E1[E2] este identic cu *((E1)+(E2)). Dacă E1 este un masiv, iar E2 un întreg, atunci E1[E2] se referă la elementul de indice E2 al masivului E1.
O regulă corespunzătoare se aplică şi masivelor multi-dimensionale. Dacă E1 este un masiv d-dimensional, de rangul i*j*...*k, atunci ori de cîte ori e1 apare într-o expresie, e1 va fi convertit într-un pointer la un masiv d-1 - dimensional de rangul j*...*k, ale cărui elemente sînt masive. Dacă operatorul * se aplică acestui pointer, rezultatul este masivul d-1 - dimensional, care se va converti imediat într-un pointer la un masiv d-2 - dimensional ş.a.m.d. Raţionamentul se poate aplica în mod inductiv pînă cînd, în final, ca urmare a aplicării operatorului * se obţine ca rezultat un întreg, de exemplu, dacă masivul a fost declarat de tipul int.


Să considerăm, de exemplu, masivul:
int x[3][5];
x este un masiv de întregi, de rangul 3*5. Cînd x apare într-o expresie, el este convertit într-un pointer la (primul din cele trei) masive de 5 întregi.




În expresia x, care este echivalentă cu expresia *(x+i), x este convertit într-un pointer la un masiv, ale cărui elemente sînt la rîndul lor masive de 5 elemente; apoi i se converteşte la tipul x, adică indicele i se înmulţeşte cu lungimea elementului pe care îl indică x (adică 5 întregi) şi apoi rezultatele se adună. Se aplică operatorul * pentru obţinerea masivului i (de 5 întregi) care la rîndul lui este convertit într-un pointer la primul întreg din cei cinci.
Se observă deci că primul indice din declaraţia unui masiv nu joacă rol în calculul adresei.


Iniţializarea masivelor şi masivelor de
pointeri


Iniţializatorul unei variabile declarate masiv constă dintr-o listă de iniţializatori separaţi prin virgulă şi închişi între acolade, corespunzători tuturor elementelor masivului. Ei sînt scrişi în ordinea crescătoare a indicilor masivului. Dacă masivul conţine sub-masive atunci regula se aplică recursiv membrilor masivului. Dacă în lista de iniţializare există mai puţini iniţializatori decît elementele masivului, restul elementelor neiniţializate se iniţializează cu zero. Nu se admite iniţializarea unui masiv de clasă cu automatic.
Acoladele { şi } se pot omite în următoarele situaţii:
– dacă iniţializatorul începe cu o acoladă stîngă ({), atunci lista de iniţializatori, separaţi prin virgulă, va iniţializa elementele masivului; nu se acceptă să existe mai mulţi iniţializatori decît numărul elementelor masivului;
– dacă însă iniţializatorul nu începe cu acoladă stîngă ({), atunci se iau din lista de iniţializatori atîţia iniţializatori cîţi corespund numărului de elemente ale masivului, restul iniţializatorilor vor iniţializa următorul membru al masivului, care are ca parte (sub-masiv) masivul deja iniţializat.
Un masiv de caractere poate fi iniţializat cu un şir, caz în care caracterele succesive ale şirului iniţializează elementele masivului.


Exemple:

1) int x[] = {1,3,5};
Această declaraţie defineşte şi iniţializează pe x ca un masiv unidimensional cu trei elemente, în ciuda faptului că nu s-a specificat dimensiunea masivului. Prezenţa iniţializatorilor închişi între acolade determină dimensiunea masivului.


2) Declaraţia
int y[4][3]={
{1,3,5},
{2,4,6},
{3,5,7},
};
este o iniţializare complet închisă între acolade. Valorile 1,3,5 iniţializează prima linie a masivului y[0] şi anume pe y[0][0], y[0][1], y[0][2]. În mod analog următoarele două linii iniţializează pe y[1] şi y[2]. Deoarece iniţializatorii sînt mai putini decît numărul elementelor masivului, linia y[3] se va iniţializa cu zero, respectiv elementele y[3][0], y[3][1], y[3][2] vor avea valorile zero.


3) Acelaşi efect se poate obţine din declaraţia:
int y[4][3] = {1,3,5,2,4,6,3,5,7};
unde iniţializatorul masivului y începe cu acolada stîngă în timp ce iniţializatorul pentru masivul y[0] nu, fapt pentru care primii trei iniţializatori sînt folosiţi pentru iniţializarea lui y[0], restul iniţializatorilor fiind folosiţi pentru iniţializarea masivelor y[1] şi respectiv y[2].


4) Declaraţia:
int y[4][3] = {
{1},{2,},{3,},{4}
};
iniţializează masivul y[0] cu (1,0,0), masivul y[1] cu (2,0,0), masivul y[2] cu (3,0,0) şi masivul y[4] cu (4,0,0).


5) Declaraţia:
static char msg[] = "Eroare de sintaxa";
iniţializează elementele masivului de caractere msg cu caracterele succesive ale şirului dat.


În ceea ce priveşte iniţializarea unui masiv de pointeri să considerăm următorul exemplu.
Fie funcţia month_name care returnează un pointer la un şir de caractere care indică numele unei luni a anului. Funcţia dată conţine un masiv de şiruri de caractere şi returnează un pointer la un astfel de şir, cînd ea este apelată.
Codul funcţiei este următorul:


char *month_name(int n) {
/* returnează numele lunii a n-a */
static char *name[] = {
"luna ilegala", "ianuarie",
"februarie", "martie", "aprilie",
"mai", "iunie", "iulie", "august",
"septembrie", "octombrie", "noiembrie",
"decembrie"
}
return ((n<1) || (n>12)) ? name[0] :
name[n] ;
}


În acest exemplu, name este un masiv de pointeri la caracter, al cărui iniţializator este o listă de şiruri de caractere. Compilatorul alocă o zonă de memorie pentru memorarea acestor şiruri şi generează cîte un pointer la fiecare din ele pe care apoi îi introduce în masivul name. Deci name va conţine un pointer la şirul de caractere avînd indice i al iniţializatorului. Dimensiunea masivului name nu este necesar a fi specificată deoarece compilatorul o calculează numărînd iniţializatorii furnizaţi şi o completează în declaraţia masivului.

Masive de pointeri şi masive
multidimensionale


Adesea se creează confuzii în ceea ce priveşte diferenţa dintre un masiv bidimensional şi un masiv de pointeri. Fie date declaraţiile:

int a[10][10];
int *b[10];


În această declaraţie a este un masiv de întregi căruia i se alocă spaţiu pentru toate cele 100 de elemente, iar calculul indicilor se face în mod obişnuit pentru a avea acces la oricare element al masivului.
Pentru masivul b, declaraţia alocă spaţiu numai pentru zece pointeri, fiecare trebuind să fie încărcat cu adresa unui masiv de întregi.
Presupunînd că fiecare pointer indică un masiv de zece elemente înseamnă că ar trebui alocate încă o sută de locaţii de memorie pentru elementele masivelor.
În această accepţiune, folosirea masivelor a şi b poate fi similară în sensul că a[5][5] şi b[5][5], de exemplu, se referă ambele la unul şi acelaşi întreg (dacă fiecare element b este iniţializat cu adresa masivului a).
Astfel, masivul de pointeri utilizează mai mult spaţiu de memorie decît masivele bidimensionale şi pot cere un pas de iniţializare explicit. Dar masivele de pointeri prezintă două avantaje, şi anume: accesul la un element se face cu adresare indirectă, prin intermediul unui pointer, în loc de procedura obişnuită folosind înmulţirea şi apoi adunarea, iar al doilea avantaj constă în aceea că dimensiunea masivelor pointate poate fi variabilă. Acest lucru înseamnă că un element al masivului de pointeri b poate indica un masiv de zece elemente, altul un masiv de două elemente şi altul de exemplu poate să nu indice nici un masiv.
Cu toate că problema prezentată în acest paragraf am descris-o în termenii întregilor, ea este cel mai frecvent utilizată în memorarea şirurilor de caractere de lungimi diferite (ca în funcţia month_name prezentată mai sus).


Argumentele unei linii de comandă

În sistemul de calcul care admite limbajul C trebuie să existe posibilitatea ca în momentul execuţiei unui program scris în acest limbaj să i se transmită acestuia argumente sau parametri prin linia de comandă. Cînd un program este lansat în execuţie şi funcţia main este apelată, apelul va conţine două argumente. Primul argument (numit convenţional argc) reprezintă numărul de argumente din linia de comandă care a lansat programul. Al doilea argument (argv) este un pointer la un masiv de pointeri la şiruri de caractere care conţin argumentele, cîte unul pe şir.
Să ilustrăm acest mod dinamic de comunicare între utilizator şi programul său printr-un exemplu.
Fie programul numit pri care dorim să imprime la terminal argumentele lui luate din linia de comandă, imprimarea făcîndu-se pe o linie, iar argumentele imprimate să fie separate prin spaţii.
Comanda:
pri succes colegi
va avea ca rezultat imprimarea la terminal a textului
succes colegi
Prin convenţie, argv[0] este un pointer la numele pri al programului apelat, astfel că argc, care specifică numărul de argumente din linia de comandă este cel puţin 1.
În exemplul nostru, argc este 3, iar argv[0], argv[1] şi argv[2] sînt pointeri la "pri", "succes" şi respectiv "colegi". Primul argument real este argv[1] iar ultimul este argv[argc-1]. Dacă argc este 1, înseamnă că linia de comandă nu are nici un argument după numele programului.
Atunci programul pri are următorul cod:


main(int argc, char *argv[]) {
/* tipăreşte argumentele */
int i;
for (i=1; i<argc; i++)
printf("%s%%c",argv,(i<argc-1)?
' ':'\n');
}


Deoarece argv este un pointer la un masiv de pointeri, există mai multe posibilităţi de a scrie acest program. Să mai scriem două versiuni ale acestui program.

main(int argc, char *argv[]) {
/* versiunea a doua */
while (--argc>0)
printf("%s%c",*++argv,(argv>1)?
' ':'\n');
}


Deoarece argv este un pointer la un masiv de pointeri, incrementîndu-l, (++argv), el va pointa la argv[1] în loc de argv[0]. Fiecare incrementare succesivă poziţionează pe argv la următorul argument, iar *argv este pointerul la argumentul şirului respectiv. În acelaşi timp argc este decrementat pînă devine zero, moment în care nu mai sînt argumente de imprimat.
Alternativ:



main(int argc, char *argv[ ]) {
/* versiunea a treia */
while (--argc>0)
printf((argc>1)? "%s ":"%s\n",*++argv);
}


Această versiune arată că argumentul funcţiei printf poate fi o expresie ca oricare alta, cu toate că acest mod de utilizare nu este foarte frecvent.
Ca un al doilea exemplu, să reconsiderăm programul din secţiunea 7.5, care imprimă fiecare linie a unui text care conţine un şir specificat de caractere (schemă).
Dorim acum ca această schemă să poată fi modificată dinamic, de la execuţie la execuţie. Pentru aceasta o specificăm printr-un argument în linia de comandă.
Şi atunci programul care caută schema dată de primul argument al liniei de comandă este:


#define MAXLINE 1000
main(int argc, char *argv[ ]) {
/* găseşte schema din primul argument */
char line[MAXLINE];
if (argc!=2)
printf("Linia de comanda eronata\n");
else
while (getline(line,MAXLINE)>0)
if (index(line,argv[1])>=0)
printf("%s",line);
}


unde linia de comandă este de exemplu: "find limbaj" în care "find" este numele programului, iar "limbaj" este schema căutată. Rezultatul va fi imprimarea tuturor liniilor textului de intrare care conţin cuvîntul "limbaj".
Să elaborăm acum modelul de bază, legat de linia de comandă şi argumentele ei.
Să presupunem că dorim să introducem în linia de comandă două argumente opţionale: unul care să tipărească toate liniile cu excepţia acelora care conţin schema, şi al doilea care să preceadă fiecare linie tipărită cu numărul ei de linie.
O convenţie pentru programele scrise în limbajul C este ca argumentele dintr-o linie de comandă care încep cu un semn '-' să introducă un parametru opţional. Dacă alegem, de exemplu, -x pentru a indica „cu excepţia” şi -n pentru a cere „numărarea liniilor”, atunci comanda:
find -x -n la
avînd intrarea:


la miezul stinselor lumini
s-ajung victorios,
la temelii, la rădăcini,
la măduvă, la os.


va produce tipărirea liniei a doua, precedată de numărul ei, deoarece această linie nu conţine schema "la".
Argumentele opţionale sînt permise în orice ordine în linia de comandă. Analizarea şi prelucrarea argumentelor unei linii de comandă trebuie efectuată în funcţia principală main, iniţializînd în mod corespunzător anumite variabile. Celelalte funcţii ale programului nu vor mai ţine evidenţa acestor argumente.
Este mai comod pentru utilizator dacă argumentele opţionale sînt concatenate, ca în comanda:
find -xn la
Caracterele 'x' respectiv 'n' indică doar absenţa sau prezenţa acestor opţiuni (switch) şi nu sînt tratate din punct de vedere al valorii lor.
Fie programul care caută schema "la" în liniile de la intrare şi le tipăreşte pe acelea, care nu conţin schema, precedate de numărul lor de linie. Programul tratează corect, atît prima formă a liniei de comandă cît şi a doua.



#define MAXLINE 1000

main(int argc, char *argv[]) {
/* caută schema */
char line[MAXLINE], *s;
long line0;
int except, number;
line0 = 0;
number = 0;
while (--argc>0 && (*++argv)[0]=='-')
for (s=argv[0]+1; *s!='\0'; s++)
switch(*s) {
case 'x': except = 1; break;
case 'n': number = 1; break;
default:
printf
("find: optiune ilegala %c\n",
*s);
argc = 0;
break;
}
if (argc!=1)
printf
("Nu exista argumente sau schema\n");
else
while (getline(line,MAXLINE)>0) {
line0++;
if ((index(line,*argv)>=0)!=except)
{
if (number)
printf("%d:",line0);
printf("%s",line);
}
}
}


Dacă nu există erori în linia de comandă, atunci la sfîrşitul primului ciclu while argc trebuie să fie 1, iar *argv conţine adresa schemei. *++argv este un pointer la un şir argument, iar (*++argv)[0] este primul caracter al şirului. În această ultimă expresie parantezele sînt necesare deoarece fără ele expresia înseamnă *++(argv[0]) ceea ce este cu totul altceva (şi greşit): al doilea caracter din numele programului. O alternativă corectă pentru (*++argv[0]) este **++argv.
Pointeri la funcţii


În limbajul C o funcţie nu este o variabilă, dar putem defini un pointer la o funcţie, care apoi poate fi prelucrat, transmis unor alte funcţii, introdus într-un masiv şi aşa mai departe. Relativ la o funcţie se pot face doar două operaţii: apelul ei şi considerarea adresei ei. Dacă numele unei funcţii apare într-o expresie, fără a fi urmat imediat de o paranteză stîngă, deci nu pe poziţia unui apel la ea, atunci se generează un pointer la această funcţie. Pentru a transmite o funcţie unei alte funcţii, ca argument, se poate proceda în felul următor:
int f();
g(f);
unde funcţia f este un argument pentru funcţia g. Definiţia funcţiei g va fi:
g(int(*funcpt) ()) {
(*funcpt)();
}
Funcţia f trebuie declarată explicit în rutina apelantă (int f();), deoarece apariţia ei în g(f) nu a fost urmată de paranteză stîngă ’(’. În expresia g(f) f nu apare pe poziţia de apel de funcţie. În acest caz, pentru argumentul funcţiei g se generează un pointer la funcţia f. Deci g apelează funcţia f printr-un pointer la ea.
Declaraţiile din funcţia g trebuie studiate cu grijă.
int (*funcpt)();
spune că funcpt este un pointer la o funcţie care returnează un întreg. Primul set de paranteze este necesar, deoarece fără el
int *funcpt();
înseamnă că funcpt este o funcţie care returnează un pointer la un întreg, ceea ce este cu totul diferit faţă de sensul primei expresii. Folosirea lui funcpt în expresia:
(*funcpt)();
indică faptul că funcpt este un pointer la o funcţie, *funcpt este funcţia, iar (*funcpt)() este apelul funcţiei.
O formă echivalentă simplificată de apel este următoarea:
funcpt();
Ca un exemplu, să considerăm procedura de sortare a liniilor de la intrare, descrisă în secţiunea 9.7, dar modificată în sensul ca dacă argumentul opţional -n apare în linia de comandă, atunci liniile se vor sorta nu lexicografic ci numeric, liniile conţinînd grupe de numere.
O sortare constă adesea din trei părţi: o comparare care determină ordinea oricărei perechi de elemente, un schimb care inversează ordinea elementelor implicate şi un algoritm de sortare care face comparările şi inversările pînă cînd elementele sînt aduse în ordinea cerută. Algoritmul de sortare este independent de operaţiile de comparare şi inversare, astfel încît transmiţînd diferite funcţii de comparare şi inversare funcţiei de sortare, elementele de intrare se pot aranja după diferite criterii.
Compararea lexicografică a două linii se realizează prin funcţiile strcmp şi swap. Mai avem nevoie de o rutină numcmp care să compare două linii pe baza valorilor numerice şi care să returneze aceiaşi indicatori ca şi rutina strcmp.
Declarăm aceste trei funcţii în funcţia principală main, iar pointerii la aceste funcţii îi transmitem ca argumente funcţiei sort, care la rîndul ei va apela aceste funcţii prin intermediul pointerilor respectivi.
Funcţia principală main va avea atunci următorul cod:


#define LINES 100 /* nr maxim de linii de sortat */

main (int argc, char *argv[]) {
char *lineptr[LINES]; /* pointeri la linii text */
int nlines; /* număr de linii citite */
int strcmp(), numcmp(); /* funcţii de comparare */
int swap (); /* funcţia de inversare */
int numeric;
numeric = 0; /* 1 dacă sort numeric */
if (argc>1 && argv[1][0]=='-' &&
argv[1][1]=='n')
numeric = 1;
if ((nlines=readlines(lineptr,LINES))>=0)
{
if (numeric)
sort(lineptr,nlines,numcmp,swap);
else
sort(lineptr,nlines,strcmp,swap);
writelines (lineptr,nlines);
}
else
printf
("Nr de linii de intrare prea mare\n");
}


În apelul funcţiei sort, argumentele strcmp, numcmp şi swap sînt adresele funcţiilor respective. Deoarece ele au fost declarate funcţii care returnează un întreg, operatorul ’&’ nu este necesar să preceadă numele funcţiilor, compilatorul fiind cel care gestionează transmiterea adreselor funcţiilor.
Funcţia sort care aranjează liniile în ordinea crescătoare se va modifica astfel:


sort(char *v[], int n, int (*comp)(),
int (*exch)()) { /* sortează 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) {
if (comp(v[j],v[j+gap])<=0)
break;
exch(v+j,v+j+gap);
}
}


Să studiem declaraţiile din această funcţie.
int(*comp)(), (*exch)();
indică faptul că comp şi exch sînt pointeri la funcţii care returnează un întreg (primul set de paranteze este necesar).
if (comp(v[j],v[j+gap])<=0)
înseamnă apelul funcţiei comp (adică strcmp sau numcmp), deoarece comp este un pointer la funcţie, *comp este funcţia, iar
comp(v[j],v[j+gap])
este apelul funcţiei.
exch(v+j,v+j+gap)
este apelul funcţiei swap, de inversare a două linii, inversare care realizează interschimbarea adreselor liniilor implicate (vezi secţiunea 9.2). Funcţia numcmp este următoarea:


numcmp(char *s1, char *s2) {
/* compară s1 şi 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;
}


Pentru ca programul nostru să fie complet să mai prezentăm şi codul funcţiei swap, care schimbă între ei pointerii a două linii.

swap(char *px[], char *py[]) {
char *temp;
temp = *px;
*px = *py;
*py = temp;
}
 
Top