Ogni variabile di un programma C vive da qualche parte, e il dove determina due cose che poi non puoi più cambiare: quanto sopravvive e quanta ne puoi avere. Il C ti offre tre regioni di memoria, e scegliere quella sbagliata produce un crash o un leak. Questa pagina le presenta e mostra il classico bug che nasce dallo sbagliare la durata.
Le tre regioni
indirizzi alti
+---------------------------+
| stack | locali, parametri, indirizzi di ritorno
| cresce verso il basso | liberato in automatico al ritorno
| v |
+---------------------------+
| (spazio inutilizzato)|
+---------------------------+
| ^ |
| cresce verso l'alto |
| heap | blocchi di malloc / calloc / realloc
+---------------------------+ liberati solo da free()
| dati statici / globali | globali e static, per tutta l'esecuzione
+---------------------------+
| codice (text) | il codice macchina, di sola lettura
+---------------------------+
indirizzi bassi
- La memoria automatica (lo stack) contiene i parametri delle funzioni e le locali non static. Un blocco di stack viene riservato quando si entra in una funzione e rilasciato quando questa restituisce. La dimensione è fissata in compilazione.
- La memoria dinamica (l'heap) contiene tutto ciò che arriva da
malloc,callocerealloc. La dimensione si decide a runtime; la durata finisce solo confree. - La memoria statica contiene le globali e tutto ciò che è dichiarato
static. Esiste per tutta l'esecuzione del programma e viene inizializzata a zero prima che partamain.
Gli indirizzi del diagramma sono la disposizione abituale, non una garanzia: lo standard descrive le durate, non la disposizione.
La memoria automatica in azione
Ogni chiamata a demo riceve un local nuovo e un table nuovo. Non si libera nulla a mano, non può esserci nessun leak e l'allocazione costa una sola istruzione che sposta lo stack pointer. Ecco perché le normali variabili locali dovrebbero essere la tua scelta predefinita: sono la memoria più veloce e più sicura che il C abbia.
Il problema è la graffa di chiusura. Una volta superata, quella memoria non c'è più.
Il puntatore pendente
Ecco il bug che ogni programmatore C scrive almeno una volta:
/* SBAGLIATO: restituisce l'indirizzo di memoria che non esiste piu */
int *make_counter(void) {
int count = 0;
return &count; /* count muore a questa graffa */
}
int main(void) {
int *p = make_counter();
*p = 5; /* scrittura in uno stack frame morto */
return 0;
}
&count era un indirizzo perfettamente valido mentre make_counter era in esecuzione. Al ritorno, quello spazio di stack passa alla prossima funzione chiamata, quindi ora p punta dentro le variabili locali di qualcun altro. Leggere dà spazzatura; scrivere le corrompe. GCC e Clang avvisano proprio per questa forma (-Wreturn-local-addr), quindi compila con gli avvisi attivi.
Con gli array lo stesso bug si traveste, e lì l'avviso spesso non scatta:
La versione sbagliata di quella funzione costruirebbe il testo in un char buf[64] locale e farebbe return buf;, restituendo l'indirizzo di un buffer che smette di esistere nello stesso istante.
Tre modi per correggerlo
1. Il chiamante fornisce il buffer (come sopra). Nessuna allocazione, nessuna questione di proprietà, ed è lo stile più comune nelle librerie C. La funzione riceve la dimensione così da restarci dentro.
2. Restituisci memoria dell'heap, e di' chi la libera.
Il blocco dell'heap sopravvive alla funzione per progetto: è proprio lo scopo della memoria dinamica. Il prezzo è il commento sulla proprietà e il free del chiamante.
3. Usa la memoria statica, quando un unico buffer condiviso è accettabile:
static dentro una funzione mantiene locale lo scope della variabile ma le dà la durata del programma, quindi restituirne l'indirizzo è lecito. Il compromesso è che ne esiste sempre una sola: tutti i chiamanti la condividono, il che rende questo schema inutilizzabile nel codice multithread e sorprendente anche in quello a thread singolo quando due chiamanti tengono il puntatore nello stesso momento.
Dimensione: dove finisce lo stack
Lo spazio di stack è piccolo e fisso. Il thread principale di solito riceve 1 MB su Windows e 8 MB su Linux; un thread creato in seguito spesso riceve molto meno. L'heap è limitato dalla memoria disponibile nel sistema.
void bad(void) {
int huge[1000000]; /* ~4 MB di stack: probabile crash appena si entra */
huge[0] = 1;
}
Non c'è alcuna diagnostica e nessun NULL da controllare: il programma muore e basta, di solito con un segmentation fault, prima che venga eseguita la prima riga del corpo. La versione con l'heap segnala l'errore come si deve:
Una ricorsione profonda esaurisce lo stack allo stesso modo, un frame alla volta: in pratica, una funzione ricorsiva senza freni è la causa più comune di stack overflow.
Costo e località
Allocare sullo stack è una singola operazione aritmetica su un registro. Allocare sull'heap è una chiamata di libreria che cerca un blocco adatto, può prendere un lock e ogni tanto chiede altra memoria al sistema operativo. In un ciclo molto usato la differenza si misura.
I dati sullo stack sono anche compatti e usati di recente, quindi tendono a stare in cache. I blocchi dell'heap possono essere sparsi. Nessuno dei due fatti dovrebbe guidare da solo un progetto, perché prima viene la correttezza della durata, ma tra due progetti entrambi corretti quello basato sullo stack di solito è il più veloce.
Vedere le regioni
Stampare gli indirizzi rende concreta la disposizione. I valori esatti cambiano a ogni esecuzione (i sistemi moderni li rendono casuali), ma i raggruppamenti si vedono:
La globale e la statica stanno una accanto all'altra; il blocco dell'heap è altrove; la locale di solito è lontana da entrambe. Fai il cast a void * per %p: è quello che richiede lo specificatore di formato.
Come scegliere
Usa lo stack quando:
- la dimensione è nota in compilazione,
- i dati servono solo dentro questa funzione e quelle che chiama,
- e sono piccoli: qualche kilobyte, non megabyte.
Usa l'heap quando:
- la dimensione dipende dall'input, da un file o da un calcolo,
- i dati devono sopravvivere alla funzione che li ha creati,
- oppure sono abbastanza grandi da mettere a rischio il limite dello stack.
Usa la memoria statica quando:
- deve esistere esattamente un'istanza per tutto il programma,
- e condividerla tra tutti i chiamanti è davvero corretto.
La scelta predefinita è lo stack. Passa all'heap quando vale uno dei suoi tre motivi e, quando lo fai, segui le regole di proprietà descritte nella pagina sui memory leak, così che il blocco a cui hai dato una durata venga comunque rilasciato.
Due bug speculari
Vale la pena nominarli insieme, perché sono la stessa domanda sulla durata con due risposte diverse:
- Puntatore pendente: la memoria è morta prima del puntatore. Restituire
&localo usare un puntatore dopofree. Il programma legge o scrive memoria che ora appartiene a qualcos'altro. - Memory leak: il puntatore è morto prima della memoria. Perdere l'ultimo riferimento a un blocco di
malloc. Niente si rompe subito; il processo semplicemente cresce.
Entrambi nascono da una discrepanza tra quanto a lungo devono vivere i dati e la regione in cui li hai messi. Decidi prima la durata, e la regione viene di conseguenza.
Domande frequenti
Qual è la differenza tra stack e heap in C?
Lo stack contiene le variabili locali: il compilatore ne stabilisce la dimensione, vengono create quando si entra in una funzione e distrutte quando questa restituisce, e allocarle non costa nulla. L'heap contiene i blocchi di malloc: scegli la dimensione a runtime, il blocco sopravvive finché non chiami free e l'allocazione ha un costo reale.
Perché non posso restituire un puntatore a una variabile locale in C?
Perché la memoria della locale viene rilasciata nel momento in cui la funzione restituisce. Il puntatore contiene ancora quell'indirizzo, ma ora la memoria appartiene alla chiamata di funzione successiva: leggerla dà spazzatura, scriverci corrompe dati che non c'entrano. Quello è un puntatore pendente. Restituisci un blocco di malloc, oppure fai in modo che sia il chiamante a fornire il buffer.
Quanto è grande lo stack in C?
Di solito da 1 a 8 MB per il thread principale, e molto meno per i thread aggiuntivi: abbastanza piccolo che int big[1000000]; come locale di solito manda in crash il programma appena si entra nella funzione. L'heap è limitato dalla memoria disponibile nel sistema, quindi i dati grandi o di dimensione sconosciuta vanno lì.
Quando conviene usare l'heap invece dello stack in C?
In tre casi: la dimensione non è nota fino a runtime, i dati devono sopravvivere alla funzione che li ha creati, oppure il blocco è troppo grande per lo stack (più o meno qualsiasi cosa oltre qualche centinaio di kilobyte). Tutto il resto dovrebbe essere una semplice variabile locale: è più veloce e non può causare leak.