Per la maggior parte dei neofiti programmatori, la gestione avanzata della memoria e' un concetto ostico da digerire specialmente perche' si utilizzano termini non sempre molto chiari. Lo scopo di questo post e' cercare di rendere piu' assimilabile la gestione della memoria nella programmazione C++.
Cominciamo a vedere cosa succede quando viene eseguito il nostro programma di test. Quando l'eseguibile viene caricato in memoria, questa viene suddivisa in tre segmenti: il
code segment, lo
stack segment e l'
heap segment.
Per meglio capire l'organizzazione della memoria consideriamo il seguente codice sorgente. Si tratta di un codice completamente inutile, ma ci permette di capire come funzionano le cose.
int funzione(int par1, double par2, bool par3) {
// definisci alcune variabili
int temp1, temp2, temp3;
float temp4, temp5, temp5;
// fai qualcosa
// ...
// ritorna
return 0;
}
int main(int argc, char ** argv) {
// definisci alcune variabili
bool isRunning = true;
double myFloat = sqrt(2);
// chiama funzione
int mioInt = funzione( argc, myFloat, isRunning );
// ritorna
return 0;
}
Il
code segment e' la memoria utilizzata per contenere la versione in linguaggio macchina del codice stesso, ovvero in parole molto povere, e' il posto dove la sequenza di operazioni da effettuare viene immagazzinata durante tutta l'esecuzione del programma.
Lo
stack segment e' una regione di memoria organizzata come "Last In First Out". Il programma inizia con l'esecuzione del main e tutte le variabili ivi dichiarate vengono allocate nel stack e vi restano fino a quando escono di scope, ovvero fino a quando il main esce e il programma si ferma. Quando funzione viene chiamata, i suoi parametri vengono allocati nello stack contestualmente a tutte le variabili dichiarate all'interno. Anche in questo caso, la memoria occupata da temp1 ... temp5 verra' liberata non appena termina l'esecuzione di funzione. Questo dovrebbe spiegare perche' lo stack e' una memoria LIFO. E' proprio questo riciclo della memoria che rende pericoloso l'uso di variabili non inizializzate; infatti quando una variabile viene deallocata, la memoria fisica corrispondente viene riassegnata la prossima volta che viene richiesta senza pulirne il contenuto.
Concludendo possiamo dire che finiscono nello stack tutte le variabili allocate in modo "statico" ovvero senza utilizzare gli operatori new e new [].
Veniamo ora all'
heap segment. Questo e' una regione piu' stabile della memoria e puo' essere utilizzata come
shared memory tra differenti funzioni. Per creare una variabile nell'heap dobbiamo usare gli operatori new o new [] e, una volta creata, vi ci resta per fino a quando o viene distrutta dall'operatore delete o delete [] o al termine del programma.
Vediamo alcuni esempi pratici.
Esempio 1:
// .....
{ int inutile; }
In questo spezzone di codice, la variabile
inutile viene creata all'interno di uno scope praticamente vuoto. In questo caso la regione di memoria corrispondente ad inutile viene creata nello stack e subito dopo de-allocata senza che questa possa mai venir utilizzata.
Esempio 2:
{
// ....
MyClass * miaClasse = new MyClass;
// attenzione al memory leak
}
Questa semplice linea di codice agisce sia sullo stack sia sull'heap. Infatti lo statement a sinistra dell'uguale definisce nello stack una variabile
miaClasse di tipo puntatore a MyClass. Per quanto detto precedentemente, la variabile
miaClasse essendo definita nello stack restera' valida fino all'uscita dallo scope corrente, ovvero nel nostro esempio, fino alla parentesi }. Da li' in avanti ci sara' impossile usare la variabile
miaClasse.
Lo statement a destra dell'uguale richiama l'operatore new, ovvero viene allocata nell'heap una regione di memoria grande a sufficienza per contenere un oggetto di tipo MyClass. Essendo nell'heap, questa regione di memoria non andra' mai fuori scope fino al termine del programma e non potra' essere riciclata se non prima opportunamente liberata con l'uso dell'operatore delete.
Attenzione che nel momento che viene chiusa la parentesi graffa, miaClasse non e' piu' valida e noi non sapremo piu' rintracciare la regione di memoria allocata dall'operatore new. Questo fenomeno e' noto come
memory leak ed e' una delle maggiori cause di perdita di prestazioni di un programma mal scritto. Infatti, non solo non saremo piu' in grado di utilizzare i valori contenuti nella regione di memoria puntata da
miaClasse, ma non saremo nemmeno in grado di liberarla al termine dell'utilizzo.
Come possiamo evitare di sprecare memoria? Una risposta semplice non esiste, se volete, una risposta banale potrebbe essere: avere ben chiaro in mente chi e' il responsabile della memoria. Questo argomento va sicuramente approfondito, ma non adesso e non qui...
Rimanete in linea se volete saperne di piu'!