Prima pagină > Tutoriale C++ > Capitolul 8: Functii, pointeri si clase de memorare

Capitolul 8: Functii, pointeri si clase de memorare


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.

Categorii:Tutoriale C++
  1. Niciun comentariu până acum.
  1. No trackbacks yet.

Lasă un răspuns

Completează mai jos detaliile despre tine sau dă clic pe un icon pentru autentificare:

Logo WordPress.com

Comentezi folosind contul tău WordPress.com. Dezautentificare / Schimbă )

Poză Twitter

Comentezi folosind contul tău Twitter. Dezautentificare / Schimbă )

Fotografie Facebook

Comentezi folosind contul tău Facebook. Dezautentificare / Schimbă )

Fotografie Google+

Comentezi folosind contul tău Google+. Dezautentificare / Schimbă )

Conectare la %s

%d blogeri au apreciat asta: