This shows you the differences between two versions of the page.
programare-ca:laboratoare:lab08 [2012/11/12 19:45] laura.vasilescu |
— (current) | ||
---|---|---|---|
Line 1: | Line 1: | ||
- | ===== Alocarea dinamică a memoriei. Aplicaţii folosind tablouri şi matrice. ===== | ||
- | **Responsabil:** [[laura.vasilescu@cti.pub.ro|Laura Vasilescu]] | ||
- | |||
- | |||
- | ===== Obiective ===== | ||
- | |||
- | În urma parcurgerii acestui laborator studentul va fi capabil: | ||
- | * să aloce dinamic o zona de memorie; | ||
- | * să elibereze o zona de memorie; | ||
- | * să lucreze cu vectori şi matrice alocate dinamic. | ||
- | |||
- | ===== Funcţii de alocare şi eliberare a memoriei ===== | ||
- | |||
- | Funcțiile standard de alocare și de eliberare a memoriei sunt declarate în fişierul antet ''stdlib.h''. | ||
- | * ''void * malloc(size_t size);'' | ||
- | * ''void * calloc(size_t nmemb, size_t size);'' | ||
- | * ''void * realloc(void *ptr, size_t size);'' | ||
- | * ''void free(void *ptr);'' | ||
- | |||
- | ==== Alocarea memoriei ===== | ||
- | |||
- | Cele trei funcţii de alocare (''malloc'', ''calloc'' și ''realloc'') au ca rezultat adresa zonei de memorie alocate (de tip ''void*'') şi ca argument comun dimensiunea, în octeţi, a zonei de memorie alocate (de tip ''size_t'' ). Dacă cererea de alocare nu poate fi satisfăcută pentru că nu mai există un bloc continuu de dimensiunea solicitată, atunci funcţiile de alocare au rezultat ''NULL'' (ce reprezintă un pointer de tip ''void *'' la adresa de memorie 0, care prin convenţie este o adresă nevalidă - nu există date stocate în acea zonă). | ||
- | |||
- | == Exemplu == | ||
- | <code c> | ||
- | char *str = malloc(30); // Aloca memorie pentru 30 de caractere | ||
- | int *a = malloc(n * sizeof(int)); // Aloca memorie pt. n numere intregi | ||
- | </code> | ||
- | |||
- | <note>Dimensiunea memoriei luată ca parametru de ''malloc()'' este specificată în ''octeţi'', indiferent de tipul de date care va fi stocat în acea regiune de memorie! Din acest motiv, pentru a aloca suficientă memorie, numărul dorit de elemente trebuie înmulţit cu dimensiunea unui element, atunci când are loc un apel ''malloc()''.</note> | ||
- | |||
- | |||
- | Alocarea de memorie pentru un vector şi iniţializarea zonei alocate cu zerouri se poate face cu funcţia ''calloc''. | ||
- | == Exemplu == | ||
- | <code c> | ||
- | int *a= calloc(n, sizeof(int)); // Aloca memorie pentru n numere intregi și inițializează zona cu zero | ||
- | </code> | ||
- | |||
- | Codul de mai sus este perfect echivalent (dar mai rapid) cu următoarea secvenţă de instrucţiuni: | ||
- | <code c> | ||
- | int i; | ||
- | int *a = (malloc(n * sizeof(int)); | ||
- | for (i = 0; i < n; i++) { | ||
- | a[i] = 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | <note>În timp ce funcţia ''malloc()'' ia un singur parametru (o dimensiune în octeţi), funcţia ''calloc()'' primeşte două argumente, o lungime de vector şi o dimensiune a fiecărui element. Astfel, această funcţie este specializată pentru memorie organizată ca un vector, în timp ce ''malloc()'' nu ţine cont de structura memoriei.</note> | ||
- | <note warning>Acest lucru aduce și o măsură de siguranță în plus deoarece înmulțirea dintre numărul de elemente și dimensiunea tipului de date ar putea face overflow, iar dimensiunea memoriei alocate să nu fie în realitatea cea așteptată.</note> | ||
- | |||
- | Realocarea unui vector care creşte (sau scade) faţă de dimensiunea estimată anterior se poate face cu funcţia ''realloc'', care primeşte adresa veche şi noua dimensiune şi întoarce **noua adresă**: | ||
- | <code c> | ||
- | a = realloc (a, 2 * n * sizeof(int)); // Dublare dimensiune anterioara (n) | ||
- | </code> | ||
- | |||
- | În exemplul anterior, noua adresă este memorată tot în variabila pointer ''a'', înlocuind vechea adresă (care nu mai este necesară şi nici nu mai trebuie folosită). Funcţia ''realloc()'' realizează următoarele operaţii: | ||
- | * alocă o zonă de dimensiunea specificată ca al doilea argument | ||
- | * copiază la noua adresă datele de la adresa veche (primul argument al funcţiei) | ||
- | * eliberează memoria de la adresa veche. | ||
- | |||
- | ==== Eliberarea memoriei ===== | ||
- | |||
- | Funcţia ''free()'' are ca argument o adresă (un pointer) şi eliberează zona de la adresa respectivă (alocată prin apelul unei funcţii de tipul ''[m|c|re]alloc''). Dimensiunea zonei nu mai trebuie specificată deoarece este ţinută minte de sistemul de alocare de memorie în nişte structuri interne. | ||
- | |||
- | ===== Vectori alocaţi dinamic ===== | ||
- | |||
- | Structura de vector are avantajul simplităţii şi economiei de memorie faţă de alte structuri de date folosite pentru memorarea unei colectii de informaţii între care există anumite relaţii. Între cerinţa de dimensionare constantă a unui vector şi generalitatea programelor care folosesc astfel de vectori există o contradicţie. De cele mai multe ori programele pot afla (din datele citite) dimensiunile vectorilor cu care lucrează şi deci pot face o alocare dinamică a memoriei pentru aceşti vectori. Aceasta este o solutie mai flexibilă, care foloseşte mai bine memoria disponibilă şi nu impune limitări arbitrare asupra utilizării unor programe. În limbajul C nu există practic nici o diferenţă între utilizarea unui vector cu dimensiune fixă şi utilizarea unui vector alocat dinamic, ceea ce încurajează şi mai mult utilizarea unor vectori cu dimensiune variabilă. | ||
- | |||
- | == Exemplu == | ||
- | <code c> | ||
- | int main() { | ||
- | int n,i; | ||
- | int *a; // Adresa vector alocat dinamic | ||
- | |||
- | printf("n = "); | ||
- | scanf "%d", &n); // Dimensiune vector | ||
- | |||
- | a = calloc(n, sizeof(int)); // Alternativ: a = malloc(n * sizeof(int)); | ||
- | printf("Componente vector: \n"); | ||
- | |||
- | for (i = 0; i < n; i++) | ||
- | scanf("%d", &a[i]); // Sau scanf (“%d”, a+i); | ||
- | for (i = 0; i < n; i++) // Afisare vector | ||
- | printf("%d ",a[i]); | ||
- | |||
- | free(a); // Nu uitam sa eliberam memoria | ||
- | |||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | Există şi cazuri în care datele memorate într-un vector rezultă din anumite prelucrări, iar numărul lor nu poate fi cunoscut de la începutul execuţiei. Un exemplu poate fi un vector cu toate numerele prime mai mici ca o valoare dată. În acest caz se poate recurge la o realocare dinamică a memoriei. În exemplul următor se citeşte un număr necunoscut de valori întregi într-un vector extensibil: | ||
- | <code c> | ||
- | #define INCR 100 // cu cat creste vectorul la fiecare realocare | ||
- | |||
- | int main() | ||
- | { | ||
- | int n, i, m; | ||
- | float x, *v; // v = adresa vector | ||
- | n = INCR; | ||
- | i = 0; | ||
- | |||
- | v = malloc(n * sizeof(float)); // Dimensiune initiala vector | ||
- | | ||
- | while (scanf("%f", &x) != EOF) { | ||
- | if (++i == n) { // Daca este necesar... | ||
- | n = n + INCR; // ... creste dimensiune vector | ||
- | v = realloc(vector, n * sizeof(float)); | ||
- | } | ||
- | |||
- | v[i] = x; // Memorare in vector numar citit | ||
- | } | ||
- | |||
- | for (i = 0; i < n; i++) // Afisare vector | ||
- | printf("%f ",v[i]); | ||
- | |||
- | free(vector); | ||
- | |||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | ===== Matrice alocate dinamic ===== | ||
- | |||
- | Alocarea dinamică pentru o matrice este importantă deoarece: | ||
- | * foloseşte economic memoria şi evită alocări acoperitoare, estimative. | ||
- | * permite matrice cu linii de lungimi diferite (denumite uneori ''ragged arrays'', datorită formelor "zimţate" din reprezentările grafice) | ||
- | * reprezintă o soluţie bună la problema argumentelor de funcţii de tip matrice. | ||
- | |||
- | Daca programul poate afla numărul efectiv de linii şi de coloane al unei matrice (cu dimensiuni diferite de la o execuţie la alta), atunci se va aloca memorie pentru un vector de pointeri (funcţie de numărul liniilor) şi apoi se va aloca memorie pentru fiecare linie (funcţie de numărul coloanelor) cu memorarea adreselor liniilor în vectorul de pointeri. O astfel de matrice se poate folosi la fel ca o matrice declarată cu dimensiuni constante. | ||
- | |||
- | == Exemplu == | ||
- | <code c> | ||
- | int main () { | ||
- | int **a; | ||
- | int i, j, nl, nc; | ||
- | |||
- | printf("nr. linii = "); scanf (“%d”, &nl); | ||
- | |||
- | printf("nr. coloane = "); scanf (“%d”, &nc); | ||
- | |||
- | a = malloc(nl * sizeof(int *)); // Alocare pentru vector de pointeri | ||
- | |||
- | for (i = 0; i < n; i++) | ||
- | a[i] = calloc(nc, sizeof(int)); // Alocare pentru o linie si initializare la zero | ||
- | |||
- | // Completare diagonala matrice unitate | ||
- | for (i = 0; i < nl; i++) | ||
- | a[i][i] = 1; // a[i][j]=0 pentru i != j | ||
- | |||
- | // Afisare matrice | ||
- | printmat(a, nl, nc); | ||
- | |||
- | free(a); // Nu uitam sa eliberam! | ||
- | |||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | Funcţia de afişare a matricei se poate defini astfel: | ||
- | <code c> | ||
- | void printmat(int **a, int nl, int nc) { | ||
- | for (i = 0; i < nl; i++) { | ||
- | for (j = 0; j < nc; j++) | ||
- | printf("%2d", a[i][j]); | ||
- | |||
- | printf("\n"); | ||
- | } | ||
- | } | ||
- | </code> | ||
- | |||
- | Notaţia ''a[i][j]'' este interpretată astfel pentru o matrice alocată dinamic: | ||
- | * '' a[i]'' conţine un pointer (o adresă ''b'') | ||
- | * '' b[j]'' sau ''b+j'' conţine întregul din poziţia ''j'' a vectorului cu adresa ''b''. | ||
- | |||
- | Astfel, ''a[i][j]'' este echivalent semantic cu expresia cu pointeri ''*(*(a + i) + j)''. | ||
- | |||
- | Totuşi, funcţia ''printmat()'' dată anterior nu poate fi apelată dintr-un program care declară argumentul efectiv ca o matrice cu dimensiuni constante. Exemplul următor este corect sintactic dar nu se execută corect: | ||
- | <code c> | ||
- | int main() { | ||
- | int x[2][2] = { {1, 2}, {3, 4} }; // O matrice patratica cu 2 linii si 2 coloane | ||
- | printmat((int**)x, 2, 2); | ||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | Explicaţia este interpretarea diferită a conţinutului zonei de la adresa aflată în primul argument: funcţia ''printmat()'' consideră că este adresa unui vector de pointeri (''int *a[]''), iar programul principal consideră că este adresa unui vector de vectori (''int x[][2]''), care este reprezentat liniar in memorie. | ||
- | |||
- | Se poate defini şi o funcţie pentru alocarea de memorie la execuţie pentru o matrice. | ||
- | |||
- | == Exemplu == | ||
- | <code c> | ||
- | int **newmat(int nl, int nc) { // Rezultat adresa matrice | ||
- | int i; | ||
- | int **p = malloc(nl * sizeof(int *)); | ||
- | |||
- | for (i = 0; i < n; i++) | ||
- | p[i] = calloc(nc, sizeof(int)); | ||
- | |||
- | return p; | ||
- | } | ||
- | </code> | ||
- | |||
- | ===== Stil de programare ===== | ||
- | ==== Exemple de programe ==== | ||
- | |||
- | **''Exemplul 1''**: Funcţie echivalentă cu funcţia de bibliotecă ''strdup()'': | ||
- | <code c> | ||
- | #include <string.h> | ||
- | #include <alloc.h> | ||
- | |||
- | // Alocare memorie si copiere sir | ||
- | char *strdup(char* adr) { | ||
- | int len = strlen(adr); | ||
- | char *rez = malloc(len); | ||
- | |||
- | strcpy(rez, adr); | ||
- | |||
- | return adr; | ||
- | } | ||
- | </code> | ||
- | |||
- | <code c> | ||
- | // Utilizare "strdup" | ||
- | #include <stdio.h> | ||
- | |||
- | int main() { | ||
- | char s[80], *d; | ||
- | |||
- | do { | ||
- | if (gets(s) == 0) | ||
- | break; | ||
- | d = strdup(s); | ||
- | puts(d); | ||
- | free(d); | ||
- | } while (1); | ||
- | |||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | **'''Exemplul 2''**: Vector alocat dinamic (cu dimensiune cunoscută la execuţie) | ||
- | <code c> | ||
- | #include <stdio.h> | ||
- | #include <stdlib.h> | ||
- | |||
- | int main() { | ||
- | int n, i; | ||
- | int *a; // Adresa vector | ||
- | |||
- | printf("n="); scanf("%d", &n); // Dimensiune vector | ||
- | |||
- | a = malloc(n * sizeof(int)); | ||
- | printf("componente vector: \n"); | ||
- | |||
- | for (i = 0; i < n; i++) // Citire vector | ||
- | scanf("%d", &a[i]); | ||
- | |||
- | for (i = 0; i < n; i++) // Afisare vector | ||
- | printf("%d", a[i]); | ||
- | |||
- | free(a); | ||
- | |||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | **'' Exemplul 3''**: Vector realocat dinamic (cu dimensiune necunoscută) | ||
- | |||
- | <code c> | ||
- | #include <stdio.h> | ||
- | #include <stdlib.h> | ||
- | |||
- | #define INCR 4 | ||
- | |||
- | int main() { | ||
- | int n, i, m; | ||
- | float x, *v; | ||
- | |||
- | n = INCR; | ||
- | i = 0; | ||
- | |||
- | v = (malloc(n * sizeof(float)); | ||
- | |||
- | while (scanf("%f", &x) != EOF) { | ||
- | if (i == n) { | ||
- | n = n + INCR; | ||
- | v = (realloc(v, n * sizeof(float)); | ||
- | } | ||
- | |||
- | v[i++] = x; | ||
- | } | ||
- | |||
- | m = i; | ||
- | |||
- | for (i = 0; i < m; i++) | ||
- | printf("%.2f ", v[i]); | ||
- | | ||
- | free(v); | ||
- | |||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | **''Exemplul 4''**: Matrice alocată dinamic (cu dimensiuni cunoscute la execuţie) | ||
- | |||
- | <code c> | ||
- | #include <stdio.h> | ||
- | #include <stdlib.h> | ||
- | |||
- | int main() { | ||
- | int n, i, j; | ||
- | int **mat; // Adresa matrice | ||
- | |||
- | // Citire dimensiuni matrice | ||
- | printf("n = "); scanf("%d", &n); | ||
- | |||
- | // Alocare memorie ptr matrice | ||
- | mat = malloc(n * sizeof(int *)); | ||
- | |||
- | for (i = 0; i < n; i++) | ||
- | mat[i] = calloc(n, sizeof(int)); | ||
- | |||
- | // Completare matrice | ||
- | for (i = 0; i < n; i++) | ||
- | for (j = 0; j < n; j++) | ||
- | mat[i][j] = n * i + j + 1; | ||
- | |||
- | // Afisare matrice | ||
- | for (i = 0; i < n; i++) { | ||
- | for (j = 0;j < n; j++) | ||
- | printf("%6d", mat[i][j]); | ||
- | |||
- | printf("\n"); | ||
- | } | ||
- | |||
- | return 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | **'''Exemplul 5''**: Vector de pointeri la şiruri alocate dinamic | ||
- | <code c> | ||
- | /* Creare / afisare vector de pointeri la siruri */ | ||
- | |||
- | #include <stdio.h> | ||
- | #include <stdlib.h> | ||
- | #include <string.h> | ||
- | |||
- | // Afisare siruri reunite in vector de pointeri | ||
- | void printstr(char *vp[], int n) { | ||
- | int i; | ||
- | |||
- | for(i = 0; i < n; i++) | ||
- | printf("%s\n", vp[i]); | ||
- | } | ||
- | |||
- | // Ordonare vector de pointeri la siruri | ||
- | void sort(char *vp[], int n) { | ||
- | int i, j; | ||
- | char *tmp; | ||
- | |||
- | for (j = 1; j < n; j++) | ||
- | for (i = 0; i < n - 1; i++) | ||
- | if (strcmp(vp[i], vp[i+1]) > 0) { | ||
- | tmp = vp[i]; | ||
- | vp[i] = vp[i+1]; | ||
- | vp[i+1] = tmp; | ||
- | } | ||
- | } | ||
- | |||
- | // Citire siruri si creare vector de pointeri | ||
- | int readstr (char * vp[]) { | ||
- | int n = 0; | ||
- | char *p, sir[80]; | ||
- | |||
- | while (scanf("%s", sir) == 1) { | ||
- | p = malloc(strlen(sir) + 1); | ||
- | strcpy(p, sir); | ||
- | vp[n] = p; | ||
- | ++n; | ||
- | } | ||
- | |||
- | return n; | ||
- | } | ||
- | |||
- | int main() { | ||
- | int n; char *vp[1000]; // vector de pointeri, cu dimensiune fixa | ||
- | |||
- | n = readstr(vp); // citire siruri si creare vector | ||
- | sort(vp, n); // ordonare vector | ||
- | printstr(vp, n); // afisare siruri | ||
- | |||
- | retrun 0; | ||
- | } | ||
- | </code> | ||
- | |||
- | ==== Practici recomandate ==== | ||
- | |||
- | Deşi au fost enunţate în momentul în care au fost introduse noţiunile corespunzătoare în cursul acestui material, se pot rezuma câteva reguli importante de folosire a variabilelor de tip pointer: | ||
- | * Aveţi grijă ca variabilele de tip pointer să indice către adrese de memorie valide înainte de a fi folosite; consecinţele adresării unei zone de memorie aleatoare sau nevalide (''NULL'') pot fi dintre cele mai imprevizibile. | ||
- | * Utilizaţi o formatare a codului care să sugereze asocierea operatorului ''*'' cu variabila asupra căreia operează; acest lucru este în special valabil pentru declaraţiile de pointeri. | ||
- | * Nu returnaţi pointeri la variabile sau tablouri definite în cadrul funcţiilor, întrucât valabilitatea acestora încetează odată cu ieşirea din corpul funcţiei. | ||
- | * Verificaţi rezultatul funcţiilor de alocare a memoriei, chiar dacă dimensiunea pe care doriţi s-o rezervaţi este mică. Atunci când memoria nu poate fi alocată rezultatul este ''NULL'' iar programul vostru ar trebui să trateze explicit acest caz (finalizat, de obicei, prin închiderea "curată" a aplicaţiei). | ||
- | * Nu uitaţi să eliberaţi memoria alocată dinamic, folosind funcţia ''free()''. Memoria rămasă neeliberată încetineşte performanţele sistemului şi poate conduce la erori (bug-uri) greu de depistat. | ||
- | |||
- | ===== Anexă: Clase de stocare ===== | ||
- | |||
- | <note>Această secţiune este opţională şi nu este necesară pentru rezolvarea exerciţiilor de laborator, însă ajută la înţelegerea aprofundată a modului în care limbajul C lucrează cu variabilele.</note> | ||
- | |||
- | Clasa de stocare (memorare) arată când, cum şi unde se alocă memorie pentru o variabilă (vector). Orice variabilă C are o clasă de memorare care rezultă fie dintr-o declaraţie explicită, fie implicit din locul unde este definită variabila. Există trei moduri de alocare a memoriei, dar numai două corespund unor clase de memorare: | ||
- | * ''Static'': memoria este alocată la compilare în segmentul de date din cadrul programului şi nu se mai poate modifica în cursul execuţiei. Variabilele externe, definite în afara funcţiilor, sunt implicit statice, dar pot fi declarate static şi variabile locale, definite în cadrul funcţiilor. | ||
- | * ''Automat'': memoria este alocată automat, la activarea unei funcţii, în zona stivă alocată unui program şi este eliberată automat la terminarea funcţiei. Variabilele locale unui bloc (unei funcţii) şi argumentele formale sunt implicit din clasa ''auto''. | ||
- | * ''Dinamic'': memoria se alocă la execuţie în zona ''heap'' alocată programului, dar numai la cererea explicită a programatorului, prin apelarea unor funcţii de bibliotecă (''malloc'', ''calloc'', ''realloc''). Memoria este eliberată numai la cerere, prin apelarea funcţiei ''free''. Variabilele dinamice nu au nume şi deci nu se pune problema clasei de memorare (atribut al variabilelor cu nume). | ||
- | |||
- | Variabilele statice pot fi iniţializate numai cu valori constante (pentru că se face la compilare), dar variabilele auto pot fi iniţializate cu rezultatul unor expresii (pentru că se face la execuţie). Toate variabilele externe (şi statice) sunt automat iniţializate cu valori zero (inclusiv vectorii). Cantitatea de memorie alocată pentru variabilele cu nume rezultă automat din tipul variabilei şi din dimensiunea declarată pentru vectori. Memoria alocată dinamic este specificată explicit ca parametru al funcţiilor de alocare. | ||
- | |||
- | O a treia clasă de memorare este clasa ''register'' pentru variabile cărora, teoretic, li se alocă registre ale procesorului şi nu locaţii de memorie, pentru un timp de acces mai bun. În practică nici un compilator modern nu mai ţine cont de acest cuvânt cheie, folosind automat registre atunci când codul poate fi optimizat în acest fel (de exemplu când observă că nu se accesează niciodata adresa variabilei în program). | ||
- | |||
- | Memoria neocupată de datele statice şi de instrucţiunile unui program este împărţită între stivă şi heap. Consumul de memorie pe stivă este mai mare în programele cu funcţii recursive şi număr mare de apeluri recursive, iar consumul de memorie heap este mare în programele cu vectori şi matrice alocate (şi realocate) dinamic. | ||
- | |||
- | ===== Exerciţii de Laborator ===== |