Capitolul 8
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.
8.1 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("%d\n", &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 */
8.2 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:
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.
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()
{
int i = 777, *p = &i;
clrscr();
printf("Valoarea lui i: %d\n", *p);
printf("Adresa lui i: %lu sau %p\n", &i, &i);
printf("Adresa lui i: %lu sau %p\n", p, p);
printf("Valoarea lui p: %lu sau %p\n", p, p);
printf("Adresa lui p: %lu sau %p\n", &p, &p);
getch();
}
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:
3*16^7+10*16^6+3*16^5+8*16^4+ 15*16^2+15*16+14
= 976752638
De observat ca un pointer se memoreaza intotdeauna pe patru octeti indiferent
de tipul variabilei catre care se face referirea.
8.3 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.
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(...);
8.4 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()
{
int a = 3, b = 7;
printf("%d %d\n", a, b);
interschimba(&a, &b);
printf("%d %d\n", a, b);
}
void interschimba(int *p, int *q)
{
int tmp;
tmp = *p;
*p = *q;
*q = tmp;
}
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.
8.5 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:
{
int a = 2;
printf("%d\n", a);
{
int a = 7;
printf("%d\n", a);
}
printf("%d\n", ++a);
}
Un program echivalent ar fi:
{
int a_afara = 2;
printf("%d\n", a_afara);
{
int a_inauntru = 7;
printf("%d\n", a_inauntru);
}
printf("%d\n", ++a_afara);
}
8.6 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".
8.7 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.
8.8 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()
{
printf("%3d\n", f());
printf("%3d%3d%3d\n", a, b, c);
}
int f(void)
{
int b, c; /* b si c sunt locale, deci b, c globale sunt
mascate */
a = b = c = 4; /* valoarea lui a se modifica */
return(a + b +c);
}
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()
{
printf("%3d\n", f());
printf("%3d%3d%3d\n", a, b, c);
}
in fisierul "fisier2.c":
int f(void)
{
extern int a; /* cauta-l peste tot */
int b, c;
a = b = c = 4; /* valoarea lui a se modifica */
return(a + b +c);
}
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)
{
..
}
8.9 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:
{
register int i;
for (i = 0; i < LIMIT; ++i)
{
. . . . .
}
} /* 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".
8.10 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)
{
static int contor = 0;
++contor;
if (contor % 2 == 0)
. . . . .
else
. . . . .
}
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.
8.11 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)
{
. . . . . /* v nu este accesibil aici */
}
static int v; /* variabila externa statica */
void g(void)
{
. . . . . /* v poate fi folosit aici */
}
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)
{
seed = (MULTIPLIER * seed + INCREMENT) % MODULUS;
return seed;
}
double probability(void)
{
seed = (MULTIPLIER * seed + INCREMENT) % MODULUS;
return (seed / FLOATING_MODULUS);
}
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)
{
. . . . . /* g() este disponibil aici, dar nu si in alte fisiere */
}
static int g(void)
{
. . . . .
}
8.12 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 c, numar_caractere = 0, numar_litere = 0;
while ((c = getchar()) != EOF)
if (procesare(&c, &numar_caractere, &numar_litere) == 1)
putchar(c);
printf("\n%s%5d\n%s%5d\n\n",
"Numar de caractere:", numar_caractere,
"Numar de litere: ", numar_litere);
}
int procesare(int *p, int *n_c_p, int *n_l_p)
{
static int contor = 0, ultim_caracter = ' ';
if (isspace(ultim_caracter) && isspace(*p))
return 0;
if (isalpha(*p))
{
++*n_l_p;
if (islower(*p))
*p = toupper(*p);
}
else
if (isspace(*p))
if (++contor % NR_CUVINTE == 0)
*p = '\n';
else
*p = ' ';
++*n_c_p;
ultim_caracter = *p;
return 1;
}
8.13 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.
8.14 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.
![]() |
![]() |
![]() |