Sunteți pe pagina 1din 9

STRUCTURI DE DATE SI ALGORITMI - LABORATOR 1

---- Pointeri, Alocare dinamica a memoriei, Recursivitate ---

I. Pointeri:

La nivel fundamental un program de calculator nu este altceva decat o serie de instructiuni care
se executa in vederea indeplinirii unei sarcini anume. Intr-o proportie covarsitoare aceste instructiuni
depind de, sau acceseaza, diferite zone din memoria de lucru (si nu numai) alocata de sistemul de
operare programului. Spre exemplu, adunarea necesita doi operanzi si o zona unde se va stoca
rezultatul, atribuirea "copiaza" datele dintr-o locatie de memorie sursa in cea destinatie, etc.

Memoria de lucru asociata unui program lansat in executie este parte din memoria virtuala /
RAM si poate fi imaginata la modul cel mai simplifcat ca fiind o succesiune de celule de dimensiunea
minima pe care un anumit procesor le poate adresa (in general byte-ul - 8 biti), fiecare astfel de celula
avand asociata o adresa unica.

Pentru un dezvoltator de programe, un prim mod prin care este permisa manipularea memoriei
este dat de existenta variabilelor. Acestea din urma sunt elemente cheie ale limbajelor de programare si
reprezinta zone de memorie de o dimensiune specificata de programator (int, float, double, char, long
int, etc.) ce au asociate o eticheta de identificare si un anumit mod de a fi interpretate / codificate:

int a;

Variabila 'a' din exemplul de mai sus nu este altceva decat o zona de memorie de 16 biti / 32 biti
(in functie de compilator) rezervata de sistemul de operare si care poate fi mereu accesata prin eticheta
sa - 'a'. Bitii stocati in aceasta zona de memorie reprezinta forma binara a unui numar intreg cu semn.
Din punct de vedere al numarului de celule rezervate (numarul de octeti), 'a' ocupa 2 sau 4, in functie de
compilator. O observatie foarte importanta ce trebuie mentionata de la bun inceput este ca s-a
abstractizat (se pierde din detaliu) modul in care se face adresarea efectiva a zonei asociate cu o
variabila. Cand intr-o instructiune este necesara utilizarea acestei zone de memorie, se face referire la
acest spatiu prin utilizarea etichetei sale, nefiind necesara cunoasterea adresei la care se afla aceasta
zona de memorie. Spre exemplu:

1) .....
a = 5;
.....

2) .....
if (a == 3 )
.....
Adresarea efectiva se va face in mod transparent dezvoltatorului, de catre compilator, si se va plasa in
codul binar rezultat in urma compilarii.

Totusi se pot imagina situatii cand este utila cunoasterea zonei de memorie la care sunt stocate
anumite informatii sau se pot gasi alte situatii in care o astfel de facilitate ar permite o utilizare generala
a memoriei mult mai economica si care s-ar dovedi benefica, spre exemplu, pe diferite sisteme / device-
uri cu putine resurse fizice (ex: smartphone-uri, console portabile, etc.).

Un pointer este o variabila ce retine o adresa din memorie unde informatia efectiva cu care se
lucreaza este stocata. Conceptual, un pointer nu este diferit de alta variabila cu care un dezvoltator de
programe poate lucra insa informatia stocata de acesta difera prin perspectiva faptului ca nu este
valoarea numerica cautata ci adresa primului octet al blocului de memorie la care este continuta
aceasta. La nivel de sintaxa, un pointer se declara in felul urmator:

char * pChar; // pChar este un pointer la tipul char


int * pInt; // pInt este un pointer la tipul intreg
int ** ppInt; // ppInt este un pointer la un pointer la tipul intreg

Fiecare din variabilele pointeri de mai sus pot contine adrese ale unor variabile / zone de
memorie catre tipul char, int, int*. Spre exemplu, urmatoarele instructiuni sunt corecte:

char * pChar; // pChar este un pointer la tipul char


int * pInt; // pInt este un pointer la tipul intreg
int ** ppInt; // ppInt este un pointer la un pointer la tipul intreg

char c1 = 'a';
int i1 = 0;

pChar = &c1; // pChar retine adresa unde a fost stocat c1 ("face referire" la c1)
pInt = &i1; // pInt retine adresa unde a fost stocat i1
ppInt = &pInt; // ppInt retine adresa unde a fost stocat pointerul pInt - sa nu pierdem din
// vedere ca si acesta este tot o variabila si este stocat intr-o anumita zona
// de memorie.

Operatia fundemantala care se aplica pointerului este dereferentierea. Prin aceasta se obtine
accesul la zona de memorie spre care indica pointerul in cauza. La nivel de sintaxa, dereferentierea se
aplica precedand pointerul cu operatorul * precum in exemplul de mai jos:

cout << *pChar; // se va afisa 'a', inf. stocata la zona de memorie spre care indica pChar - c1
cout << *pInt; // se va afisa 0
cout << *(*ppInt); // se va afisa tot 0 -> ppInt indica spre pInt care indica spre i1

O observatie foarte importanta legata de pointeri este aceea ca acestia TREBUIE initializati
inainte sa fie folositi. Daca aceasta conditie nu este respectata sistemul se va comporta instabil sau se va
bloca. Exemplul de mai jos prezinta o situatie care trebuie evitata:
int * a;

*a = 25; // se incearca scrierea lui 25 in zona de mem. indicata de a dar a NU indica o zona de
// mem initializata.
cout << a; // se incearca utilizarea lui a (pentru afisarea adresei stocate) dar acesta nu este
// initializat; nu are stocata nici o adresa / nu indica nimic.

Pentru a corecta aceasta situatie se poate utiliza urmatorul cod:

int * a; // se declara pointerul a.

a = new int; // se rezerva o zona de memorie de dimensiune 2 / 4 bytes (int in functie de


// compilator). Adresa acestei zone de memorie este stocata in variabila pointer
// a. Acum a este initializat si poate fi folosit.

*a = 25; // se scrie 25 in zona de memorie rezervata mai sus.


cout << a << *a; // se afiseaza adresa zonei de memorie si valoarea stocata in aceasta

delete a; // se marcheaza eliberarea / stergerea acestei zone de memorie. In acest fel


// blocul de mem. va putea fi folosit si de alte procese ulterior incheierii
// programului de mai sus.

Codul de mai sus prezinta o prima modalitate de ocupare a memoriei in mod dinamic, atunci
cand este necesar, si eliberarea acesteia la momentul in care toate operatiunile dependente s-au
incheiat. In acest fel se poate optimiza consumul de memorie comparativ cu modalitatea clasica de
alocare fixa. In cazul alocarii fixe ("statice"), o variabila poate exista (ocupa efectiv memorie) atata
vreme cat vizibilitatea acesteia nu este depasita. Spre exemplu o variabila globala, va fi alocata in
deschiderea programului si va fi stearsa imediat inainte de incheierea acestuia. O variabila locala unei
functii va exista atata vreme cat functia in cauza este executata. De indata ce succesiunea de instructiuni
a programului paraseste aceasta functie, variabilele locale vor fi distruse.

Exercitii:

1) Sa se declare trei variabile si trei pointeri la tipurile char, int, double. In functia main,
initializati pointerii utilizand adresele celor trei variabile declarate, initializati variabilele cititind date de
la tastatura si afisazi valorile citite folosind dereferentierea pointerilor.

2) Sa se scrie o functie void Swap(int * a, int * b) care face inversarea valorilor stocate la
variabilele primite ca argumente. De exemplu, in functia main() a programului:

int i1 = 5;
int i2 = 7;

Swap (&i1, &i2); // dupa ce functia isi incheie executia i1 trebuie sa fie 7 si i2, 5
3) Sa se scrie un program care foloseste aritmetica pointerilor pentru a determina dimensiunea
in memorie (in octeti) a unei variabile dintr-un tip de date la alegere. Pe scurt, "aritmetica pointerilor" se
refera la abilitatea de a utiliza operatii aritmetice simple asupra pointerilor (adunare, scadere,
comparatie, atribuire) cu amendamentul ca valoarea (adresa) stocata de pointer se va modifca (la
adunare / scadere) in mod dependent de tipul de date asociat. Spre exemplu, incrementarea unui
pointer pInt la tipul int va genera cu sine o crestere de doi / patru octeti a adresei stocate (in functie de
compilator) deoarece un int ocupa doi / patru octeti.

II. Alocarea dinamica a memoriei

Principalul rol al pointerilor este acela de a permite o organizare optimala dintr-un anumit punct
de vedere a memoriei cu care procesul (programul lansat in executie) lucreaza. Asa cum s-a amintit mai
sus, in cazul in care un program este dezvoltat strict cu ajutorul alocarii statice a memoriei, pot aparea
situatii in care memoria alocata ( "variabilele declarate" ) nu este suficienta sau ocuparea acesteia se
dovedeste a fi nerationala in raport cu anumite criterii. Totodata, considerand ca memoria alocata static
este stocata in zona de stiva a memoriei procesului, pot aparea situatii in care aceasta regiune nu este
suficient de mare comparativ cu necesarul de memorie dintr-un anumit moment al aplicatiei. Acest
lucru se intampla deoarece dimensiunea stivei procesului este in general mica, ea fiind gandita pentru
stocarea de variabile locale de dimensiune mica respectiv pentru stocarea adreselor de intoarcere a
functiilor, nicidecum pentru blocuri de date de dimensiune mare precum tablouri cu multe elemente,
structuri de date elaborate (liste generalizate, arbori, tabele hash, etc.), obiecte (in cazul C++), etc.

