Functii, pointeri si clase de memorare Va amintiti ca daca o expresie este transmisa ca argument pentru o functie, atunci se creeaza o copie a valorii expresiei care se transmite. Acest mecanism este cunoscut sub numele de apel prin valoare ('call-by-value') si se foloseste in limbajul C. Presupunem ca avem o variabila v si o functie f(). Daca scriem v = f(v);
atunci valoarea returnata de functia f va schimba valoarea lui v, altfel nu. In interiorul functiei f, nu se modifica valoarea lui v. Aceasta se datoreaza faptului ca se transmite doar o copie a lui v catre f. In alte limbaje de programare, un apel de functie poate schimba valoarea lui v din memorie. Acest mecanism se mai numeste apel prin referinta ('call-by-reference'). Noi vom simula apelul prin referinta transmitand adresele variabilelor ca argumente in apelul functiei. Declararea si atribuirea pointerilor Pointerii sunt folositi in programe pentru accesarea memoriei si manipularea adreselor. Deja ne-am intalnit cu adresele variabilelor ca argumente ale functiei 'scanf()'. De exemplu, putem avea: scanf('%dn', &n);
Daca v este o variabila, atunci &v este o adresa (sau locatie) din memorie. Operatorul de adresa & este unar si are aceeasi precedenta si asociativitate de la dreapta la stanga ca si ceilalti operatori unari. Variabilele pointer pot fi declarate in programe si apoi folosite pentru a lua valori adrese din memorie. Exemplu: Declaratia int i, *p;
defineste i de tip 'int' si p 'pointer catre int'. Domeniul legal de valori pentru orice pointer cuprinde adresa speciala 0 si o multime de numere naturale care sunt interpretate ca fiind adrese masina ale sistemului C. De obicei, constanta simbolica NULL este 0 (definita in ). Exemple: 1. p = &i; /* valoarea lui p este adresa lui i */ 2. p = 0; /* valoarea lui p este adresa speciala 0 */ 3. p = NULL; /* echivalent cu p = 0; */ 4. p = (int *) 1307; /* o adresa absoluta din memorie */ Adresare si indirectare Am vazut ca operatorul de adresa & se aplica unei variabile si intoarce valoarea adresei sale din memorie. Operatorul de indirectare (sau de dereferentiere) se aplica unui pointer si returneaza valoarea scrisa in memorie la adresa data de pointer. Intr-un anumit sens, acesti doi operatori sunt inversi unul altuia. Pentru a intelege mai bine aceste notiuni, sa vedem pe un exemplu ce se intampla in memorie: Exemplu: Presupunem ca avem declaratiile: int i = 777, *p = &i;
Atunci, in memorie avem: Nume Tip Valoare Adresa
-------- ----- ------ ------
| i | int | 777 | 3A38:0FFE |
-------- ----- ------ ------
/ |
| |
(*p == i) * | -------- (p = &i)
| | &
| /
-------- ----- ------ ----- ----- -----
| p | int * | 3A38:0FFE | 3A38:0FFA |
-------- ----- ------ ----- ----- -----
Nume Tip Valoare Adresa
Mentionam ca adresa unei variabile este dependenta de sistem (C aloca memorie acolo unde poate). Exemplu: float x, y, *p;
p = &x;
y = *p;
Mai intai 'p' se asigneaza cu adresa lui 'x'. Apoi, 'y' se asigneaza cu valoarea unui obiect la care pointeaza p (adica *p). Aceste doua instructiuni de asignare se pot scrie y = *&x;
care este echivalent cu y = x;
Am vazut mai sus ca un pointer se poate initializa in timpul declararii sale. Trebuie sa avem totusi grija ca variabilele din membrul drept sa fie deja declarate. Exemplu: Unde este greseala ? int *p = &a, a; Tabelul de mai jos ilustreaza modul de evaluare a expresiilor cu pointeri. Exemplu: Presupunem ca avem declaratiile: int i = 3, j = 5, *p = &i, *q = &j, *r;
double x; | Expresie | Expresie echivalenta | Valoare | p == & i p == (& i) 1 p = i + 7 p = (i + 7) gresit * * & p * (* (& p)) 3 r = & x r = (& x) gresit 7 * * p / * q + 7 (((7 * (* p))) / (* q)) + 7 11 * (r = & j) *= * p (* (r = (& j))) *= (* p) 15 Spre deosebire de C traditional, in ANSI C, singura valoare intreaga care poate fi asignata unui pointer este 0 (sau constanta NULL). Pentru asignarea oricarei alte valori, trebuie facuta o conversie explicita (cast). In cele ce urmeaza, vom scrie un program care ilustreaza legatura dintre valoarea unui pointer si adresa lui. Exemplu: #include #include void main() Locatia curenta a unei variabile din memorie este dependenta de sistem. Operatorul * (din expresia *p) va afisa valoarea scrisa la adresa care este egala cu valoarea lui p. Adresa lui i (valoarea lui p) va fi afisata ca fiind ceva de genul 3A38:0FFE
care reprezinta un numar scris in baza 16 (in care cifrele sunt 0, 1, , 9, A, B, C, D, E, F) si are valoarea De observat ca un pointer se memoreaza intotdeauna pe patru octeti indiferent de tipul variabilei catre care se face referirea. Explicati de ce ? Pointeri catre 'void' In C traditional, pointerii de tipuri diferite sunt considerati compatibili ca asignare. In ANSI C, totusi, un pointer poate fi asignat altuia doar daca au acelasi tip, sau cand unul dintre ei este de tipul 'void'. De aceea, putem gandi 'void *' ca un tip pointer generic. Exemple: Presupunem ca avem declaratiile int *p;
float *q;
void *v;
| Asignari legale | Asignari ilegale | | p = 0; p = 1; | | p = (int *) 1; v = 1; | | p = v = q; p = q; | | p = (int *) q; | Vom discuta in capitolele ulterioare despre functiile 'calloc()' si 'malloc()', care produc alocare dinamica a memoriei pentru vectori si structuri. Ele returneaza un pointer catre 'void', de aceea putem scrie: int *a;
a = calloc();
In C traditional, trebuie sa facem conversie explicita: a = (int *) calloc();
Apel prin adresa (referinta) Am vazut ca C foloseste mecanismul apelului prin valoare ('call-by-value') in cazul apelurilor functiilor si anume se fac copii ale parametrilor actuali care se transmit functiilor. In cele ce urmeaza, vom descrie mecanismul apelului prin adresa si astfel se va asigura modificarea valorii variabilei transmise. Pentru aceasta, vom utiliza pointeri. Exemplu: #include void interschimba(int *, int *); void main() void interschimba(int *p, int *q) Efectul apelului prin adresa este realizat prin: 1. Declararea parametrului functiei ca fiind un pointer; 2. Folosirea unui pointer de indirectare in corpul functiei; 3. Transmiterea adresei unui argument cand functia este apelata. Reguli pentru stabilirea domeniului Domeniul unui identificator este partea din textul unui program unde identificatorul este cunoscut sau accesibil. Aceasta idee depinde de notiunea de 'bloc', care este o instructiune compusa cu declaratii. Regula de baza in stabilirea domeniului este aceea ca identificatorii sunt accesibili numai in blocul unde sunt declarati si necunoscuti in afara granitelor blocului. Unii programatori folosesc acelasi nume de identificatori prezenti in anumite blocuri. Exemplu: printf('%dn', ++a); }
Un program echivalent ar fi: printf('%dn', ++a_afara); }
Clase de memorare Orice variabila si functie are doua atribute: tipul si clasa de memorare
Exista patru clase de memorare in C, automata, externa, registru si statica si sunt date de urmatoarele cuvinte rezervate: auto extern register static
Cea mai cunoscuta clasa de memorare este 'auto'. Clasa de memorare 'auto' Variabilele declarate in interiorul functiilor sunt implicit automate. De aceea, clasa 'auto' este cea mai cunoscuta dintre toate. Daca o instructiune compusa (bloc) incepe cu declararea unor variabile, atunci aceste variabile sunt in domeniu in timpul acestei instructiuni compuse (pana la intalnirea semnului }). Exemplu: auto int a, b, c; auto float f; Declaratiile variabilelor in blocuri sunt implicit automate. La executie, cand se intra intr-un bloc, se aloca memorie pentru variabilele automate. Variabilele sunt considerate locale acestui bloc. Cand se iese din acest bloc, sistemul elibereaza zona de memorie ocupata de acestea si deci valorile acestor variabile se pierd. Daca intram din nou in acest bloc, atunci se aloca din nou memorie pentru aceste variabile, dar vechile valori sunt necunoscute. Clasa de memorare 'extern' O metoda de transmitere a informatiei in blocuri si functii este folosirea variabilelor externe. Daca o variabila este declarata inafara functiei, atunci acesteia i se aloca permanent memorie si spunem ca ea apartine clasei de memorare 'extern'. O variabila externa este considerata globala tuturor functiilor declarate dupa ea, si chiar dupa iesirea din blocuri sau functii, ea ramane permanent in memorie. Exemplu: #include int a = 1, b = 2, c = 3; int f(void); void main() int f(void) Explicatia este foarte simpla. La inceput se memoreaza cate 2 octeti pentru 'a', 'b', 'c'. Cand ajungem la functia 'f()', memoram inca cate doi octeti pentru 'b' si 'c' (notate la fel din intamplare). La intoarcerea in functia apelanta, aceste 'b' si 'c' noi nu mai exista pentru ca erau locale functiei 'f()'. Sa vedem mai exact ce se intampla in memorie: Inainte de apelul functiei 'f()': Nume Tip Valoare Adresa
-------- ----- ------ ------
| a | int | 1 | 3A38:0FFE |
-------- ----- ------ -------
-------- ----- ------ -------
| b | int | 2 | 3A38:0FFC |
-------- ----- ------ -------
-------- ----- ------ -------
| c | int | 3 | 3A38:0FFA |
-------- ----- ------ -------
In timpul executiei functiei 'f()' (dupa a = b = c = 4): Nume Tip Valoare Adresa
-------- ----- ------ ------
| a | int | 4 | 3A38:0FFE |
-------- ----- ------ ------
-------- ----- ------ ------
| b | int | 2 | 3A38:0FFC |
-------- ----- ------ ------
-------- ----- ------ ------
| c | int | 3 | 3A38:0FFA |
-------- ----- ------ ------
-------- ----- ------ -----
| b | int | 4 | 3A38:0FF8 |
-------- ----- ------ -----
-------- ----- ------ -----
| c | int | 4 | 3A38:0FF6 |
-------- ----- ------ -----
La intoarcerea in functia 'main()': Nume Tip Valoare Adresa
-------- ----- ------ ------
| a | int | 4 | 3A38:0FFE |
-------- ----- ------ ------
-------- ----- ------ ------
| b | int | 2 | 3A38:0FFC |
-------- ----- ------ ------
-------- ----- ------ ------
| c | int | 3 | 3A38:0FFA |
-------- ----- ------ -----
Deci, cuvantul rezervat 'extern' spune compilatorului 'cauta peste tot, chiar si in alte fisiere !'. Astfel, programul precedent se poate rescrie: in fisierul 'fisier1.c': #include int a = 1, b = 2, c = 3; /* variabile externe */ int f(void); void main() in fisierul 'fisier2.c': int f(void) Deci, putem conchide ca informatiile se pot transmite prin variabile globale (declarate cu extern) sau folosind transmiterea parametrilor. De obicei se prefera al doilea procedeu. Toate functiile au clasa de memorare externa. De exemplu, extern double sin(double);
este un prototip de functie valid pentru functia 'sin()', iar pentru definitia functiei, putem scrie: extern double sin(double x)
Clasa de memorare 'register' Clasa de memorare 'register' spune compilatorului ca variabilele asociate trebuie sa fie memorate in registri de memorie de viteza mare, cu conditia ca aceasta este fizic si semantic posibil. Daca limitarile resurselor si restrictiile semantice (cateodata) fac aceasta imposibila, clasa de memorare register va fi inlocuita cu clasa de memorare implicita 'auto'. De obicei, compilatorul are doar cativa astfel de registri disponibili. Multi sunt folositi de sistem si deci nu pot fi alocati. Folosirea clasei de memorare 'register' este o incercare de a mari viteza de executie a programelor. De regula, variabilele dintr-o bucla sau parametrii functiilor se declara de tip 'register'. Exemplu: } /* la iesirea din bloc, se va elibera registrul i */
Declaratia register i; este echivalenta cu register int i;
Daca lipseste tipul variabilei declarata intr-o clasa de memorare de tip 'register', atunci tipul se considera implicit 'int'. Clasa de memorare 'static' Declaratiile 'static' au doua utilizari distincte si importante: a) permite unei variabile locale sa retina vechea valoare cand se reintra in bloc (sau functie) (caracteristica ce este in contrast cu variabilele 'auto' obisnuite); b) folosita in declaratii externe are alta comportare (vom discuta in sectiunea urmatoare); Pentru a ilustra a), consideram exemplul: Exemplu: void f(void) Prima data cand functia este apelata, 'contor' se initializeaza cu 0. Cand se paraseste functia, valoarea lui 'contor' se pastreaza in memorie. Cand se va apela din nou functia 'f()', 'contor' nu se va mai initializa, ba mai mult, va avea valoarea care s-a pastrat in memorie la precedentul apel. Declararea lui 'contor' ca un 'static int' in functia 'f()' il pastreaza privat in 'f()' (adica numai aici i se poate modifica valoarea). Daca ar fi fost declarat in afara acestei functii, atunci si alte il puteau accesa. Variabile externe statice Ne vom referi acum la folosirea lui 'static' ca declaratie externa. Aceasta pune la dispozitie un mecanism de 'izolare' foarte important pentru modularitatea programelor. Prin 'izolare' intelegem vizibilitatea sau restrictiile de domeniu. Deosebirea dintre variabile externe si cele externe static este ca acestea din urma sunt variabile externe cu restrictii de domeniu. Domeniul este fisierul sursa in care ele sunt declarate. Astfel, acestea sunt inaccesibile pentru functiile definite anterior in fisier sau definite in alte fisiere, chiar daca functiile folosesc clasa de memorare 'extern'. Exemplu: void f(void)
static int v; /* variabila externa statica */
void g(void)
Vom mai prezenta un exemplu de generare a numerelor aleatoare bazata pe metode de congruente liniara (Knuth, D., E.: 'The Art of Computer Programming', 2nd ed., vol. 2, 'Seminumerical Algorithms', Reading Mass. Addison-Wesley, 1981). Exemplu: #define INITIAL_SEED 17 /* SEED - samanta */ #define MULTIPLIER 25273 #define INCREMENT 13849 #define MODULUS 65536 #define FLOATING_MODULUS 65536.0 static unsigned seed = INITIAL_SEED; /* externa, dar locala acestui fisier */ unsigned random(void) double probability(void) Functia 'random()' produce o secventa aleatoare (aparenta) de numere intregi situate intre 0 si MODULUS. Functia 'probability()' produce o secventa aleatoare (aparenta) de valori reale intre 0 si 1. Observam ca un apel al functiei 'random()' sau 'probability()' produce o noua valoare a variabilei 'seed' care depinde de cea veche. Din moment ce 'seed' este o variabila externa statica, aceasta este locala acestui fisier si valoarea sa se pastreaza de la un apel la altul. Putem acum crea functii in alte fisiere care apeleaza aceste numere aleatoare fara sa avem grija efectelor laterale. Prezentam, in continuare, un ultim exemplu de utilizare a lui 'static' ca specificator de clasa de memorare pentru functii. Functiile declarate 'static' sunt vizibile doar in fisierul unde au fost declarate. Exemplu: void f(int a)
static int g(void)
Initializari implicite In C, variabilele externe si statice care nu sunt explicit initializate de catre programator, sunt initializate de catre sistem cu 0. Aceasta include siruri, siruri de caractere, pointeri, structuri si inregistrari (union). Pentru siruri (de caractere), aceasta inseamna ca fiecare element se initializeaza cu 0, iar pentru structuri si 'union' fiecare membru se initializeaza tot cu 0. In contrast cu aceasta, variabilele 'registru' si 'auto' nu se initializeaza de catre sistem, ci pornesc cu valori 'garbage' (adica cu ce se gaseste la momentul executiei la acea adresa). Exemplu: Procesarea caracterelor O functie care utilizeaza 'return' poate returna o singura valoare. Daca dorim sa trasmitem mai multe valori pentru mediul apelant, atunci trebuie sa transmitem adresele unor variabile. Vrem sa procesam un sir de caractere (in stilul 'top-down') astfel: - citeste caractere de la intrare pana cand avem EOF; - schimba litere mici in litere mari; - scrie pe fiecare linie trei cuvinte separate de un singur spatiu; - numara caracterele si literele de la intrare. #include #include #define NR_CUVINTE 3 int procesare(int *, int *, int *); void main() int procesare(int *p, int *n_c_p, int *n_l_p) else if (isspace(*p)) if (++contor % NR_CUVINTE == 0)
*p = 'n';
else
*p = ' ';
++*n_c_p; ultim_caracter = *p; return 1; Definitii si declaratii de functii Pentru compilator, declaratiile functiilor sunt date in multe moduri: - apelul functiei - definitia functiei
- prototipuri si declaratii explicite
Daca un apel de functie cum ar fi f(x) apare inainte de a fi declarata atunci compilatorul presupune declaratia implicita int f();
In stilul C traditional, declararea functiilor se face astfel: int f(x)
double x;
Este responsabilitatea programatorului de a transmite o variabila de tip 'double'. In stilul ANSI C, aceasta s-ar scrie: int f(double x)
In acest caz, compilatorul stie tipul argumentelor din functia 'f()'. De exemplu, daca un 'int' este transmis ca parametru, atunci el va fi convertit automat la 'double'. Exista cateva limitari pentru definitiile si prototipurile functiilor. Clasa de memorare a functiei, daca este prezenta, poate fi 'extern' sau 'static', dar nu ambele; 'auto' si 'register' nu se pot folosi. Singura clasa care se poate folosi in lista de tipuri a parametrilor este 'register'. Parametrii nu se pot initializa. Calificatorii de tip 'const' si 'volatile' Comitetul ANSI a adaugat cuvintele rezervate 'const' si 'volatile' pentru limbajul C (acestea nu sunt disponibile in limbajul C traditional). De obicei, 'const' este plasat intre clasa de memorare si tipul variabilei. Exemplu: static const int k = 3; Citim aceasta 'k este o constanta de tip int cu clasa de memorare static'. Deoarece 'k' are tipul 'const', atunci putem initializa 'k', dar nu mai poate fi reasignat (incrementat sau decrementat). Chiar daca variabila este calificata ca fiind 'const', aceasta nu se poate folosi pentru precizarea lungimii unui sir. Exemplu: const int n = 3;
int v[n]; /* gresit */
Deci o variabila calificata 'const' nu este echivalenta cu o constanta simbolica. Un pointer necalificat nu poate fi asignat cu adresa unei variabile calificata 'const'. Exemplu: const int a = 7; int *p = &a; /* gresit */
Motivul este ca 'p' este un pointer obisnuit catre 'int' si l-am putea folosi mai tarziu in expresii de genul '++*p'. Totusi, utilizand pointeri, putem schimba valoarea lui a (ceea ce contravine conceptului de constanta). Exemplu: const int a = 7;
const int *p = &a;
Nu vom putea modifica valoarea lui 'a', utilizand '*p'. Pointerul 'p' nu este constant (putem face p++). Presupunem ca vrem ca 'p' sa fie constant, si nu 'a'. Consideram declaratiile: int a;
int * const p = &a;
Ultima declaratie spune ca 'p este un pointer constant catre int, si valoarea sa initiala este adresa lui a'. Apoi, nu mai putem asigna o valoare lui p, dar putem da valori lui '*p'. Consideram acum un exemplu si mai interesant: Exemplu: const int a = 7;
const int * const p = &a;
Ultima declaratie spune ca p este un pointer constant catre o constanta intreaga. Nici 'p', nici '*p', nu mai pot fi reasignate. In contrast cu 'const', calificatorul 'volatile' este rar folosit. Un obiect 'volatile' este unul ce poate fi modificat intr-un mod nespecificat de catre hard. Exemplu: Consideram declaratia extern const volatile int real_time_clock;
Clasa de memorare 'extern' inseamna 'cauta-l oriunde, in acest fisier sau in alte fisiere'. Calificatorul 'volatile' presupune ca obiectul poate fi modificat de hard. Din moment ce apare si calificatorul 'const', inseamna ca obiectul nu poate fi modificat din program. Exercitii propuse spre implementare 1. Daca 'i' si 'j' sunt de tip 'int', iar 'p' si 'q' sunt pointeri catre 'int', precizati care dintre urmatoarele asignari sunt corecte: p = &i; p = &*&i; i = (int) p; q = &p; *q = &j; i = (*&)j; i = *&*&j; i = (*p)++ + *q; 2. Scrieti o functie C care sa faca o permutare circulara a cinci variabile. (1,2,3,4,5) -> (2,3,4,5,1). 3. Fie codul C int v = 7, *p = &v, **q = &p;
printf('%pn%dn%pn%pn%dn%pn%pn%pn%dn',
&v, *&v, &p, *&p, **&p, &q, *&q, **&q, ***&q); Explicati de ce anumite numere se repeta ! Observati ca am folosit combinatia '*&', si nu '&*'. Explicati daca exista situatii unde '&*' este corect semantic. 4. Scrieti un program C care arata pe cati octeti sunt memorati pointerii catre tipurile fundamentale de date. Ce observati !