Menu

Puntatori NULL in C: cosa significa NULL e come controllarlo

NULL è l'indirizzo che contiene un puntatore quando non punta a nulla. Questa pagina spiega cos'è davvero NULL, perché dereferenziarlo causa un crash, in cosa differisce da 0 e da un puntatore non inizializzato, e gli schemi difensivi che tengono i bug da null fuori dal tuo codice.

Questa pagina include editor eseguibili: modifica, esegui e vedi subito l'output.

Un puntatore contiene sempre qualche valore. Quando non ha ancora un oggetto a cui puntare (prima di essere assegnato, dopo che l'oggetto a cui puntava è stato liberato, o quando una funzione non ha nulla da restituire), gli serve un valore che significhi senza ambiguità "qui non c'è niente". Quel valore è il puntatore nullo, e NULL è il nome che scrivi.

La disciplina che ci ruota intorno è piccola e si ripaga di continuo: imposta i puntatori a NULL quando non hanno un obiettivo, controlla NULL prima di dereferenziare e restituisci NULL quando non hai un risultato.

Cos'è davvero NULL

NULL è una macro, definita in <stddef.h> e inclusa anche da <stdio.h>, <stdlib.h>, <string.h> e altri. Si espande in una costante di puntatore nullo, tipicamente ((void*)0) o semplicemente 0.

Lo standard C garantisce la proprietà che conta: un puntatore nullo risulta diverso da un puntatore a qualsiasi oggetto o funzione reale. Due puntatori nulli dello stesso tipo risultano sempre uguali tra loro.

La maggior parte dei sistemi stampa (nil) o 0x0 per il puntatore nullo, cioè l'indirizzo zero. È un'implementazione comune, non una regola; lo standard non promette mai che i bit siano zero. Promette il confronto, quindi scrivi p == NULL, mai qualcosa che dipenda dalla rappresentazione.

Dereferenziare NULL causa un crash

Il motivo di ogni controllo su null che esista:

int *p = NULL;
printf("%d\n", *p);   // comportamento indefinito: quasi certamente un crash

Su Windows, macOS e Linux la prima pagina dello spazio degli indirizzi viene lasciata volutamente non mappata, quindi la CPU intercetta l'accesso e il sistema operativo termina il programma. Vedrai:

Segmentation fault (core dumped)          # Linux
zsh: segmentation fault  ./program        # macOS

Quel crash immediato è una caratteristica. Trasforma un bug logico in un fallimento rumoroso proprio sulla riga che l'ha causato, invece che in una corruzione silenziosa che scopri tre settimane dopo. Vedi la pagina sul segmentation fault per capire come leggere il crash e trovare la riga.

Ecco perché in C il "puntatore nullo" non è un'eccezione da catturare. Non esiste un meccanismo per riprendersi; esiste solo il controllo preventivo.

Controlla ciò che può fallire

Le funzioni che possono fallire lo segnalano restituendo NULL. Controllare non è una cortesia facoltativa: l'alternativa è il crash.

La libreria standard ne è piena: malloc, calloc, realloc, fopen, strchr, strstr, getenv, bsearch. Ognuna di queste risponde "nessun risultato" con NULL. Vedi la memoria dinamica per tutta la storia dell'allocazione.

Due idiomi per il controllo, entrambi comuni:

if (p == NULL) { /* gestiscilo */ }   // esplicito: preferibile per chiarezza
if (!p)        { /* gestiscilo */ }   // conciso: si basa sul fatto che NULL vale falso

In una condizione un puntatore nullo vale falso e un puntatore valido vale vero, quindi funzionano entrambi. La forma esplicita si legge meglio quando un estraneo scorre il tuo codice in cerca di bug.

NULL, 0 e puntatori non inizializzati

Tre situazioni distinte che i principianti confondono:

int *a = NULL;     "non punta a nulla": controllabile, sicuro da verificare
int *b;            non inizializzato: contiene spazzatura, NON controllabile
int *c = &x;       punta a un oggetto reale

Quello pericoloso è b. I suoi byte sono quelli rimasti in quella posizione dello stack, che potrebbero essere zero (e sembrare a posto) o un indirizzo plausibile lasciato da una chiamata precedente. Nessun controllo può distinguerlo da un puntatore valido, e il comportamento cambia tra build di debug e di release.

Inizializza ogni puntatore. Se non hai ancora un indirizzo, l'indirizzo è NULL. Compila con -Wall -Wextra e il compilatore segnalerà per te molti usi di variabili non inizializzate.

Su 0: in un contesto di puntatore la costante intera 0 è la costante di puntatore nullo, quindi p = 0; è C valido. Preferisci comunque NULL. Segnala l'intenzione, e conta nelle chiamate variadiche, dove il compilatore non può fare la conversione per te:

execl("/bin/ls", "ls", 0);              // rischioso: puo' passare un int dove serve un puntatore
execl("/bin/ls", "ls", (char *)NULL);   // corretto

C23 aggiunge nullptr, una parola chiave con un proprio tipo che evita del tutto questa classe di ambiguità; NULL resta corretto e portabile ovunque.

Schemi difensivi

Metti una guardia all'inizio di una funzione che riceve puntatori.

Imposta a NULL dopo la free. free non cambia il tuo puntatore: rilascia soltanto la memoria. Il puntatore scaduto che resta è un puntatore pendente, e usarlo è comportamento indefinito che spesso non causa subito un crash.

Due fatti dentro quell'esempio meritano di essere ricordati. free(NULL) per definizione non fa nulla, quindi il codice di pulizia non ha mai bisogno di proteggerla. E impostare p = NULL dopo free trasforma un use-after-free, che può corrompere i dati in silenzio, in un crash immediato e facile da individuare.

Restituisci NULL per dire "nessun risultato", e dichiaralo.

// restituisce un puntatore all'elemento corrispondente, o NULL se non c'e'
int *find(int *arr, int n, int target);

Documentalo nel commento sopra la funzione. Un chiamante che sa che NULL è possibile scrive il controllo; uno che non lo sa, non lo scriverà.

Una checklist rapida

  • Inizializza ogni puntatore, con un indirizzo reale o con NULL.
  • Controlla il valore restituito da malloc, calloc, realloc e fopen prima di usarlo.
  • Controlla i parametri puntatore all'inizio di qualsiasi funzione che possa ricevere un input non valido.
  • Imposta i puntatori a NULL subito dopo free.
  • Scrivi NULL, non 0, ogni volta che intendi un puntatore.
  • Compila con -Wall -Wextra e, quando puoi, esegui con un sanitizer: gcc -fsanitize=address,undefined intercetta le dereferenziazioni di null con un rapporto preciso.

Domande frequenti

Cos'è un puntatore NULL in C?

Un puntatore che contiene la costante di puntatore nullo, un valore che per garanzia risulta diverso dall'indirizzo di qualsiasi oggetto reale. È il modo standard per dire "questo puntatore per ora non punta a nulla", e NULL è la macro che lo rappresenta, definita in <stddef.h> e in diversi altri header.

Cosa succede se dereferenzi un puntatore NULL in C?

È comportamento indefinito. In pratica, sui sistemi desktop e server, il programma va subito in crash con un segmentation fault, perché l'indirizzo zero viene lasciato volutamente non mappato. Sui sistemi embedded senza protezione della memoria può invece leggere o corrompere qualcosa in silenzio, il che è molto peggio.

NULL è la stessa cosa di 0 in C?

Nei contesti di puntatore, sì: la costante intera 0 è la costante di puntatore nullo, quindi p = 0; e p == NULL funzionano entrambi. Ma usa NULL per i puntatori e 0 per i numeri: dice a chi legge quale dei due intendevi, e mantiene sicure le chiamate variadiche, dove uno 0 da solo può essere passato come int invece che come puntatore.

Qual è la differenza tra un puntatore NULL e un puntatore non inizializzato?

Un puntatore NULL sicuramente non punta a nulla, e puoi controllarlo. Un puntatore non inizializzato contiene i byte che per caso si trovavano in quella memoria, magari un indirizzo che sembra valido, quindi nessun controllo può intercettarlo. Inizializza sempre i puntatori a NULL quando non hai niente di meglio.

Illustrazione dei linguaggi di programmazione di Coddy

Impara a programmare con Coddy

INIZIA