Cum funcționează alocarea dinamică în C
Cum funcționează alocarea dinamică în C
Cum funcționează alocarea dinamică în C
În limbajul C, gestionarea memoriei este un aspect crucial. Până acum, probabil ai lucrat cu variabile a căror dimensiune este cunoscută la momentul compilării (alocare statică) sau cu tablouri a căror dimensiune este fixă.
Însă, în multe situații, nu știi exact de câtă memorie vei avea nevoie decât în timpul execuției programului. Aici intervine alocarea dinamică a memoriei.
Alocarea dinamică îți permite să ceri memorie de la sistemul de operare în timp ce programul rulează și să o eliberezi atunci când nu mai ai nevoie de ea. Aceasta oferă flexibilitate și eficiență în utilizarea resurselor sistemului.
De ce Avem Nevoie de Alocare Dinamică?
- Dimensiuni necunoscute la compilare: Nu știi câte elemente va trebui să stochezi (de exemplu, citind dintr-un fișier sau de la utilizator).
- Utilizare eficientă a memoriei: Aloci memorie doar atunci când ai nevoie de ea și o eliberezi când nu mai este necesară, evitând risipa de memorie.
- Structuri de date complexe: Permite crearea de structuri de date precum liste înlănțuite, arbori, grafuri, unde dimensiunea și structura se schimbă în timpul execuției.
Funcțiile de Alocare Dinamică în C
În C, alocarea dinamică a memoriei se realizează cu ajutorul unor funcții standard din biblioteca <stdlib.h>
:
malloc()
(Memory Allocation)calloc()
(Contiguous Allocation)realloc()
(Reallocation)free()
(Eliberarea memoriei)
1. malloc()
Funcția malloc()
alocă un bloc de memorie de o anumită dimensiune (în octeți) și returnează un pointer de tip void *
(un pointer generic) către începutul blocului alocat. Dacă alocarea eșuează (de exemplu, nu mai este suficientă memorie disponibilă), malloc()
returnează NULL
.
Sintaxa:
void* malloc (size_t size);
size
: Numărul de octeți de alocat.
Exemplu: Alocarea unui singur întreg
#include <stdio.h>
#include <stdlib.h> // Pentru malloc si free
int main() {
int *ptr_int; // Declaram un pointer la int
// Alocam memorie pentru un singur int
ptr_int = (int*) malloc(sizeof(int)); // sizeof(int) returneaza dimensiunea in octeti a unui int
// Verificam daca alocarea a reusit
if (ptr_int == NULL) {
printf("Eroare la alocarea memoriei!\n");
return 1; // Iesire cu cod de eroare
}
// Acum putem folosi memoria alocata
*ptr_int = 100;
printf("Valoarea alocata dinamic: %d\n", *ptr_int);
// Eliberam memoria alocata
free(ptr_int);
ptr_int = NULL; // O buna practica: setam pointerul la NULL dupa eliberare
return 0;
}
- Explicație:
- Am declarat un pointer la
int
. - Am apelat
malloc(sizeof(int))
pentru a aloca memorie suficientă pentru un întreg.sizeof(int)
este utilizat pentru a asigura portabilitatea codului (dimensiunea unuiint
poate varia pe sisteme diferite). - Am făcut un cast explicit la
(int*)
deoarecemalloc
returneazăvoid *
. Deși în C casting-ul de lavoid *
la un alt tip de pointer este implicit, este o bună practică să-l includem pentru claritate și compatibilitate cu C++. - Am verificat dacă alocarea a returnat
NULL
. Dacă da, înseamnă că nu s-a putut aloca memorie. - Am folosit operatorul de dereferențiere (
*
) pentru a accesa și modifica valoarea stocată în memoria alocată. - Am apelat
free(ptr_int)
pentru a elibera memoria. Este esențial să eliberați memoria alocată dinamic pentru a evita pierderile de memorie (memory leaks). - Setarea pointerului la
NULL
după eliberare ajută la prevenirea utilizării unui pointer care indică spre o zonă de memorie eliberată (dangling pointer).
- Am declarat un pointer la
Exemplu: Alocarea unui tablou de întregi
#include <stdio.h>
#include <stdlib.h>
int main() {
int *tablou;
int n, i;
printf("Introduceti numarul de elemente: ");
scanf("%d", &n);
// Alocam memorie pentru n intregi
tablou = (int*) malloc(n * sizeof(int));
if (tablou == NULL) {
printf("Eroare la alocarea memoriei pentru tablou!\n");
return 1;
}
// Citim elementele in tablou
printf("Introduceti %d elemente:\n", n);
for (i = 0; i < n; i++) {
scanf("%d", &tablou[i]);
}
// Afisam elementele din tablou
printf("Elementele introduse sunt:\n");
for (i = 0; i < n; i++) {
printf("%d ", tablou[i]);
}
printf("\n");
// Eliberam memoria alocata
free(tablou);
tablou = NULL;
return 0;
}
- Explicație:
- Am citit numărul de elemente (
n
) de la utilizator. - Am alocat memorie pentru
n
întregi folosindmalloc(n * sizeof(int))
. - Am folosit pointerul
tablou
ca și cum ar fi un tablou, accesând elementele cutablou[i]
. Aceasta este posibil deoarece memoria alocată demalloc
este contiguă. - Am eliberat memoria la sfârșit.
- Am citit numărul de elemente (
2. calloc()
Funcția calloc()
alocă un bloc de memorie pentru un număr specificat de elemente, fiecare având o anumită dimensiune. Un avantaj major al lui calloc()
este că inițializează toți octeții din blocul alocat cu zero. Ca și malloc()
, returnează NULL
în caz de eșec.
Sintaxa:
void* calloc (size_t num, size_t size);
num
: Numărul de elemente de alocat.size
: Dimensiunea (în octeți) a fiecărui element.
Exemplu: Alocarea unui tablou de întregi cu inițializare la zero
#include <stdio.h>
#include <stdlib.h>
int main() {
int *tablou;
int n, i;
printf("Introduceti numarul de elemente: ");
scanf("%d", &n);
// Alocam memorie pentru n intregi si le initializam la zero
tablou = (int*) calloc(n, sizeof(int));
if (tablou == NULL) {
printf("Eroare la alocarea memoriei pentru tablou!\n");
return 1;
}
// Afisam elementele initiale (vor fi toate zero)
printf("Elementele initiale (initializate cu 0):\n");
for (i = 0; i < n; i++) {
printf("%d ", tablou[i]);
}
printf("\n");
// Putem folosi memoria acum
// ... (citire sau modificare elemente) ...
// Eliberam memoria alocata
free(tablou);
tablou = NULL;
return 0;
}
- Explicație:
- Sintaxa
calloc(n, sizeof(int))
este echivalentă cumalloc(n * sizeof(int))
, dar cu avantajul că memoria este inițializată cu zero. - Utilitatea inițializării la zero este evidentă în cazul tablourilor numerice sau al structurilor unde se dorește o valoare implicită de zero.
- Sintaxa
3. realloc()
Funcția realloc()
încearcă să redimensioneze un bloc de memorie alocat anterior dinamic. Poate extinde sau reduce dimensiunea blocului. Există câteva scenarii posibile:
- Dacă blocul curent are spațiu suficient la sfârșit, blocul poate fi extins în loc.
- Dacă nu este suficient spațiu la sfârșit,
realloc()
alocă un nou bloc de memorie de dimensiunea cerută, copiază conținutul vechiului bloc în noul bloc și eliberează vechiul bloc. - Dacă alocarea eșuează,
realloc()
returneazăNULL
și blocul de memorie original rămâne neschimbat.
Sintaxa:
void* realloc (void* ptr, size_t size);
ptr
: Pointerul la blocul de memorie alocat anterior. Poate fiNULL
, caz în carerealloc()
se comportă camalloc()
.size
: Noua dimensiune (în octeți) a blocului. Dacă este zero,realloc()
se comportă cafree()
.
Exemplu: Redimensionarea unui tablou
#include <stdio.h>
#include <stdlib.h>
int main() {
int *tablou;
int n_initial, n_nou, i;
printf("Introduceti numarul initial de elemente: ");
scanf("%d", &n_initial);
// Alocam memorie initiala
tablou = (int*) malloc(n_initial * sizeof(int));
if (tablou == NULL) {
printf("Eroare la alocarea memoriei initiale!\n");
return 1;
}
printf("Introduceti %d elemente initiale:\n", n_initial);
for (i = 0; i < n_initial; i++) {
scanf("%d", &tablou[i]);
}
printf("Elementele initiale:\n");
for (i = 0; i < n_initial; i++) {
printf("%d ", tablou[i]);
}
printf("\n");
printf("Introduceti noul numar de elemente: ");
scanf("%d", &n_nou);
// Redimensionam tabloul
int *temp_tablou = (int*) realloc(tablou, n_nou * sizeof(int));
// Verificam daca realloc a reusit
if (temp_tablou == NULL) {
printf("Eroare la redimensionarea memoriei!\n");
// Atentie: in caz de eroare, blocul initial (tablou) ramane valid
free(tablou); // Eliberam memoria initiala
return 1;
} else {
tablou = temp_tablou; // Actualizam pointerul la noul bloc
}
// Daca noul numar este mai mare, putem adauga noi elemente
if (n_nou > n_initial) {
printf("Introduceti elementele suplimentare:\n");
for (i = n_initial; i < n_nou; i++) {
scanf("%d", &tablou[i]);
}
}
printf("Elementele dupa redimensionare:\n");
for (i = 0; i < n_nou; i++) {
printf("%d ", tablou[i]);
}
printf("\n");
// Eliberam memoria alocata dinamic (noul bloc)
free(tablou);
tablou = NULL;
return 0;
}
- Explicație:
- Am alocat un bloc inițial cu
malloc
. - Am folosit
realloc
pentru a încerca să redimensionăm blocul lan_nou * sizeof(int)
. - Foarte important: Am atribuit rezultatul lui
realloc
unui pointer temporar (temp_tablou
) înainte de a-l atribui pointerului original (tablou
). Acest lucru este crucial deoarece, dacărealloc
returneazăNULL
(indicând o eroare), blocul de memorie original (tablou
) rămâne valid și îl putem elibera. Dacă am atribui direct rezultatul eșuat al luirealloc
latablou
, am pierde referința la blocul original și am avea o pierdere de memorie. - Dacă redimensionarea a reușit, actualizăm pointerul
tablou
la noul bloc. - Am arătat cum se pot adăuga elemente dacă noul tablou este mai mare.
- Am eliberat memoria noului bloc la sfârșit.
- Am alocat un bloc inițial cu
4. free()
Funcția free()
este utilizată pentru a elibera memoria alocată anterior dinamic de către malloc()
, calloc()
, sau realloc()
. Eliberarea memoriei o face disponibilă pentru a fi utilizată de alte părți ale programului sau de către alte procese.
Sintaxa:
void free (void* ptr);
ptr
: Pointerul la blocul de memorie care trebuie eliberat. Dacăptr
esteNULL
, apelul lafree()
nu are niciun efect.
Reguli Importante pentru free()
:
- Eliberează doar memorie alocată dinamic: Nu apela
free()
pe pointeri care nu indică spre memorie alocată cumalloc
,calloc
, saurealloc
(de exemplu, pointeri la variabile locale sau globale). - Eliberează o singură dată: Nu apela
free()
pe același bloc de memorie de mai multe ori. Acest lucru poate duce la comportament nedefinit (de exemplu, crash-uri). - Verifică pentru
NULL
: Deșifree(NULL)
este sigur, este o bună practică să verifici dacă un pointer esteNULL
înainte de a încerca să-l eliberezi, mai ales după un apel eșuat lamalloc
,calloc
saurealloc
. - Setează pointerul la
NULL
după eliberare: După ce ai eliberat memoria la care indica un pointer, este o bună practică să setezi acel pointer laNULL
. Acest lucru previne problemele cauzate de utilizarea unui pointer care indică spre o zonă de memorie eliberată (dangling pointer).
Exemplu de utilizare corectă a free()
:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *data = NULL; // Initializam pointerul la NULL
int size = 5;
// Alocam memorie
data = (int*) malloc(size * sizeof(int));
if (data != NULL) {
// Folosim memoria
for (int i = 0; i < size; i++) {
data[i] = i * 10;
printf("%d ", data[i]);
}
printf("\n");
// Eliberam memoria
free(data);
data = NULL; // Seteam pointerul la NULL
} else {
printf("Alocarea memoriei a esuat!\n");
}
// Daca incercam sa eliberam din nou (nu se intampla nimic pentru ca data este NULL)
free(data);
return 0;
}
Pierderi de Memorie (Memory Leaks)
O pierdere de memorie apare atunci când memorie alocată dinamic nu este eliberată înainte ca programul să-și termine execuția. Acest lucru poate duce la epuizarea memoriei disponibile pentru program și, în cazuri extreme, poate afecta performanța sistemului. Este crucial să eliberați întotdeauna memoria alocată dinamic atunci când nu mai aveți nevoie de ea.
Exemplu de Pierdere de Memorie:
#include <stdio.h>
#include <stdlib.h>
int main() {
int *data;
int size = 1000;
// Alocam memorie
data = (int*) malloc(size * sizeof(int));
if (data != NULL) {
// Folosim memoria...
// ... dar uitam sa apelam free(data);
}
// Programul se termina, dar memoria alocata cu malloc nu este eliberata explicit
// Sistemul de operare o va elibera la terminarea procesului,
// dar in programe de lunga durata (ex: servere) acest lucru este problematic.
return 0; // Memory leak aici!
}