Comparativ cu C, limbajul C++ ofera o metoda alternativa de alocare dinamica a memorie cu


avantaje ce doresc sa usureze munca programatorului. Ea se bazeaza pe utilizarea operatorilor new si
delete. Daca in cazul limbajului C se folosea functia malloc specificandu-se in prealabil dimensiunea
blocului de memorie prin apelul sizeof iar ulterior se facea conversia explicita a pointerului la void intors
catre tipul dorit, new calculeaza automat dimensiunea necesara blocului de memorie si intoarce un
pointer la tipul de date dorit. Spre exemplu:

int * pMem = new int; // se aloca un bloc de 2/4 octeti. Acesta va fi indicat de pMem
int * pArrayMem = new int[10]; // se aloca un bloc de 20 / 40 octeti. Adresa de inceput va fi
// indicata de pArrayMem.

In cazul in care alocarea nu s-a incheiat cu succes, new intoarce NULL. Pentru eliberarea zonei de
memorie alocata, se foloseste operatorul delete precum in exemplele de mai jos:

delete pMem; // se elibereaza zona de memorie indicata de pMem


delete [] pArrayMem; // se elibereaza blocul de memorie indicat de pArrayMem

Remarcati in cazul secund utilizarea [] in instructiune. Este necesara specificarea operatorului []


in instructiune in cazul in care pointerul indica spre un bloc de memorie alocat cu dimensiune multipla a
unui tip de baza.
Exemplu: citirea de la tastatura a unui vector de 10 numere intregi si afisarea lor in ordine inversa
folosind memorie alocata dinamic si aritmetica pointerilor.

#include "iostream"
...
#define DIM 10
...

int main()
{
int * pArray = new int[DIM]; // pArray este adresa de inceput a vectorului
int i;

//citire
for ( i = 0; i < DIM; i++ )
{
cout << "vect[" << i <<"]= ";
cin >> *(pArray + i); // se poate utiliza si pArray[i] cu acelasi efect
}

//afisare
for( i = DIM - 1; i > -1 ; i-- )
cout <<" vect[" << i << "]=" << *(pArray + i) << "\n";

//eliberare memorie folosita


delete [] pArray;
pArray = NULL;

system("PAUSE");
return 0;
}
Exemplu: memorarea unui tablou 2D de elemente intregi (matrice)

#include "iostream"

#define DIM_X 5
#define DIM_Y 7

int main()
{
int ** pMatrix; // se foloseste un pointer la pointer la tipul intreg
int i, j;

pMatrix = new int* [DIM_X]; // aloc memorie pentru stocarea vectorului de pointeri ce retin
// liniile matricei. Asadar matricea va fi retinuta prin
// intermediul unui pointer la un vector de pointeri (de
// dimensiune DIM_X) catre liniile matricei (fiecare linie
// avand lungimea DIM_Y = numarul de coloane).

for ( i = 0; i < DIM_X; i++)


pMatrix[i] = new int[DIM_Y]; // se aloca memorie pentru fiecare linie in parte

//citire date de la tastatura


for( i = 0; i <DIM_X; i++)
for (j = 0; j < DIM_Y; j++)
{
cout << "pMatrix ["<<i<<"]["<<j<<"]= ";
cin >> pMatrix [i][j]; // se poate utiliza si *( *(pArray + i) + j )
}

.... //eventual procesare

//afisare
for( i = 0; i <DIM_X; i++)
{
for (j = 0; j < DIM_Y; j++)
{
cout << "pMatrix ["<<i<<"]["<<j<<"]= " << pMatrix [i][j] << " ";
}
cout <<"\n";
}
//eliberare memorie
for (i = 0; i < DIM_X; i++)
delete [] pMatrix [i]; //eliberez memoria pentru fiecare linie in parte
delete [] pMatrix; //eliberez si vectorul de pointeri catre liniile matricei

system("PAUSE");
return 0;
}

Exercitiul 4:
Sa se scrie o functie care calculeaza maximul de pe fiecare linie a unei matrici de intregi primite
ca parametru si intoarce rezultatele prin intermediul unui pointer la un vector de DIM_X elemente unde
fiecare element reprezinta maximul de pe linia i. Prototipul functiei va fi: int * Max(int ** pMatrix);

III. Recursivitate

