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>:

  1. malloc() (Memory Allocation)
  2. calloc() (Contiguous Allocation)
  3. realloc() (Reallocation)
  4. 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 unui int poate varia pe sisteme diferite).
    • Am făcut un cast explicit la (int*) deoarece malloc returnează void *. Deși în C casting-ul de la void * 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).

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 folosind malloc(n * sizeof(int)).
    • Am folosit pointerul tablou ca și cum ar fi un tablou, accesând elementele cu tablou[i]. Aceasta este posibil deoarece memoria alocată de malloc este contiguă.
    • Am eliberat memoria la sfârșit.

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ă cu malloc(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.

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 fi NULL, caz în care realloc() se comportă ca malloc().
  • size: Noua dimensiune (în octeți) a blocului. Dacă este zero, realloc() se comportă ca free().

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 la n_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 lui realloc la tablou, 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.

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 este NULL, apelul la free() nu are niciun efect.

Reguli Importante pentru free():

  • Eliberează doar memorie alocată dinamic: Nu apela free() pe pointeri care nu indică spre memorie alocată cu malloc, calloc, sau realloc (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și free(NULL) este sigur, este o bună practică să verifici dacă un pointer este NULL înainte de a încerca să-l eliberezi, mai ales după un apel eșuat la malloc, calloc sau realloc.
  • Setează pointerul la NULL după eliberare: După ce ai eliberat memoria la care indica un pointer, este o bună practică să setezi acel pointer la NULL. 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!
}

Trebuie să fii autentificat pentru a accesa editorul de cod și pentru a experimenta codul prezentat în acest tutorial.

Intră în cont