Puntatori e memoria dinamica in C
Indirizzi, puntatori e dereferenziazione, passaggio per riferimento, aritmetica dei puntatori e array, malloc e free, errori classici, memoria nei microcontrollori, puntatori a funzione.
I puntatori sono la caratteristica più potente del C e quella che spaventa di più chi lo impara. In realtà l’idea è semplice: un puntatore è una variabile che contiene un indirizzo di memoria. Con i puntatori una funzione può modificare le variabili del chiamante, si scorrono gli array, si alloca memoria durante l’esecuzione e si accede direttamente ai registri dell’hardware. In questa lezione li vediamo passo per passo, insieme alla memoria dinamica e agli errori da evitare.
Indirizzi e puntatori
Ogni variabile occupa una o più celle di memoria, e l’operatore & ne dà l’indirizzo. Un puntatore si dichiara con l’asterisco e contiene un indirizzo; l’operatore * applicato al puntatore dà accesso alla variabile puntata (dereferenziazione).
int x = 42;
int *p = &x; /* p contiene l'indirizzo di x: p "punta a" x */
printf("%d\n", *p); /* 42: il valore della variabile puntata */
*p = 7; /* modifica x attraverso il puntatore */
printf("%d\n", x); /* 7 */
Il tipo del puntatore (int *, double *, char *) dice che tipo di dato c’è all’indirizzo, e quindi quanti byte leggere. Il puntatore nullo NULL non punta a niente: è il valore da usare per dire “nessun oggetto”, e dereferenziarlo fa terminare il programma.
Passaggio per riferimento
Per permettere a una funzione di modificare una variabile del chiamante, le si passa il suo indirizzo:
void scambia(int *a, int *b)
{
int t = *a;
*a = *b;
*b = t;
}
int main(void)
{
int x = 3, y = 8;
scambia(&x, &y); /* ora x vale 8 e y vale 3 */
return 0;
}
Così una funzione può anche restituire più risultati, per esempio modulo e fase di un numero complesso, ed è il motivo per cui scanf vuole &x: deve scrivere nella variabile del chiamante.
Puntatori e array
Il nome di un array, in quasi tutte le espressioni, si converte nell’indirizzo del suo primo elemento. E sui puntatori si può fare aritmetica: p + 1 non è l’indirizzo successivo in byte, ma quello dell’elemento successivo.

Valgono le equivalenze a[i] == *(a + i) e &a[i] == a + i. Si può scorrere un array con un puntatore:
double media(const double *v, int n)
{
double s = 0.0;
for (const double *p = v; p < v + n; p++) {
s += *p;
}
return s / n;
}
La memoria dinamica
Gli array hanno una dimensione fissa, decisa quando si scrive il programma. Se la dimensione si conosce solo durante l’esecuzione (quanti campioni leggere da un file, quanti nodi ha un circuito) si chiede memoria al sistema con malloc, nella zona chiamata heap, e la si restituisce con free quando non serve più.
#include <stdlib.h>
int n = leggi_numero_campioni();
double *campioni = malloc(n * sizeof *campioni);
if (campioni == NULL) { /* malloc può fallire: va sempre controllato */
fprintf(stderr, "Memoria insufficiente\n");
return 1;
}
/* ... si usa campioni[0] ... campioni[n - 1] come un array ... */
free(campioni);
campioni = NULL; /* evita di riusarlo per errore */
Gli errori classici
- Puntatore non inizializzato:
int *p; *p = 5;scrive in un indirizzo casuale. - Dereferenziare NULL: per esempio non controllare il risultato di
malloc. - Memory leak: dimenticare
free. In un programma che gira per mesi, come il firmware di un router, la memoria si esaurisce un po’ alla volta. - Use after free: usare la memoria dopo averla liberata. Il suo contenuto può essere stato riassegnato ad altro.
- Doppio free: liberare due volte la stessa zona corrompe le strutture interne dell’allocatore.
- Puntatore a una variabile locale restituito da una funzione: quando la funzione termina, la variabile non esiste più.
Questi errori sono responsabili di una grande parte delle vulnerabilità del software scritto in C e C++: è per questo che linguaggi più recenti come Rust controllano l’uso della memoria già durante la compilazione. Strumenti come valgrind e AddressSanitizer li trovano durante i test.
Nei microcontrollori l’allocazione dinamica si evita quasi sempre: con pochi kilobyte di RAM, allocazioni e liberazioni ripetute frammentano la memoria fino a far fallire malloc dopo ore o giorni di funzionamento. Le linee guida per il software critico, come MISRA C usata nell’automotive, la vietano dopo l’inizializzazione: si usano array statici dimensionati per il caso peggiore.
Puntatori a funzione
Un puntatore può contenere anche l’indirizzo di una funzione, che poi si chiama attraverso di esso. Si usa per scegliere un comportamento durante l’esecuzione: la funzione qsort della libreria standard riceve il puntatore alla funzione che confronta due elementi, e le tabelle dei vettori di interrupt dei microcontrollori sono array di puntatori alle funzioni da eseguire quando arriva un’interruzione.




