Stack e Heap in C++
Due aree principali della memoria
In C++ la memoria dei nostri programmi viene principalmente gestita in due zone:
| Zona | Chi la gestisce | Quando viene allocata | Quando viene liberata |
|---|---|---|---|
| Stack | il compilatore | quando viene dichiarata una variabile nello scope | automaticamente quando esce dallo scope |
| Heap | il programmatore (manuale) | quando viene usata la keyword new | manualmente tramite keyword delete |
Stack
Lo stack è l’area in cui vivono le variabili normali che dichiariamo dentro funzioni o blocchi { }.
int main() {
int x = 10; // x vive nello stack
} // qui x viene distrutto automaticamente
Le variabili sullo stack non richiedono nessuna azione manuale: vengono gestite dal C++ automaticamente.
- è veloce
- è gestito automaticamente
- quando lo scope finisce → la variabile muore
Lo stack è perfetto per variabili locali e oggetti che “vivono” solamente dentro una funzione. Per questo in C++ è comune creare oggetti semplicemente scrivendo il loro nome (senza new): questa scelta è considerata sicura e non comporta rischi di dimenticare di liberare memoria. Quando si esce dalla funzione o dal blocco, tutto quello che si trova sullo stack viene liberato automaticamente.
Heap
Nell’heap la memoria viene richiesta e gestita in modo dinamico.
Questa zona è utile quando servono dati che non devono sparire appena termina una funzione, ma devono restare disponibili più a lungo.
Per allocare memoria nell’heap, in C++ si utilizza l’operatore new.
Questo operatore crea dinamicamente un’area di memoria e restituisce un puntatore al tipo richiesto.
int* p = new int; // alloca un intero nell'heap
*p = 5; // assegna un valore
cout << *p << endl; // stampa 5
delete p; // libera la memoria
Ricorda: ogni
newdeve avere un corrispondentedelete, altrimenti si genera un memory leak.
Se però la memoria non viene restituita correttamente al sistema, rimane occupata e inutilizzabile fino a fine programma. Questo problema si chiama memory leak.
Nei programmi molto semplici, o su computer moderni con tanta RAM, questi sprechi possono non apparire immediatamente. Ma in sistemi con poca memoria (come sistemi embedded, microcontrollori, o piccoli dispositivi) ogni spreco pesa e può portare rallentamenti o malfunzionamenti.
Più avanti verranno introdotti strumenti per gestire questa memoria in modo corretto e sicuro.
Attenzione importante: dangling pointer
Se dentro una funzione dichiari una variabile locale sullo stack e provi a ritornare un puntatore verso quella variabile, quel puntatore diventa immediatamente pericoloso. Quando la funzione termina, lo stack frame viene distrutto e quella variabile non esiste più.
int* crea() {
int x = 10;
return &x; // ERRORE: x era sullo stack, fuori da questa funzione non esiste più
}
Il puntatore che ritorna non punta più ad una memoria valida → questo si chiama dangling pointer e porta a undefined behavior.
Concetto chiave
Non tutta la memoria vive allo stesso modo: alcune variabili vivono automaticamente e muoiono da sole (stack), altre devi gestirle tu con attenzione (heap).
- stack → automatico
- heap → manuale
Esempio pratico di funzioni: stack vs heap
#include <iostream>
using namespace std;
// funzione che ritorna un puntatore a variabile sullo stack
int* creaStack() {
int x = 42; // vive nello stack
return &x;
}
// funzione che ritorna un puntatore a variabile nell'heap
int* creaHeap() {
int* p = new int(99); // vive nell'heap
return p; // ok, rimane valida finché non facciamo delete
}
int main() {
int* pStack = creaStack();
int* pHeap = creaHeap();
cout << "Valore stack: " << *pStack << endl; // comportamento indefinito
cout << "Valore heap: " << *pHeap << endl; // stampa correttamente 99
}
Nell’esempio sopra:
creaStack()ritorna un puntatore a una variabile sullo stack → questo porta a dangling pointer.creaHeap()ritorna un puntatore a una variabile nell’heap → funziona correttamente
Questo mostra chiaramente la differenza di lifetime: le variabili sullo stack vivono solo durante la funzione, mentre quelle nell’heap rimangono finché non le liberiamo manualmente.
Soluzione preferibile: tornare un valore invece di un puntatore
#include <iostream>
using namespace std;
// funzione che ritorna un valore direttamente
int creaValore() {
int x = 42; // vive nello stack, ma viene copiato nel return
return x; // sicuro, non c'è dangling pointer
}
int main() {
int valore = creaValore();
cout << "Valore restituito: " << valore << endl; // stampa correttamente 42
}
In questo caso:
- La variabile
xvive nello stack, ma viene copiata nel momento del return. - Non ci sono puntatori coinvolti → niente dangling pointer.
- Questa è la soluzione più sicura e consigliata in C++ quando possibile, perché sfrutta lo stack in modo automatico e sicuro.