|
Unpointer este o variabila care contine adresa unei alte variabile. Pointerii sint foarte mult utilizati in C parte pentru ca uneori sint singura cale de rezolvareauneianumite probleme, parte pentru ca folosirea lor duce la alcatuirea unui cod mai compact si mai eficient decit altul obtinut in alt mod.
Pointeriiau fost 'ingramaditi' la citeva instructiuni goto ca un minunat mod de a crea programe 'imposibil' depriceput. Acest lucru devine pe deplin adevarat atuncicindpointerii sintfolositi neatent, fiind usor de creat pointeri care sa pointeze in locuri cu totul neasteptate. Cu metoda, pointerii se utilizeaza pt un plus de simplitate. Aceasta este aspectul pe care vom incerca sa-l ilustram.
Din moment ce un pointer sustine adresa unui obiect, este posibila adresarea acelui obiect 'indirect' prin intermediul pointerului. Sa presupunem ca x este o variabila, sa spunem int si ca px este un pointer creat intr-un mod neprecizat. Operatorul & da adresa unui obiect, astfel incit instructiunea
px=&x
asigneaza variabilei px adresa lui x acum, px inseamna 'pointeaza pe x'. Operatorul& poate fi aplicat numai variabilelor si elementelor unui tablou, constructii ca &(x+1) si &3 sint interzise. Este deasemenea interzisa pastrarea adresei unei variabile registru.
Operatorul unar * testeaza operandul sau ca adresa ultimului semnal si acceseaza aceasta adresa pentru a aducecontinutul locatiei de la adresa respectiva. Astfel, daca y este tot un int
y = *px
asigneazalui y, ori de cite ori este cazul continutul locatiei unde pointeaza px. Astfel secventa
px = &x;
y = *px;
asigneaza lui y aceasi valoare ca si
y = x
Totodata este necesara declararea variabilelor care apar in secventa:
int x, y;
int *px;
Declararea lui x si y este deja cunoscuta. Noua este declararea pointerului px
int *px
este interpretata ca o mnemonica; aceasta inseamna ca *px este un int, adica in momentul in care px apare in context sub forma *px, este echivalenta cu a intilni o variabila de tip int. De fapt, sintaxa declararii unei varaibile imita sintaxa expresiilor in care ar putea sa apara respectiva variabila. Acest rationament este util in toate cazurile care implica declaratii complicate. De exemplu:
double atof(), *dp;
spune ca intr-o expresie atof() si *dp au valoare de tip double.
De notat declaratia implicita,ceea ce vrea sa insemne ca un pointer este constrins sa pointeze o anumita categorie de obiecte. (Functie de tipul obiectului pointat).
Pointerii pot aparea in expresii. De exemplu, daca px pointeaza pe intregul x atunci *px poate aprarea in orice context in care ar putea apare x.
y = *px + 1
da lui y o valoare egala cu x plus 1.
printf('%dn', *px)
imprima o valoare curenta a lui x si
d = sqrt((double) *px)
face ca d = radical din x,care este fortat de tipul double inainte de a fi transmis lui sqrt (vezi capitolul 2).
In expresii ca
y = *px + 1
operatoriiunari*si & au prioritate mai mare decit cei aritmetici, astfel aceasta expresie ori de cite ori pointerul px avanseaza, aduna 1 si asigneaza valoarea lui y. Vom reveni pe seama asupra a ceea ce inseamna
y = *(px + 1)
Referiri prin pointer pot apare si in partea stinga a asignarilor. Daca px pointeaza pe x atunci
*px = 0
il pune pe x pe zero si
*px += 1
il incrementeaza pe x, ca si
(*px)++
In acest ultim exemplu parantezele sint necesare; fara ele, expresia va incrementa pe px in loc sa incrementeze ceea ce pointeaza px deoarece operatorii unari * si + sint evaluati de la dreapta la stinga.
In sfirsit, daca pointerii sint variabile, ei pot fi manipulati ca orice alta variabila. Daca py este un alt pointer pe int, atunci
py = px
copiaza continutul lui px in py facind astfel ca py sa varieze odata cu px.
Datorita faptului ca in C este posibila transmiterea de argumente unei functii prin 'apel prin valoare' nu exista modalitate directa pentru functia apelata de a altera o variabila in functia apelanta. Ce este de facut atunci cind de fapt, se intentioneaza schimbarea unui argument obisnuit ? De exemplu, o rutina de sortare trebuie sa inverseze doua elemente neordonate cu o functie swap. Nu este suficient sa se scrie
swap(a, b);
unde functia swap este definita ca
swap(x, y)/* GRESIT */
int x, y;
Din cauza apelului prin valoare, swap nu poate afecta argumentele a si b in rutina care o apeleaza.
Din fericire, exista o modalitate de a obtine efectul dorit. Programul apelant trasmite pointeri pe valorile care trebuie schimbate.
swap(&a, &b);
Din moment ce operatorul & da adresa unei variabile, &a este un pointer pe a. In swap insasi, argumentele sint declarate ca fiind pointeri iar adevaratii operanzi sint accesati prin ei (prin pointeri).
swap(px, py) /* interscimba *px si *py */
int *px, *py;
O utilizare comuna a argumentelor de tip poiter se intilneste in cadrul functiilor care trebuie sa returneze mai mult decit o singura valoare. (Veti putea obiecta ca swap returneaza doua valori, noile valori ale argumentelor sale.) Ca un exemplu sa luam o functie getint care realizeaza inversia la intrareprin transsformarea unui sir de caractere in valori intregi, un intreg la fiecare apel,getint trebuie sa returneze valoarea gasita sau semnul de sfirsit de fisier atunci cind s-a terminat sirul de caractere de la intrare. Aceste valori trebuie sa fie returnate ca obiecte separate, pentru indiferent ce valoare este utilizata pentru EOF aceasta putind fi deasemenea valoarea unui intreg-input
O solutie, care este bazata pe functia input scanf, functie care o vom descrie in cap7,este de a folosi getint care sa returneze ca valoare o functie EOF, atunci cind seintilneste sfirsitul de fisier; orice alta valoare returnata inseamna ca a fost prelucrat un intreg obisnuit Valoarea numerica a intregului gasit este returnata printr-un argument care trebuie sa fie pointer pe un intreg.Aceasta organizare separa stareade sfirsit de fisier de valorile numerice.
Urmatoarea bucla completeaza un tablou cu intergi prin apeluri la get int.
int n, v, array[SIZE]
for (n = 0; n < SIZE && getint(&v) != EOF; n++)
array[n] = v;
Fiecare apel pune pe y pe urmatorul intreg gasit la intrare. De notat faptul ca este esential a scrie &y in loc de y, ca arg al lui getint. A scrie doar y constituie eroare de adresare, getint sustinind ca are de a face cu un pointer propriu zis.
Insasi getint este o modificare evidenta a lui atoi tratata mai inainte.
getint(pn)/* ia numarul interg -input */
int *pn;
for (*pn = 0; c >= '0' && c <= '9'; c = getch())
*pn = 10 * *pn + c - '0';
*pn *= sign;
if (c != EOF)
ungetch(c);
return(c);
}
Peste tot in getint, *pn este utilizat ca o variabila int ordinara. Deasemenea, am utilizat getch si ungetch ( descrise in cap 4) in asa fel incit caracterul special ( semnalul EOF) care trebuie citit sa poata fi restocata la intrare.
Exercitiul 1 Scrieti getfloat pentru virgula floatnta analoa ga lui getint. Ce tip de valoare returneaza functia getfloat.
InC,exista o relatie strinsa intre pointeri si tablouri, atit de strinsa incit pointerii si tablourile pot fi tratate simultan. Orice operatie care poate fi rezolvata prin indicierea tablourilor poate fi rezolvata si cu ajutorul pointerilor. Versiunea cu pointeri va fi in general, mai rapida dar, pentru incepatori, mai greu de inteles imediat.
Declaratia
int a[10]
defineste un tablou de dimensiunea 10,care este un bloc de 10 obiecte consecutive numite a[0], a[1], , a[9] notatia a[i] desemneaza elementul deci pozitiile, ale tabloului, numarate de la inceputul acestuia. Daca pa este un pointer pe un interg, decalarat ca
int *pa
atunci asignarea
pa = &a[0]
face ca pa sa pointeze pe al 'zero-ulea' element al tabloului a; aceasta inseamna ca pa contine adresa lui a[0]. Acum asignarea
x = *pa
va copia continutul lui a[0] in x.
Dacapa pointeaza pe un element oarecare al lui a atunci prin definitie pa+1 pointeaza pe elemmentul urmator si in general pa-i pointeaza cu i elemente inaintea elementuluipointatde paiar pa+i pointeaza cu i elemente dupa elementul pointat de pa. Astfel, daca pa pointeaza pe a[0]
*(pa + 1)
refera continutul lui a[1],pa + i este adresa lui a[i] si *(pa+i) este continutul lui a[i].
Acesteremarci sint adevarate indiferent de tipul varaiabilelor din tabelul a. Definitia 'adunarii unitatii la un pointer ' si prin extensie, toata aritmetica pointerilor estedefapt calcularea prin lungimea in memorie a obiectului pointat. Astfel, in pa+i i este inmultit cu lungimea obiectelor pe care pointeaza pa inainte de a fi adunate la pa.
Corespondenta intre indexare si aritmetica pointerilor este evident foarte strinsa.De fapt,referinta la un tablou este convertita de catre compilator intr-un pointer pe inceputul tabloului. Efectul este ca numele unui tablou este o expresie pointer. Aceasta are citeva implicatii utile. Din moment ce numele unui tablou este sinonim cu locatia elementului sau zero, asignarea
pa = &a[0]
poate fi scrisa si
pa = a
Incasimaisurprinzator la prima vedere este faptul ca o referinta la a[i] poate fi scrisa si ca *(a+i). Evaluind pe a[i], C il converteste in *(a+i); cele doua forme sint echivalente. Aplicind operatorul & ambilor termeni ai acestei echivalente, rezulta ca &a[i] este identic cu a+i: a+i adresa elementuluial i-lea in tabloul a. Reciproc: daca pa este un pointer el poate fi utilizat in expresii cu un indice pa[i] este identic cu *(pa+i). Pe scurt orice tablou si exprimare de indice pot fi scrise ca un pointer si offset si orice adresa chiar in aceeasi instructiune
Trebuietinut seama de o difernta ce exista intre numele tablou si un pointer. Un pointer este o variabila, astfel ca pa=a si pa++ sint operatii. Dar, un nume de tablou este o constanta, nu o variabila: constructii ca a=pa sau a++ sau p=&a sint interzise.Atuncicind se transmite un nume de tablou unei functii, ceea ce se transmite este locatia de inceput a tabloului. In cadrul functiei apelate acest fapt argument este ovariabila ca oricare alta astfel incit un argument nume de tablou este un veritabil pointer, adica o variabila continind o adresa. Ne vom putea folosi de aceasta pentru a scrie o noua versiune a lui strlen, care calculeaza lungimea unui sir.
strlen(s) /* returneaza lungimea sirului s */
char *s
Incrementarealuis este perfect legala deoarece el este o variabila pointer; s++nu are efect pe sirul de caractere in funca care a apelat-o pe strlen, dar incrementeaza doar copia adresei. Ca parametri formali in definirea unei functii
char s[]
si
char *s;
sint echivalenti; alegerea celui care trebuie scris este determinata in mare parte de expresiile ce vor fi scrise in cadrul functiei. Atunci cind un nume de tablou estetransmisunei functii,aceasta poate, dupa necesitatis-o interpreteze ca tablou sau ca pointer si sa-l manipuleze in consecinta. Functia poateefectuachiar ambele tipuri de operatii daca i se pare potrivit si corect.
Este posibila si transmiterea catre o functie doar a unei parti dintr-un tablou prin transmiterea unui pointer pe inceputul subtabloului. De exemplu, daca a este un tablou;
f(&a[2])
si
f(a + 2)
ambele transmit functiei f adresa elementului a[2] deoarece &a[2] si a+2 sint expresii pointer care refera al treilea element al lui a. In cadrul lui f, declarea argumentului poate citi
f(arr)
int arr[];
sau
f(arr)
int *arr;
Astfel, dupa cum a fost conceputa functia f faptul ca argumentul refera de fapt o parte a unui tablou mai mare nu are consecinte.
Daca p este un pointer, atunci p++ incrementeaza pe p in asa fel incit t acesta sa pointeze pe elementul urmator indiferent de tipul obiectelor pointate, iar p+=i incrementeaza pe p pentru
a pointa peste i elemente din locul unde p pointeaza curent.
Ceste consistent si constant cu aritmetica pointerilor; pointerii, tablourile si aritmetica adresarii constitue punctul forte al limbajului. Sa ilustram citeva dintreproprietatile lui scriind un program pentru alocare de memorie rudimentar(dar este util in ciuda simplitatii sale exista doua rutine:alloc(n)returneaza un pointer p pe n pozitii caracter consecutive care poate fi utilizat de catre apelantul lui alloc pentru alocarea de caractere; free(p) elibereaza memoria facind-o astfel refolosibila mai tirziu. Rutinele sint 'rudimentare'deoarece apelurilela free trebuie facute in ordine inversa apelurilor laalloc.Aceasta inseamna ca memoria gestionata de alloc si
free este o stiva sau o lista prelucrabila in regim LIFP. Biblioteca standard C este prevazuta in functii analoage carenu auatit de multe restrictii iar in capitolul 8 vom da, pentru demonstratie si alte versiuni. Intre timp se vor ivi multe aplicatii care au realmente nevoie de canalul alloc pentru a dispensa mici portiuni de memorie, de lungimi neprevazute la momente neprevazute.
Ceamai simpla implementare este de a scrie alloc pentru declararea de parti ale unui tablou mare pe care il vom numi allocbuf. Acest tablou este propriu lui alloc si free. Lucrind cu pointeri, nu cu indici in tablou nu este necesar ca vreo alta rutinasa cunoasca numele tabloului, care poate fi declarat static,adicalocal fisierului sursa care sustine pe alloc si free numele tabloului fiind invizibil in afara acestui fisier. In implementarile practice tabloul poate chiar sa nu aiba nici un nume el putind fi obtinut prin cererea catre sistemul de operare a unui pointer pe un bloc de memorie fara nume.
O alta informatie necesara este legata de cit anume din allocbuf a fost folosit. Vom utiliza un pointer pe urmatorul element liber, numit allocp. Cind este apelat alloc pentru n caractere, el verifica daca exista suficient loc eliberatin allocbuf.Daca astfel alloc returneaza valoarea curenta a lui allocp (adica inceputul blocului liber) atunci aceasta valoare esteincrementata cun in asa fel incit allocp sa pointeze pe inceputul urmatoarei zone libere. Free(p) pune pur si simplu pe allocp pe p daca p este in interiorul lui allocbuf.
#define NULL 0/* val pointerului in caz de eroare */
#define ALLOCSIZE 1000 /* lung spatiului disponibil */
static char allocbuf[ALLOCSIZE]; /* memorie pentru alloc*/
static char *allocp = allocbuf; /*memorarea parti libere*/
char *alloc(n) /* pointer de return pe n caractere */
int n;
else /* nu-i destul loc */
return(NULL)
free(p) /* zona de memorie libera pointata de p */
char *p;
Citeva explicatii.In general un pointer poate fi initializat ca orice alta variabila,desi in mod normal singurele valori semnificative sint NULL sau o expresie careopereazaadrese ale unor date in prealabil definite, de tip specificat. Declaratia
static char *allocp = allocbuf;
defineste pe allocp ca fiind un pointer pe caractere si il initializeaza pentru a-lpointa pe allocbuf care este urmatoarea pozitie libera atunci cind incepe programul. Aceasta stare ar putea fi scrisa si astfel
static char *allocp = &allocbuf[0];
deoararece numele tabloului este adresa elementului zero.
Testul
if (allocp + n <= allocbuf + ALLOCSIZE)
verifica daca este suficient loc pentru a satisface cererea pt n caractere. Daca ezista loc, noua valoare a lui allocp va fi cel mult mai dincolo de sfirsitul lui allocbuf. Daca cererea poate fi satisfacuta, alloc retur neaza un pointer normal (observati declaratia functiei). Daca nu, alloc trebuie sa returneze un semnal care sa semnifice ca nu exista spatiu liber. Limbajul C garanteazaca nici un pointer care pointeaza o data valida nu va contine zero, asa ca valoarea zero returnata poate fi utilizata ca semnal de eveniment anormal, nu exista spatiu liber. Se scrie NULL in loc de zero pt a indica mai clar ca aceasta este o valoare speciala pt un pointer. In general intregii nu pot fi asignati pointerilor; zero este u caz special.
Teste ca
if (allocp + n <= allocbuf + ALLOCSIZE)
si
if (p >= allocbuf && p < allocbuf + ALLOCSIZE)
relevaciteva fatete importante ale aritmeticii pointerilor. Mai intii ca in unele situatii pointerii pot fi separati. Daca p si q pointeaza pe elemente ale aceluiasi tablou, relatii ca <, >, =, etc lucreaza exact.
p < q
este adevarata, de ex, in cazul in care p pointeaza pe un element anterior elementului pe care pointeaza q. Relatiile c= si != sint si ele permise. Orice pointer poate fi testat cu NULL. Dar nu exista nici o sansa in a compara pointeri in tablouri diferite. In cazul fericit se va obtine un evident nonsens, indiferent de masina pe care se lucreaza. Mai poate sa apara situatia nefericita in care codul va merge pe vreo masina esuind 'misterios' pe altele
In al doilea rind, tocmai s-a observat ca un pointer si un interg pot fi adunati sau scazuti. Instructiunea
p + n
desemneaza al n-lea obiect dupa cel pointat curent de p. Acest lucru este adevarat indiferent de tipul obiectelor pe care p a fost declarat ca pointer. Compilatorul atunci cind il intilneste pe n, il delaleaza in functie de lungimea obiectelor pe care pointeaza p, lungime determinata prin declaratia lui p. De exemplu, pe PDP11 factorii de scalare sint 11 pentru char, 2 pentru int si short, 4 pentru long si float si 8 pentru double.
Estevalidasiscaderea pointerilor:daca psi q pointeaza pe elementele aceluiasi tablou, p-q este numarul de elementedintre p si q. Acest fapt poate fi utilizat pentru a scrie o noua versiune a lui strlen.
strlen(s) /* returneaza lungimea sirului */
char *s;
Prin declarare, p este initializat pe s, adica sa pointeze pe primul caracter din s. In cadrul buclei while este examinat pe care caracter pina se intilneste /0 care semnifica sfirsitul iar daca while testeaza numai daca expresia este zero este posibila omiterea testului expilcit iar astfel de bucle sint scrise adesea
while (*p)
p++;
Deoarece p pointeaza pe caractere, p++ face ca p sa avanseze de fiecare data pe caracterul urmator, iar p-v da numarul de caractere parcurse, adica lungimea sirului. Aritmetica pointerilor este consistenta: daca am fi lucrat cu float care ocupa mai multa memorie decit char, si daca p ar fi un pointer pe float, p++ ar avansa pe urmatorul float. Astfel, vom putea scrie o alta versiune a lui alloc care pastreaza sa zicem, float in loc de char, pur si simplu prin schimbarea lui char in float. in cadrul lui alloc si free. Toate manipularile de pointeri iau automat in considerare lungimea obiectului pointat in asa fel incit trebuie sa nu fie alterat.
Alteoperatii in afara celor mentionate deja (adunarea sau scaderea unui pointer cu un intreg, scaderea sau comapararea a doi pointeri). Toate celelalte operatii arrrtmeticecupointeri sint ilegale.Nu este permisa adunarea, impartirea, deplasarea logica, sau adunarea unui float sau double la pointer.
Un sir constant scris astfel
'I am a string '
este un tablou de caractere. In reprezentare interna, compilatorul termina un tablou cu caracterul 0 in asa fel incit programele sa poata detecta sfirsitul. Lungimea in memorie esteastfel mai mare cu 1 decit numarul de caractere cuprinse intre ghilimele.
Poate cea mai comuna aparitie a unui sir de constante este ca argument al functiei cum ar fi in
char *message;
atunci instructiunea
message = 'now is the time';
asigneazaalui mesage un pointer in functie de caracterele reale. Aceasta nu este o copie a sirului; nu sint implicati decit pointerii. C nu este inzestrat cu alti operatori care sa trateze eze un sir de caractere ca o unitate.
Vom ilustra mai multe aspecte in legatura cu pointerii si cu tablourile stiind ca doua functii cu adevarat utile, din biblioteca standard de I/O, subiect care va fi discutat in capitolul 7.
Primafunctie este strcpy(s, t) care copiaza sirul t in sirul s. Argumentele sint scrise in aceasta ordine prin analogie cu aranjarea, unde cineva ar putea spune
s = t
pentru a asigna pe t lui s. Versiunea ca tablouri este mai intii.
strcpy(s, t) /*copiaza t in s */
char s[], t[];
Iata o versiune a lui strcpy cu pointeri
strcpy(s, t) /* copiaza t in s, versiunea pointeri 1*/
char *s, *t;
}
Deoareceargumentele sint transmise prin valoare, strcpy poate utiliza s si t in orice fel se doreste. Aici ei sint conventional utilizati ca pointeri, care parcurg tablourile pina in momentul in care s-a copiat 0 sfirsitul lui t, in s.
In practica, strrcpy nu va fi scris asa cum s-a aratat mai sus. O a doua posilitate ar fi
strcpy(s, t) /* copiaza t in s, versiunea 2*/
char *s, *t;
In aceasta ultima versiune se imita incrementarea lui s si t in partea de test. Valoarea lui *t++ este caracterul pe care a pointat inainte ca t sa fi fost incrementat;prefixul ++ nu-l schimba pe t inainte ca acest caracter sa fi fost adus. In acelasi fel, caracterul este stocat in vede a pozitie s inainte ca s sa fie incrementat.Acest caracter este deasemenea valoarea care se grupeaza cu 0 pentru simboul buclei. Efectul net este ca, caracterele sint copiate din t in s, inclusiv sfirsitul lui 0.
Ca o ultima abreviere vom observa ca si gruparea cu 0 este redundanta, astfel ca functia este adesea scrisa ca
strcpy(s, t)/* copiaza t in s; versiunea pointeri 3 */
char *s, *t;
Desiaceastaversiune poate parea compilcata la prima vedere, aranjamentul ca notatie este considerat suveran daca nu exista alte ratiuni de a schimba astfel ca il veti intilni frecvent in programele C.
A doua rutina este strcmp(s, t) care compara sirurile de caractere s si t si returneaza negativ, zero sau pozitiv in functie de relatia dintre s si t; care poate fi: s<t, s=t sau s>t. Valoarea returnata este obtinuta prin scaderea caracterului de pe prima pozitie unde s difera de t.
strcmp(s, t)
/* returneaza <0 daca s<t, 0 daca s==t, >0 daca s>t */
char s[], t[];
Versiunea pointeri a lui strcmp.
strcmp(s, t) /* returneaza <0 daca s<t, 0 daca s==t,
>0 daca s>t */
char *s, *t;
Daca ++ si -- sint folositi altfel decit operatori prefix sau postfix pot apare alte combinatii de * si ++ si --, desi mai putin frecvente. De exemplu:
*++p
incrementeaza pe p inainte de a aduce caracterul pe care pointeaza p.
*--p
decrementeaza pe p in acelasi conditii.
Exercitiul 2.Scrieti o versiune pointeri pentru o functiestrcat expusa in capitolul 2: strcat(s, t) copiaza sirul t la sfirsitul sirului s.
Exercitiul 3. Scrieti un macro pentru strcpy.
Exercitiul 4.Rescrieti variantele programelor din capito lele anterioare si exercitii cu pointeri in loc de tablouri indexate. Bune posibilitati ofera: getline, atoi, itoa si variantele lor, reverse, index, getop.
S-aputut observa ca programele C mai vechi au o atitudine mai toleranta fata de copierea pointerilor. In general a fost adevarat ca pe majoritatea masinilor un pointer poate fi asignat unui intreg si invers, fara a-l schimba; nu are loc nici o scalare sauconversiesi nu se pierd biti. In mod regretabil aceasta stare de lucruri a condus la asumarea unor libertati nepermise desi partea programatorului in lucru cu rutina ce returneaza pointeri ce sint transmisi apoi pur si simplu altor rutine necesitatea declararii pointerului fiind adesea omisa. De exemplu, sa luam o functie strsave care copiaza sirul s undeva, intr-o zona obtinuta printr-un apel la alloc, returnind apoi un pointer pe ea. Strsave se poate scrie astfel
char *strsave(s) /* salveaza undeva sirul s */
char *s;
In practica, exista o tendinta puternica de a omite declararile:
strsave(s) /* salveaza undeva sirul s */
Acest cod s-ar putea sa mearga pe multe masini deoarece tipul implicit al functiilor si al argumentelor este int iar atit int-ul cit si pointerul pot fi asignati la inceput cit si la sfirsit. Cu toate acestea, acest gen de cod este inerent riscant deoarece el depinde de detalii de implementare si de arhitectura masinii, care nu pot fi rezolvate pentru compilatorul particular utilizatde dvs. Este recomandabil sa se efectueze toate declararile necesare. (Programul lint va avertiza in legatura cu astfel de restrictii in cazul in care se vor strecura inadvertente).
C este prevazut cu probabilitatea de a lucra cu tablouri multidimensionale, cu toate ca in practica exista tendinta ca ele sa fie mult mai putin utilizate decit tabloutile de pointeri. In aceasta sectiune vom da citeva dintre proprietatile lor.
Sa reluam problema de conversia datei, zi-in-luna in zi-in-an si viceversa. De exemplu, 1 martie este a 60-a zi dintr-un an nebisect si a 61-a dintr-un an bisect. Sa definim doua functii care sa faca conversia 'day_of_year' converteste luna si ziua in ziua din an si luna, iar 'month_day' converteste ziua din an in luna si ziua. Daca aceasta din urma functie returneaza doua valori, argumentele 'luna si ziua' vor fi pointeri:
month day (1977, 60, &m, &d)
puna pe m pe 3 si pe d pe 1.
Aceste functii au nevoie de aceasi informatie, o tabela cu numarul zilelor din fiecare luna ('30 zile are septembrie'). Din moment ce numarul de zil/luna difera in functie de an bisect sau an nebisect, este mai usor sa separam aceste informatiipe doua linii ale unui tablou bidimensional; apoi sa incercam sa tinem contdecese intimpla cu februarie in timpul calcului. Tabloul si functia pentru rezolvarea transformarilor sint dupa cum urmeaza:
static int day_tab[2][13] = ,
};
day-of-year(year, month, day) /* pune nr zilei in an */
int year, month, day;/* din luna &an */
month_day(year, yearday, pmonth, pday) /*pune luna, zi */
int year, yearday, *pmounth, *pday; /* din ziua in an */
Tabloul day-tab trebuie sa fie extern ambelorfunctii 'day_of_year' si 'month_day', in asa fel incit ambele sa-l poata utiliza.
'day-tab' este primul tablou bidimensional cu care avem de a face in C, Prin definitie un tablou bidimensional este de fapt un tablou unidimensional alei carei elemente sint fiecare in parte cite un tablou. Prin uramre, indicii se scriu astfel
day_tab[i][j]
in loc de
day_tab[i, j]
ca in majoritatea limbajelor in plus un tablou bidimensional paote fi tratat in mai multe moduri decit in alte limbaje. Elementele sint memorate pe linii, ceea ce inseamna ca indicele din deapta
varaiaza primul in asa fel incit elementele sint accesate in ordinea memoriei.
Un tablou se initializeaza printr-o lista de initializatori scrisi intre acolade; fiecare liniea unui tablou bidimensional este initializata printr-o sublista corspondenta. Am inceput tabloul day-tab cu o coloana de zero, in asa fel incit numerotarea liniilor poate fi facuta de la 1 la 12 in loc de 1 0-11. Dacaexista spatiu suficient, este mai usor sa se procedeze innmodul mai sus aratat in loc sa se ajusteze indicii.
In cazul in care un tablou bidimensional trebuie transmis unei functii, declararea argumentelor in funtie trebuie sa includa dimensiunea coloanei, dimensiunea liniei este irelevanta deoarece unei functii i se transmite ca si in cazurile anterioare, uh pointer. In cazul de fata este vorba de un pointer care parcurge obiectecare sint tablouri de cite 13 int. Astfel daca trebuie transmis tabelul day-tab unei functii f, declararea lui f va fi
f(day_tab)
int day_tab[2][13];
Declararea argumentului in f va fi deasemenea
int day-tab[][13];
din moment ce nr liniilor este irelevant, sau ar putea fi
int (*day-tab)[13];
care spune ca argumentul este un pointer pe un tablou de 13 intregi. Sint necesare parantezele pentru ca crosetele au prioritate mai mare decit *, fara paranteze; declararea
int *day-tab[13];
este un tablou de 13 pointeri pe intregi, dupa cum se va vedea in sectiunea urmatoare.
Datorita faptului ca pointerii sint ei insisi variabile, este de asteptat ca ei sa fie utilizati in tablouri de pointeri. Deci, se pune problema de a ilustra prin scrierea unui program care sorteaza un set de linii de text in ordine alfabetica, o versiune a sortului utilitar UNIX.
Incapitolul 3 am prezentat o functie sort shell care sorta un tablou de intregi. Vom utiliza acelasi algoritm cu exceptia faptului ca acum vom avea de-a face cu linii de text de lungimi diferite si care, spre deosebire de intregi, nu pot fi comparate sau deplasate printr-o singura operatie. Avem nevoie de o reprezentare a datelor care sa poata face eficient si potrivit regulilor in gestionarea linilor de text de lungime diferita.
Acum este momentul potrivit pt a introduce tabloul de pointeri. Daca liniile de sortat sint memorate cap la cap intr-un lung sir de caractere (rezervat prin alloc,sazicem) atunci fiecare linie poate fi accesata printr-un pointer pe primul sau caracter. Pointerii insisi pot fi memorati intr-un tablou. Doua liniipot fi comparate prin transmiterea pointerilor respectivi lui strcmp. Cind doua linii neordonate trebuiesc inversate se inverseaza pointerii lor in tabelul de pointeri, nu liniile insele. Acest mod de lucru elimina cuplul de probleme legate de gestionarea memoriei si, ceea ce este mai presus de orice, poate deplasa liniile reale.
Procesul de sortare consta din trei parti:
citirea tuturor liniilor la intrare
sortarea liniilor
tiparirea liniilor in ordine
Ca de obicei cel mai bine este sa impartim programul in functii care rezolva aceasta defalcare,cu o rutina principala care controleaza totul. Sa aminam pentru un moment pasul de sortare si sa ne concentram asupra structurii de date si a I/O. Rutina de inceput trebuie sa colecteze si sa salveze caracterele din fiecare liniesi sa construiasca un tablou de pointeri pe linii. Va trebui, deasemeneasa se numere liniile la intrare, deoarece aceasta informstie este necesara pt sortare si tiparire. Deoarece functia de intrare poate opera doar cu un numar finit de linii input, ea va returna o valoare, cum ar fi -1, in cazul in care se vor prezenta mai multe linii.RUtina de output trebuie doar sa tipareasca liniile in ordinea in care apar i tabloul de pointeri.
#define NULL 0
#define LINES 100 /* maximum de linii de sortat */
main()/* sortarea liniilor de intrare */
else
printf('input prea mare pt sort n');
}
#define MAXLEN 1000
readlines(lineptr, maxlines) /* citeste linii input */
char *lineptr[];
int maxlines;
return(nlines);
}
'newline'de la sfirsitul fiecarei linii este sters astfel incit nu va fi afectata ordinea in care sint sortate liniile.
writelines(lineptr, nlines) /* scrie linii la iesire */
char *lineptr[];
int nlines;
Principala noutate este declarata pentru 'lineptr':
char *lineptr[LINES];
spune ca lineptr este un tablou de elemente LINES, fiecare element fiind un pointer pechar. Adica, lineptr[i] este un pointer pe caractere iar *lineptr[i] acceseaza un caracter.
Daca lineptr este el insusi un tablou care este transmis lui writelines, el poate fi tratat ca un pointer in exact aceeasi maniera ca in exemplul nostru anterior, iar functia poate fi scrisa.
writelines(lineptr, nlines) /* scrie linii la iesire */
char *lineptr[];
int nlines;
*lineptr pointeaza initial pe prima linie, cu fiecare incrementare el avanseaza pe linia urmatoare pina cind nlines se epuizeaza.
Intrareasiiesirea fiind controlate, se poate duce la sortare. Sortul -shell din cap 3 va suferi modificari: declaratiile trebuie modificate iar operatia de grupare trebuie montata intr-o functie separata. Algoritmul de baza ramine acelasi ceea ce ne da o oarecare speranta ca totul va merge bine inca
short(v, n)/* sorteaza sirurile v[0]. . . v[n-1] */
char *v[];/* in ordine crescatoare */
int n;
}
Daca orice element individual din v(alias lineptr) este un pointerpe caractere, temp va fi si el astfel de pointer, asa incit cei doi pot fi copiati unul in altul.
Am scris un program care, in fc de cunostintele din acel moment a fost rapid pe cit posibil.Acest program poatefi facut mai rapid, de exemplu sa copieze liniileinputdirect intr-untablou mentinut prin readlines in loc sa le copieze in linept ca apoi sa le plaseze undeva prin alloc. Dar, pentru a facilita intelegerea programului sa intocmim mai intii o schema logica, si abia dupa aceea sa ne preocupam de eficienta sa. Modalitatea de a face acest program mai eficeintnuvizeaza neaparatevitarea unei copii aliniilor input. Inlocuirea sortuluishellprin ceva mai bun cum ar fi sortul Quicksort, este probabil mai mult decit a marca o simpla diferenta.
In capitolul 1 am semnalat acest lucru deoarece buclele while si for testeaza conditia finala inaintea executarii chiar si pt prima data a corpului buclei; ele ajuta la a ne asigura ca programele vor merge chiar si la limita, in particular fara input. Este edificator a umbla prin functiile programelor de sortare pt a verifica ce se intimpla daca nu exista deloc text de intrare.
Exercitiul Rescrieti readlines pt a crea linii intr-un tablou umplut cu main, in loc de a apela pe alloc pt rezervarea de memorie, Cu cit este mai rapid acest program ?
Sa ne pune problema scrierii unei functii month_name(n) care returneaza un pointer pe un sir de caractere continind numele a n linii. Aceasta este o aplicatie ideala pentru un tablou static intern. month-name contine un tablou propriu de siruri de caractere si returneaza un pointer pe sirul convenabil atunci cind este apelat. Scopul acestei sectiuni este de a arata cum se initializeaza tabloul de nume.
Sintaxa este similara cu cea a initializarilor precedente:
char *month-name(n) /*returneaza numele celei de-a n-a luni*/
int n;
;
return((n < 1 || n > 12) ? name[0] : name[n]);
}
Declararea numelui, care este un tablou de pointeri pe caractere este aceeasi ca si la lineptr, in ex de sortare. Valorile de initializare sint de fapt o lista de caractere; fiecare dintre acestea din urma este asignat pozitiei corespunzatoare din tablou. Mai precis,caracterele celui de-al i-lea sint plasateundeva iarpointerulpe ele este stocat in name[i]. Daca lungimea tabloului name nu este specificata, compilatorul numara valorile de intializare si pune lungimea corecta.
Nou venitii in C sint uneori confuzi in legatura cu deosebirea dintre un tablou bidimensional si un tablou de pointeri cum ar fi name din exemplul de mai sus. Fiind date declaratiile
int a[10][10];
int *b[10];
utilizarile lui a si b pot fi similare, in sensul ca a[5][5] si b[5][5] sint ambele referinte legale ale aceluiasi int.Dar a este un tablou in toata regula:toate cele 100 celule de memorie trebuie alocate iar pentru gasirea fiecarui element se face calculul obisnuit al indicelui. Pentru b, oricum prin declararea se aloca 10 pointeri; fiecare trebuie facut sa pointeze un tablou de intregi. Presupunind ca fiecare pointeaza cite 10 elemente din tablou, atunci vom obtine 100 celule de memorie rezervate, plus cele 10 celule pt pointeri. Astfel tabloul de pointeri utilizeaza sensibil mai mult spatiu si poate cere un pro explicit de initializare. Dar, exista doua avantaje: accesarea unui element se face indirect prin intermediul unui pointer, in loc sa se faca prin inmultire si adunare iar liniile tabloului pot fi de lungimi diferite. Aceasta insemna ca nu orice element al lui b este constrins sa pointeze pe un vector de 10 elemente,unii pot pointa pe cite 2 elemente, altii pe cite 20 si altii pe niciunul.
Desi am mai discutat acest lucru la intregi, de departe, cea mai frecventa utilizare a tabloului de pointeri este cea ilustrata prin mounth-name: sa stocheze lanturi de caractere de lungimi diferite.
Exercitiul 6. Rescrieti rutinele day_of_year si mounth-day cu pointeri in loc de indexare.
Printre facilitatile oferite de C exista modalitatea de a transmite argumentele liniei de comanda sau parametrii unui program atunci cind el incepe sa se execute. Pt inceperea executiei este apelat main prin doua argumente. Primul (numit convetional arge) este numarul argumentelor liniei de comanda prin care a fost apelat programul; al doilea (argv) este un pointer pe un tablou de lanturi de caractere care contine argumentele,unul pentru fiecare lant. Manipularea acestor lanturi de caractere este o utilizare comuna a nivelelor multiple de pointeri.
Cea mai simpla ilustrare a declaratiilor necesare si a celor de mai sus amintite este programul echo,care pune pur si simplu pe o singura linie argumentele liniei de comanda, separate prin blancuri. Astfel, daca este data comanda
echo hello, world
iesirea este
hello, world
Princonventie,argv[0] este numele prin care se recunoaste programul, asa ca argc este 1. In exemplul de mai sus, argc este 3 si argv[0], argv[1] si argv[2] sint respectiv echo, hello si world. Aceasta este ilustrata in echo:
main(argc, argv) /* arg. echo; prima versiune */
int argc;
char *argv[];
Dacaargveste un pointer peinceputultablouluicare contine siruri de argumente, a-l incrementa cu i(++argv) face ca el sa pointeze pe argv[1] in loc de argv[0]. Fiecare incrementare succesiva muta pe argv pe urmatorul argument; argvestedeci pointerulpeacel argument. Simultan argc este decrementat; atunci cind el devine zero, nu mai exista argumente de imprimat.
main(argc, ragv) /* arg echo; a treia versiune */
int argc;
char *argv[];
Aceastaversiune arata ca formatul argumentului lui printf poate fi o expresie ca oricare alta. Aceasta utilizare nu este foarte frecventa dar este bine sa fie retinuta.
Ca un al doilea exemplu, sa facem unele modificari in configuratia programului de cautare din cap4. In cazul unui apel repetat, configuratia ce serveste de model va fi prelucrata ca atare, de fiecare data de catre program, ceea ce ar duce la un aranjament evident nesatisfacator. Urmind exemplul utilitarului grep-UNIX, sa schimbam programul in asa fel incit configuratia model sa fie specificata prin primul argument al liniei de comanda.
#define MAXLINE 1000
main(argc, argv)
/* cautarea model specificat prin primul argument*/
int argc;
char *argv[];
Acum poate fi elaborat modelul de baza in asa fel incit sa ilustreze viitoarele constructii realizate cu ajutorul pointerilor. Sa presupunem ca dorim ca doua argumente sa fie optionale. Unul dintre ele spune 'tipareste toate liniile cu exceptia celor care contin modelul '; al doilea cere 'fiecare linie tiparita sa fie precedata de numarul curent'.
O conventie uzuala pt programele C este legata de argumentul care incepe cu un semn minus si care introduce un flag sau un paramentru optional. Daca se alege -x(pt 'exceptie') pt semnalarea inversarii,si('nr') pt a cere numararea liniilor, atunci comanda
find -x -n the
cu intrarea
now is the time
for all good men
to come to the aid
of their party
va produce iesirea
2: for all good men
Argumentele optionale sint admise in orice ordine iar restul programului va fi insensibil la numarul argumentelor care au fost, de fapt, prezente. In particular, apelul la index nu va referi pe argv[2] atunci cind a fost un singur flag si nici la argv[1] daca n-a existat nici un argument flag. In plus, este convenabil pt utilizatori daca argumentele optionale pot fi adunate, ca in
find -nx the
Iata programul
#define MAXLINE 1000
main(argc, argv) /*gasirea configuratiei din primul arg*/
int argc;
char *arv[];
, *s;
long lineno = 0;
int except = 0, number = 0;
while (--argc > 0 && (*++argv)[0] == '-')
for (s = argv[0] + 1; *s != '0'; s++)
switch (*s)
if (argc != 1)
printf('usage: find -x -n patternn');
else
while (getline(lim, MAXLINE) > 0)
}
argv este incrementat inaintea fiecarui argument optional si argc este decrementat. Daca nu exista erori, la sfirsitul buclei argc va fi 1 iar argv va pointa pe configuratia data. De notat ca *++argv este un pointer pe un lant de caractere; (*++argv)[0] este primul caracter. Parantezele sint necesare deoarece fara de expresia ar fi *++(argv[0]), ceea ce este cu totul altceva (si eronat). O forma corecta ar fi.
**++argv
Exercitiul7.Scrieti programul add care evalueaza o expresie poloneza inversata din linia de comanda. De exemplu,
add 2 3 4 + * calculeaza 2 x (3 + 4)
Exercitiul 8. Modificati programele entab si detab (scrise caexercitii in cap 1) in asa fel incit sa accepte ca argumente o lista de tab-stop-uri utilizati tab-urile normale daca nu existaargumente.
Exercitiul 9. Extindeti entab si dentab in asa fel incit sa accepte prescurtarea.
entab m +n
care inseamna tab-stop dupa fiecare n coloane, incepind de la coloana m. Scrieti functia oarecare implicita convenabila pentru utilizator.
Exercitiul 10. Scrieti programul tail care tipareste rutinele n linii-input. Presupunem, implicit n=10, dar el poate fi schimbat un argument optional, astfel
tail -n
imprima ultimele n linii. In mod normal, programul va functiona indiferent de intrare (rationala sau nu), sau de valoare lui n. Scrieti progarmul in asa fel incit sa utilizeze in mod optimmemoria: liniile vor fi pastrate ca in short, nu intr-un tablou bidimensional de lungime fixata.
In C functia in sine nu este o variabila, dar exista posibilitatea de a defini un pointer pe o functie, care poate fi manipulat, transmis functiilor, plasat in tablouri etc. Vom ilustra aceasta modificind procedura de sortare scrisa mai anterior in acest cap, in asa fel incit fiind dat argumentul optional-n sa se sorteze liniile input memorie, nu linii copiate.
Deobicei, un sort consta in trei parti-o comparatie care realizeaza ordonarea oricarei perechi de obiecte, o schimbare, prin care se inverseaza oridinea obiectelor si un algoritm de sortare care face comparatii si schimbari pina cindobiectele sint definitiv ordonate. Algoritmul de sortare este independent de operatiile de comparare si schimbare, astfel incit prin transmiterea functiei de comparare si schimbare catre el, se va putea realiza sortarea pe diferite criterii. Acest lucru ni-l propunem
in noul sort.
Compararea lexicografica a doua linii este realizata prin strcmp iar schimbarea prin swap;avem nevoie deo rutina numecmp care compara doua linii pe baza valorilor numericesi returneazaun indice de conditie de acelasi fel ca si strcmp. Aceste trei functii sint declarate in main iar pointerii pe ele sint transmisi la sort. Sort la rindul sau apeleaza functiile prin sort, la rindul sau apeleaza functiile prin pointeri. Am sarit peste procesul de tratare a arguentelor eronate, concentrindu-neastfelpe rezolvarea problemelor principale.
#define LINES 1000 /* maximum de linii de sortat */
main(argc, argv) /* sortarea linii input */
int argc;
char *argv[];
else
printf('intrare prea mare pentru sortn');
}
strcmp, numecmp si swap sint adrese de functii; din moment ce ele sint cunoscute ca fiind functii, operatorul & nu este necesar la fel cum el nu este necesar inaintea numelui unui tablou. Compilatorul este cel care rezolva transmiterea adresei functie.
Al doilea pas este modificarea lui sort:
sort(v, n, comp, exch)
/* sorteaza sirurile v[0]. . . v[n-1] */
char *s[];
int n;
int (*comp)(), (*exch)();
}
Declaratiile vor fi studiate cu grija.
int (*comp)()
spune ca comp este un pointer pe o fc ce returneaza un int. Primul set de paranteze este necesar; fara ele
int *comp()
ar spune ca cmp este o functie ce returneaza un pointer pe un integer,ceea ce este cu totul altceva. Utilizarea lui comp in linia
if ((*comp)(v[j], v[j+gap]) <= 0)
este comparabila cu declaratia potrivit careia cmp este u pointer pe o functie; *comp este functie, iar
(*comp)(v[j], v[j+gap])
este apelul ei. Parantezele sint necesare pentru asocierea corecta a componentelor.
Am ilustrat deja prin strcmp compararea a doua siruri. Iata numecmp care compara doua siruri numerice pe baza valorii numerice:
numcmp(s1, s2) /* compara numeric s1 cu s2 */
char *s1, *s2;
Pasulfinalesteadaugarea functiei swap care schimba doi pointeri. Aceasta este adoptata direct din ceea ce am prezentat mai devreme in acest capitol.
swap(px, py) /* interschimba *px si *py **/
char *px[], *py[];
Exista o varietate de alte optiuni care pot fi adaugate la programul de sortare; unele dintre ele pot fi reincercate ca exerctii.
Exercitiul 11. Modificati sort in asa fel incit sa gestioneze un flag, -r care indica sortarea in ordine inversa (descrescatoare). Bineinteles -r trebuie sa fie compatibil cu -n.
Exercitiul 12.Adaugati optiunea -n pt a prelucra impreuna literele mari si literele mici adica sa nu se mai faca distinctia intre aceste doua tipuri de caracteregraficeintimpul sortarii; datele cu litere mari si cee cu litere micisint sortate impreuna, in asa fel incit a si A apar adiacent, nuseparate prin intregul alfabet al literelor mici sau mari.
Exercitiul 13. Adaugati optiunea -d ('ordinea din dictionar')care realizeaza comparari doar pentrulitere,numeresi flancuri. Asiguarti-va ca -d merge impreuna cu -f.
Exercitiul 14. Adaugati o faclitatea legata de gestionarea cimpurilor, in asa fel incit sortarea sa poata fi facuta pe cimpuri si interiorul liniilor, fiecare corespunzind unui set independent de optiuni. (Indexul acestei carti fost sortat cu -df pt ordinea alfabetica si cu -n pentru paginilor.)