Din punct de vedere matematic "o problema care expune recursivitatea are proprietatea ca se
poate defini cu ajutorul unor instante fundamentale ale acesteia respectiv prin intermediul unor reguli
care directioneaza toate celelalte cazuri catre instantele fundamentale." Exemplul clasic in acest sens
este dat de sirul lui Fibonacci:

Fib(0) = 0 (caz fundamental)


Fib(1) = 1 (caz fundamental)

(∀) n > 1: , n ∊ ℕ

Fib(n) = (Fib(n-1) + Fib(n-2)) (regula)

Asadar Fib(4) = ( Fib(3) + Fib(2) ) = [ ( Fib(2) + Fib(1) ) + (Fib(1) + Fib(0) ] =

= { [ ( Fib(1) + Fib(0) ) + Fib(1) ] + [ Fib(1) + Fib(0) ] } =

= { [ (1 + 0) + 1 ] + [1 + 0] } = [ (1 + 1) + (1 + 0) ] = (2 + 1) = 3

Se observa ca pentru rezolvarea problemei fiecare caz oarecare a fost descompus pana la
atingerea unui caz fundamental moment in care procesul de agregare a rezultatului a inceput prin
executarea operatiilor de adunare, urmand calea inversa descompunerii.

In acelasi mod se poate analiza recursivitatea si atunci cand vine vorba de proprietatea, expusa
de un limbaj de programare, de a permite unei functii sa se auto-apeleze. Cele doua etape fundamentale
de rezolvare a problemei sunt evident cele descrise mai sus:
- descompunerea problemei in instante din ce in ce mai simple (prin auto-apelari) pana la
atingerea unui caz fundamental.
- revenirea din apelurile recursive si construirea rezultatului.
Exemplu: Implementarea problemei de calcul a elementului k din sirul lui Fibonacci

unsigned int GetFib(unsigned int k)


{
if(k == 0) // conditie fundamentala, de oprire
return 0;

if(k == 1)
return 1; // conditie fundamentala, de oprire

return GetFib(k-1) + GetFib(k-2) ; // daca nici una din conditiile de mai sus nu este indeplinita,
// descompun problema
}

Reluand exemplul matematic de mai sus, pentru o variabila de intrare k = 4, se arata mai jos in
Fig. 1, succesiunea in care instructiunile sunt executate. Ordinea de procesare a instructiunilor, in figura,
se va face de la stanga la dreapta; fiecare nou apel va genera o avansare in arbore pana la intalnirea unei
conditii de oprire. Ordinea initiala a apelurilor va fi GetFib(4) -> GetFib(3) -> GetFib(2) -> GetFib(1) (o
deplasare pe ramurile din stanga) urmand o intercalare cu revenirea din apeluri de indata ce s-a intalnit
o conditie de oprire. Asadar, din apelul GetFib(1) de la cel mai scazut nivel al arborelui se intalneste
prima conditie de oprire (" (prima) ") iar executia programului este redata functiei apelante (GetFib(2)
de pe nivelul imediat de deasupra) care trebuie sa execute acum GetFib(0). Si aici se intalneste o
conditie de oprire ( " (a doua) " ) iar apelul este din nou redat lui GetFib(2). Aceasta are acum ambii
operanzi pentru adunare si realizeaza prima agregare a rezultatului. Odata ce adunearea a fost
executata GetFib(2) reda executia programului lui GetFib(3) de la primul nivel al arborelui care, acum, va
apela GetFib(1) de la nivelul inferior pentru a obtine si cel de-al doilea operand instructiunea sa de
agregare. GetFib(1) intalneste "(a treia)" conditie de oprire si intoarce catre GetFib(3) valoarea 1. Acum
GetFib(3) poate face agregarea si va aduna 1 + 1 obtinand primul operand din GetFib(4) (apelul initial).

GetFib(4) (3) --> GetFib(3) (2) + GetFib(2) (1)


| |
V V
GetFib(2) (1) + GetFib(1) (1) GetFib(1) (1) + GetFib(0) (0)
| | | |
V V V V
GetFib(1) (1) + GetFib(0)(0) 1 1 0
| | (a treia) (a patra) (a cincea)
V V
1 0
(prima) (a doua)
OBS: initial k = 4; la fiecare nivel parametrul k este decrementat

Figura 1 - Ordinea de executie a programului in cazul rezolvarii problemei sirului lui Fibonacci
Exercitii:

1) Sa se implementeze in mod recursiv problema calcularii lui n!


2) Sa se implementeze in mod recursiv problema calcularii sumei primelor n numere naturale.
3) Sa se implementeze in mod recursiv o functie care ia drept parametru un pointer catre un sir de
caractere terminat in NULL si calculeaza lungimea acestuia.

S-ar putea să vă placă și