Cum aloci memoria dinamic in C (si de ce nu poti folosi intotdeauna alocarea statica)
Aparent, programarea e compusa din doua etape:
1) te gandesti ce variabile iti trebuie
2) si apoi te gandesti ce operatii faci cu ele ca sa faci treaba.
Dar daca nu stii de la inceput ce sau cate variabile iti sunt necesare?…
Evident, o idee ar fi sa iti rezervi spatiu (de memorie) acoperitor. Dar asta ar duce la situatia total nepractica in care un programel mititel care in majoritatea timpului foloseste doar un pic de memorie sa isi rezerve o bucata uriasa de memorie pentru acele cazuri rare cand cantitatea aia mare de memorie chiar e necesara.
E ca si cum te-ai duce sa iti faci cumparaturile zilnice cu un camion doar fiindca e posibil ca vreodata sa cumperi mobila sau alte lucruri foarte grele sau voluminoase.
OK — hai sa revenim cu discutia la variabilele din C si sa vedem de ce feluri sunt ele si cum sunt alocate fiecare.
Variabile locale si variabile globale
Am zis “feluri”, si nu “tipuri”, caci nu ma refer la tipul valorilor ce pot fi puse in ele, ci ma refer la modul in care ele pot fi utilizate.
Am mai vorbit de variabilele locale si variabilele globale. Variabilele locale pot fi folosite doar in functia in care au fost definite, in vreme ce variabilele globale pot fi flolosite in tot programul.
Asta inseamna ca variabilele globale trebuie sa aiba alocate pentru ele niste portiuni de memorie inca de la inceputul executie programului. Si vor ramane alocate pana la finalul programului.
Dar memoria peentru variabilele locale ale unei functii poate fi alocata abia atunci cand functia aceea este apelata. Iar dupa incheierea apelului functiei, zona de memorie alocata variabilelor locale ale functiei poate fi eliberata.
Chiar asta se intampla in realitate, doar ca intr-o maniera automata, “invizibila” pentru cel ce programeaza.
Compilatorul de C aloca in mod automat memorie pentru parametrii si variabilele locale ale functiilor folosind mecanismul de lucru cu stiva al procesorului. Nu conteaza acum detaliile, ci important e doar ca nu trebuie sa ne facem griji privind alocarea sii eliverarea memoriei unde vor fi stocate variabilele locale.
Si nici pentru variabilele globale nu trebuie sa ne facem griji. Toate astea merg eficient si transparent prin mecanismul de alocare statica.
Variabile alocate dinamic
Dar cum am putea face sa alocam si sa eliberam zone de memorie atunci cand dorim? Adica sa alocam memoria dinamic, cum se zice.
Hai sa incepem cu un exemplu generic, ca sa vedem cum stau lucrurile:
Sa zicem ca vrem sa avem in program o variabila de tipul T pentru care sa alocam memorie doar daca se indeplineste o anumita conditie in program.
Adica acea variabila poate sa existe sau nu in functie de ce se intampla prin program la diferite momente de timp. De asemenea, pozitia ei in memorie (adica adresa de inceput a ei) nu se stie a inceputul programului si chiar se poate modifica pe parcursul rularii lui.
Inseamna ca in program variabila aceea n-o putem defini, ci putem defini o variabila de tip pointer catre tipul T (T fiind tipul variabilei dorite a fi alocata dinamic):
T *p;
Ca sa alocam memorie pentru aceasta variabila (potentiala) folosim functia malloc (din fisierul antet stdlib.h):
p = (T*) malloc(sizeof(T));
Hai sa “disecam” treaba asta:
- Operatorul sizeof returneaza dimensiunea (ca numar de octeti a) tipului de date caruia i se aplica. Aici T poate fi oricare dintre tipurile de date prezentate la lectia despre variabile in C (tipuri gen: char, int, double, …).
- Functia malloc aloca o zona continua de memorie pentru o variabila cu numarul specificat de octeti.) In cazul asta, acest numar e sizeof(T) — adica malloc va aloca exact atatia octeti in memorie cati sunt necesari pentru o variabila de tipul T.
- Iar (T*) anunta compilatorul ca e OK sa considere adresa returnata de malloc ca fiind un pointer catre o variabila de tip T (caci malloc returneaza void *, adica pointer catre orice tip de data).
Variabila construita astfel (cu malloc, adica) va ramane in memorie pana cand apelam functia free:
free(p);
Cu conditia ca in p sa avem adresa acestei “variabile” alocate dinamic (adresa ce a fost returnata de malloc).
Hai sa vedem un exemplu concret (desi nu foarte practic):
#include <stdio.h>
#include <stdlib.h>
void main()
{
int *p;
p = (int*) malloc(sizeof(int));
if (p != NULL)
{
printf("Cat face 1+1? ");
scanf("%d", p);
switch ( *p )
{
case 2:
printf("Da!\n");
break;
case 10:
printf("TRUE!\n");
break;
case 1:
printf("Logic ca da!\n");
break;
default:
printf("Exact! Fara numar.\n");
}
free(p);
}
else
{
printf("EROARE: Nu s-a putut aloca memorie!\n");
}
}
Atunci cand vreau sa aloc un vector de 13 int-uri, nu scriu in malloc sizeof(int[13]), ci inmultesc sizeof(int) cu 13.
Uite, de exemplu, cum as putea sa calculez media si varianta notelor unui elev:
#include <stdio.h>
#include <stdlib.h>
void main()
{
int n;
unsigned char *note;
int suma;
int i;
float med;
float var;
printf("Cate note?: ");
while (1)
{
scanf("%d", &n);
if (n>0 && n<20)
{
break;
}
printf("Un numar intre 1 si 19, te rog!: ");
}
note = (unsigned char*) malloc(n*sizeof(unsigned char));
if (note == NULL)
{
printf("Eroare! Nu s-a putut aloca o zona de memorie de %ld octeti!\n", n*sizeof(unsigned char));
return;
}
for (i=0; i<n; i++)
{
printf("nota[%d] = ?: ", i);
scanf("%hhd", note+i);
}
suma = 0;
for (i=0; i<n; i++)
suma += note[i];
med = suma / (float)n;
var = 0;
for (i=0; i<n; i++)
var += (note[i]-med) * (note[i]-med);
var /= n;
printf("Media e %.2f si varianta e %.2f.\n", med, var);
free(note);
}
Cateva explicatii:
- Prin acel while (1) am construit o “bucla infinita”, care va repeta citirea unei valori de la tastatura pana cand utilizatorul introduce un numar intre 1 si 19. Iesirea din acel while se face prin instructiunea break din interirul if-ului.
- Daca malloc nu a reusit sa aloce memoria, returneaza valoarea NULL (adica 0). In acest caz, am afisat un mesaj de eroare si am oprit programul prin instructiunea return (care iese din functia main).
- Apoi am citit fiecare nota pe rand (in acel for) cu scanf(“%hhd”, note+i). Specificatorul de format “%hhd” se refera la o valoare de un octet, iar note+i e adresa unde va fi scrisa in memorie acea valoare.
- In urmatorul for adun fiecare nota la variabila suma, dupa care impart suma la numarul de note. Cum suma si n au valori de tip intreg, rezultatul operatiei de impartire va fi intreg (deci fara virgula). Pentru a forta operatorul de impartire sa calculeze impartirea cu virgula, trebuie ca cel putin una dintre valori sa fie de tip real. Pentru asta am pus operatorul de conversie de tip inainte de n. (Daca il puneam inainte de suma, e posibil sa nu fi mers (in cazul in care impartirea s-ar fi efectuat inainte de conversia de tip; deci ar fi trebuit verificata precedenta operatorilor sau puse niste paranteze care sa clarifice ordinea de efectuare a operatiilor).)
- La impartirea lui var la n n-am mai facut conversia de tip, caci var era deja o valoare de tip real.
- Iar la final am eliberat memoria alocata pentru vectorul de note, cu free(note).
Concluzie
Cam dificil — da, stiu… 🙂
Dar mecanismul asta de alocare dinamica (malloc + free) iti da o libertate incredibila de a gestiona in programul tau memoria sistemelor pe care el ruleaza.
Evident, “with great power comes great responsibility” (cum zice in Spiderman, parca), asa ca trebuie sa ai grija ce si cat aloci, sa verifici ca s-a alocat inainte de a scrie ceva acolo si sa nu uiti sa eliberezi memoria alocata.
Poate ca lucrul cel mai fascinant in legatura cu programarea in C e ca te lasa sa intri foarte mult in detaliile intime ale functionarii calculatorului pe care il programezi.
De asta eu recomand limbajul C celor care vor sa ridice niste “piloni” foarte rezistenti pe “temelia” construita in Abecedar.
Altfel (daca vor sa sara direct la vreunul din limbajele foarte populare din prezent), exista riscul sa ajunga doar la o intelegere superficiala a limbajului — ceea ce le va limita serios dezvoltarea ca programatori.
Alatura-te celor peste 8000 de oameni din armata noastra de creiere cu muschi si vei primi testul care iti va spune daca ai sau nu minte de programator.
In plus, vei fi mereu la curent cu tot ce pun la cale.
Cu drag,