Se c’è un argomento che manda in crisi gli studenti al primo esame di programmazione in C, sono i puntatori. Ho visto persone che scrivevano cicli e funzioni senza problemi bloccarsi completamente davanti a un asterisco. E quasi sempre non è colpa loro: è colpa di come vengono spiegati.
Il puntatore viene presentato come un mostro astratto — “una variabile che contiene un indirizzo” — e poi ci si aspetta che lo studente capisca int **p al volo. Non funziona così. In questa guida ti spiego i puntatori e la gestione della memoria come li spiego a lezione: partendo da un modello mentale concreto, poi salendo di complessità un pezzo alla volta.
Prima di tutto: cos’è la memoria
Non puoi capire i puntatori se non hai chiaro cos’è la memoria di un programma. Immaginala come una lunghissima fila di caselle numerate, una accanto all’altra. Ogni casella contiene un byte e ha un numero che la identifica: quel numero è il suo indirizzo.
Quando scrivi int x = 42;, il compilatore fa tre cose:
- Riserva un po’ di caselle consecutive (di solito 4, per un
int). - Ci scrive dentro il valore 42.
- Si segna che quel gruppo di caselle si chiama
x.
Il nome x è comodo per te, ma il computer ragiona con gli indirizzi. E qui entra in gioco il puntatore.
Il puntatore è solo un indirizzo
Un puntatore è una variabile che, invece di contenere un valore “normale” come 42, contiene l’indirizzo di un’altra casella. Tutto qui. Non c’è nessuna magia.
int x = 42;
int *p = &x; // p contiene l'indirizzo di x
Due simboli da tenere ben distinti, perché è qui che nasce metà della confusione:
&xsignifica “l’indirizzo di x” (l’operatore address-of).*psignifica “il valore contenuto all’indirizzo che sta in p” (l’operatore dereference).
Quindi dopo le due righe sopra, *p vale 42. E se scrivi *p = 7;, stai modificando x attraverso il puntatore: ora x vale 7. Non hai toccato x per nome, ma sei arrivato alla sua casella tramite il suo indirizzo.
Il consiglio che do sempre: leggi int *p come “*p è un int”. Cioè: quando dereferenzi p, ottieni un intero. Questo ti aiuta a non confondere la dichiarazione con l’uso.
Perché i puntatori esistono
A questo punto molti studenti pensano: “ok, ma perché complicarsi la vita? Se voglio 42, scrivo 42”. Domanda legittima. I puntatori servono per tre motivi concreti.
Primo: modificare una variabile dentro una funzione. In C i parametri vengono passati per copia. Se scrivi una funzione raddoppia(int n) e dentro fai n = n * 2, il chiamante non vede nessun cambiamento, perché hai modificato una copia. Per modificare davvero la variabile originale devi passarle l’indirizzo:
void raddoppia(int *n) {
*n = *n * 2;
}
// chiamata: raddoppia(&x);
Secondo: gestire array e stringhe. In C il nome di un array è di fatto l’indirizzo del suo primo elemento. Scorrere un array con i puntatori è il modo in cui il linguaggio è pensato per funzionare.
Terzo: allocare memoria quando non sai in anticipo quanta te ne serve. Ed è qui che entra la parte che spaventa di più: malloc e free.
Stack e heap: le due memorie
Il tuo programma usa la memoria in due modi diversi, ed è fondamentale capire la differenza.
Lo stack è dove vivono le variabili locali. Quando entri in una funzione, le sue variabili vengono create lì; quando la funzione finisce, vengono distrutte automaticamente. Veloce, ordinato, ma con due limiti: lo spazio è limitato e la vita di una variabile finisce con la funzione.
L’heap è la memoria che gestisci tu a mano. Ne chiedi un pezzo quando ti serve, con malloc, e sei tu il responsabile di restituirlo, con free. Vive finché non lo liberi, anche oltre la funzione che l’ha creato. È potente ed è esattamente il motivo per cui in C hai il controllo totale — ma anche perché in C ti dai la zappa sui piedi con più facilità che in Python o Java.
int *v = malloc(10 * sizeof(int)); // chiedo spazio per 10 int
if (v == NULL) { /* gestisci l'errore! */ }
v[3] = 99; // uso l'array
free(v); // restituisco la memoria
v = NULL; // buona abitudine
Nota sizeof(int) invece di scrivere 4 a mano: rende il codice portabile e leggibile. E nota il controllo v == NULL: malloc può fallire, e ignorarlo è uno dei primi errori che correggo.
I quattro errori che vedo a ogni sessione
Dopo qualche anno di lezioni, gli errori con la memoria sono quasi sempre gli stessi quattro. Se impari a riconoscerli sei già a metà dell’opera.
-
Memory leak. Chiedi memoria con
malloce ti dimentichi di farefree. Il programma continua a girare ma consuma sempre più RAM. In un esercizio d’esame non crasha, quindi passa inosservato — finché non usi uno strumento come Valgrind che te lo sbatte in faccia. Regola: ognimallocdeve avere il suofree, e mentre scrivi il primo pensa già a dove metterai il secondo. -
Dangling pointer. Fai
free(v)ma continui a usarev. La memoria non è più tua, e quello che leggi o scrivi è comportamento indefinito: a volte funziona, a volte crasha, a volte corrompe altri dati in modo silenzioso. Per questo dopofreeconviene metterev = NULL: almeno un uso successivo fallisce in modo evidente invece che subdolo. -
Puntatore non inizializzato. Dichiari
int *p;e poi fai*p = 5;senza aver mai fatto puntarepa qualcosa. Stai scrivendo in una casella a caso della memoria. Inizializza sempre, anche solo aNULL. -
Uso di variabile locale dopo la fine della funzione. Ritorni l’indirizzo di una variabile locale (
return &x;dentro una funzione): quella casella nello stack non esiste più appena la funzione termina. Se ti serve qualcosa che sopravviva alla funzione, va sull’heap.
Il filo conduttore di tutti e quattro: in C nessuno ti protegge. Il compilatore spesso non si lamenta, il programma parte lo stesso, e il bug si manifesta ore dopo in un punto lontano. Imparare a ragionare sulla proprietà della memoria — chi la possiede, chi la libera, per quanto vive — è la vera competenza da acquisire.
Come si studiano davvero i puntatori
La teoria dei puntatori la leggi in mezz’ora. Il problema è che leggere non basta: devi vederli muoversi. Ecco come lavoro con i miei studenti di informatica, e come ti consiglio di studiare da solo.
Disegna la memoria su carta. Per ogni riga di codice, disegna le caselle con i loro indirizzi e le frecce dei puntatori. Sembra roba da bambini, ma è esattamente ciò che distingue chi “capisce” i puntatori da chi li subisce. Quando vedi *p = *q, sul foglio segui la freccia di q, prendi il valore, e lo scrivi dove punta p. Se lo sai fare su carta, lo sai fare in testa.
Usa un debugger passo-passo. Con gdb (o il debugger integrato di VS Code) esegui il programma una riga alla volta e guarda come cambiano gli indirizzi e i valori. Vedere p che contiene 0x7ffe... e *p che cambia è più istruttivo di dieci pagine di libro.
Parti sempre da un livello di indirezione. Prima domini int *p. Solo dopo passi a int **p (puntatore a puntatore), che serve per le matrici dinamiche e per modificare un puntatore dentro una funzione. Saltare questo passaggio è la ricetta perfetta per confondersi.
Fai girare Valgrind sui tuoi esercizi. È lo strumento che ti dice esattamente dove hai un leak o un accesso illegale. Prendi l’abitudine di lanciarlo su ogni programma con malloc: all’esame di solito non serve, ma ti forma un istinto che vale oro.
Tempistiche realistiche
Quanto ci vuole per essere solido sui puntatori? Se parti dalle basi del C già acquisite (variabili, cicli, funzioni), con lavoro mirato bastano due-tre settimane: una per il modello mentale e i puntatori semplici, una per stack/heap e allocazione dinamica, una per array dinamici, stringhe e puntatori doppi.
Non è un argomento che si “capisce all’improvviso”: è un muscolo che si allena facendo esercizi e sbagliando. Chi mi dice “ho capito la teoria ma sbaglio gli esercizi” quasi sempre non ha ancora disegnato abbastanza memoria su carta.
Come lavoriamo nelle lezioni
Nelle mie lezioni sui puntatori parto sempre dal tuo codice, non da esempi astratti da manuale. Se hai un esercizio d’esame o un progetto che segmenta (il famigerato segmentation fault), lo apriamo insieme, lo mandiamo in gdb e lo seguiamo riga per riga finché non vediamo dove e perché il puntatore va dove non dovrebbe. È il modo più rapido per far scattare il click.
Sono un ingegnere del Politecnico di Milano e seguo studenti universitari su C, algoritmi e strutture dati da anni: gli esami di programmazione e fondamenti sono tra quelli che seguo più spesso. La media dei miei studenti è 8.2/10, con oltre 215 studenti seguiti e il 97% di test superati — numeri che vengono proprio dal lavorare sui punti in cui uno si blocca, invece che rifare la teoria che già sa.
Il costo è di 40 € per la lezione singola, oppure 360 € per il pacchetto da 10 ore (36 €/h): per un argomento come i puntatori, un paio di lezioni mirate spesso bastano a sbloccare tutto il resto del corso.
Se ti stai preparando anche a un esame di ammissione con parte di logica o informatica, guarda la pagina dedicata ai test di ammissione. E se vuoi capire su cosa sei bloccato senza impegno, prenota una diagnosi gratuita: mi mandi l’esercizio che ti fa impazzire e ne parliamo. Spesso in venti minuti si capisce se è un problema di puntatori o, come capita più spesso di quanto pensi, di non aver ancora messo a fuoco cos’è davvero la memoria.