Il segmento di memoria che il Sistema Operativo riserva nella RAM ad un processo (un programma in esecuzione), in generale è suddiviso in quattro distinte aree di memoria, così come mostrato nella figura seguente e che sono:
- l’area del programma, che contiene le istruzioni macchina del programma;
- l’area globale, che contiene le costanti e le variabili globali;
- lo stack, che contiene la pila dei record di attivazione creati durante ciascuna chiamata delle funzioni;
- l’heap, che contiene le variabili allocate dinamicamente.
Che cosa significa tutto ciò lo vediamo con il codice del semplice esempio seguente.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> using namespace std; //COSTANTI E VARIABILI GLOBALI int a; //DEFINIZIONE FUNZIONI int raddoppia(int x) { int b; b=2*x; return b; } int main() { int y; cout<<"Inserisci un intero: "; cin>>y; y=raddoppia(y); cout<<"Complimenti hai raddoppiato: "<<y<<endl; } |
In questo esempio si ha che:
La variabile intera a, che ha un tempo di vita equivalente alla durata dell’intero processo, viene allocata nell’area globale.
Lo stack è utilizzato per memorizzare innanzitutto il record di attivazione della funzione main costituito dalla variabile y, sul quale viene impilato il record di attivazione della funzione raddoppia() in corrispondenza della chiamata che avviene nella riga 16. Questo record di attivazione è costituito da:
- L’indirizzo di rientro nella funzione main, ossia l’indirizzo che nell’area del programma individua l’istruzione successiva a quella che ha invocato la funzione raddoppia().
- Il parametro passato alla funzione raddoppia().
- La variabile locale b della funzione raddoppia().
In particolare, nell’esempio questo secondo record di attivazione viene eliminato dallo stack quando la funzione raddoppia() termina e l’esecuzione del programma riprende nel sottoprogramma chiamante, il main, nella riga 17. In questo modo, quando si rientra nel main la variabile del parametro passato alla funzione e la variabile locale b vengono distrutte.
Si fa osservare che l’uso dello stack consente ad una funzione di invocare un’altra funzione e, come abbiamo visto nell’esempio di prima, ciò determina la creazione di un nuovo record di attivazione in cima alla pila dei record di attivazione dello stack. Più in generale, il meccanismo della pila dei record di attivazione permette il corretto flusso di esecuzione del programma e di gestione dei tempi di vita delle variabili locali, quando all’interno di un programma si hanno le chiamate alle funzioni. Questo meccanismo permette anche che una funzione possa invocare al suo interno se stessa (meccanismo detto di ricorsione).
Differenze fra allocazione statica e dinamica di una variabile
Si mette in evidenza che le variabili globali e le variabili locali (quelle cioè comunemente dichiarate all’interno di una funzione) in C/C++ devono essere sempre allocate staticamente, ossia la loro dimensione in byte deve essere conosciuta a priori dal compilatore. Questo assicura che sebbene, in generale, non è noto a priori quanti record di attivazione saranno allocati nello stack durante l’esecuzione del programma, comunque il compilatore sarà in grado di calcolare la dimensione di un record di attivazione dall’analisi del codice sorgente, rendendo possibile una gestione corretta dello stack in quanto deterministica.
Caratteristiche/Vantaggi
L’area di memoria dinamica, denominata heap, consente di allocare spazi di memoria la cui dimensione è nota solo a runtime (in fase di esecuzione del programma) e per questo, si dice, dinamicamente. Una variabile viene allocata nell’heap utilizzando opportune istruzioni messe a disposizione dal linguaggio di programmazione. Una variabile allocata dinamicamente ha la caratteristica di avere un tempo di vita che non è legato a quello delle funzioni. In particolare, non è legato al tempo di esecuzione della funzione in cui essa viene creata, nel senso che sopravvive anche dopo che la la funzione termina. Ciò consente di creare una variabile dinamica in una funzione, di utilizzarla in altre funzioni e di distruggerla (deallocarla esplicitamente con le istruzioni opportune) in una funzione ancora diversa.
Un errore tipico
Un caso tipico di allocazione dinamica della memoria è quello in cui si ha l’esigenza di dimensionare un array “al volo”, dopo aver scoperto quanto esso deve essere grande. Ossia, è questo il caso in cui si ha bisogno di creare un array con un numero di elementi che non è noto a priori e per il quale cioè è impossibile determinare la dimensione in fase di compilazione dall’analisi del codice, in quanto dipendente, per esempio, da un valore in input. Il questo caso l’errore tipico che può essere commesso dal programmatore è il seguente:
1 2 3 4 5 6 7 8 |
int main() { int n; cout << "Quanti sono gli elementi del vettore? << endl; cin >> n; //ATTENZIONE QUESTO MODO DI OPERARE E' SBAGLIATO!!! int v[n]; //Allocazione statica errata!!! .... } |
Nella riga 6 si commette il grave errore di dichiarare un vettore ricorrendo ad un’allocazione statica senza però specificare al compilatore un valore costante e noto a priori della dimensione, bensì un valore variabile che sarà noto solo a runtime. In questo caso il vettore deve essere dichiarato in maniera dinamica utilizzando le opportune istruzioni. La soluzione a questo problema viene fornita nell’articolo raggiungibile a questo link.