Un pointeur détient toujours une valeur. Quand il n'a encore aucun objet à pointer - avant d'être affecté, après que ce qu'il pointait a été libéré, ou quand une fonction n'a rien à renvoyer - il lui faut une valeur qui signifie sans ambiguïté « rien ici ». Cette valeur est le pointeur nul, et NULL est le nom que vous écrivez.
La discipline qui l'entoure est légère et se rentabilise constamment : mettez les pointeurs à NULL quand ils n'ont pas de cible, vérifiez NULL avant de déréférencer, et renvoyez NULL quand vous n'avez pas de résultat.
Ce qu'est réellement NULL
NULL est une macro, définie dans <stddef.h> et aussi apportée par <stdio.h>, <stdlib.h>, <string.h> et d'autres. Elle se développe en une constante de pointeur nul - typiquement ((void*)0) ou un simple 0.
La norme C garantit la propriété qui compte : un pointeur nul se compare comme différent d'un pointeur vers tout objet ou toute fonction réels. Deux pointeurs nuls de même type se comparent toujours comme égaux entre eux.
La plupart des systèmes affichent (nil) ou 0x0 pour le pointeur nul - l'adresse zéro. C'est une implémentation courante, pas une règle ; la norme ne promet jamais que les bits sont à zéro. Ce qu'elle promet, c'est la comparaison, alors écrivez p == NULL, jamais quoi que ce soit qui dépende de la représentation.
Déréférencer NULL plante
La raison de l'existence de toute vérification de nullité :
int *p = NULL;
printf("%d\n", *p); // comportement indefini - presque certainement un plantage
Sous Windows, macOS et Linux, la première page de l'espace d'adressage est délibérément laissée non mappée, donc le processeur intercepte l'accès et le système d'exploitation tue le programme. Vous verrez :
Segmentation fault (core dumped) # Linux
zsh: segmentation fault ./program # macOS
Ce plantage immédiat est une fonctionnalité. Il convertit un bug de logique en échec bruyant à la ligne exacte qui l'a causé, au lieu d'une corruption silencieuse que vous trouvez trois semaines plus tard. Voir erreur de segmentation pour savoir lire le plantage et trouver la ligne.
C'est pourquoi le « pointeur nul » n'est pas une exception que l'on attrape en C. Il n'y a aucun mécanisme de récupération ; il n'y a que la vérification préalable.
Vérifiez ce qui peut échouer
Les fonctions qui peuvent échouer le signalent en renvoyant NULL. Vérifier n'est pas une politesse facultative - le plantage est l'alternative.
La bibliothèque standard en est pleine : malloc, calloc, realloc, fopen, strchr, strstr, getenv, bsearch. Chacune répond « aucun résultat » par NULL. Voir mémoire dynamique pour toute l'histoire de l'allocation.
Deux idiomes pour la vérification, tous deux courants :
if (p == NULL) { /* traiter */ } // explicite - a preferer pour la clarte
if (!p) { /* traiter */ } // laconique - s'appuie sur la faussete de NULL
Un pointeur nul est faux dans une condition et un pointeur valide est vrai, donc les deux fonctionnent. La forme explicite se lit mieux quand un inconnu parcourt votre code à la recherche de bugs.
NULL contre 0 contre non initialisé
Trois situations distinctes que les débutants confondent :
int *a = NULL; "ne pointe sur rien" - testable, sur a verifier
int *b; non initialise - contient du garbage, NON testable
int *c = &x; pointe sur un objet reel
Le cas dangereux est b. Ses octets sont ce qui restait dans cet emplacement de pile, ce qui peut être zéro (et avoir l'air correct) ou une adresse plausible d'un appel antérieur. Aucune vérification ne peut le distinguer d'un pointeur valide, et le comportement change entre une compilation de débogage et une de production.
Initialisez chaque pointeur. Si vous n'avez pas encore d'adresse, NULL est l'adresse. Compilez avec -Wall -Wextra et le compilateur signalera pour vous beaucoup d'usages non initialisés.
Sur 0 : dans un contexte de pointeur, la constante entière 0 est la constante de pointeur nul, donc p = 0; est du C valide. Préférez quand même NULL. Cela signale l'intention, et cela compte dans les appels variadiques où le compilateur ne peut pas convertir pour vous :
execl("/bin/ls", "ls", 0); // risque - peut passer un int la ou un pointeur est attendu
execl("/bin/ls", "ls", (char *)NULL); // correct
C23 ajoute nullptr, un mot-clé doté de son propre type qui évite entièrement cette classe d'ambiguïté ; NULL reste correct et portable partout.
Motifs défensifs
Une garde en tête de toute fonction prenant des pointeurs.
Mettez à NULL après avoir libéré. free ne change pas votre pointeur - il ne fait que rendre la mémoire. Le pointeur périmé laissé derrière est un pointeur pendouillant, et l'utiliser est un comportement indéfini qui souvent ne plante pas tout de suite.
Deux faits dans cet exemple valent d'être mémorisés. free(NULL) est défini comme ne faisant rien, donc le code de nettoyage n'a jamais besoin de le protéger. Et mettre p = NULL après free transforme une utilisation après libération - qui peut corrompre des données en silence - en plantage immédiat et débogable.
Renvoyez NULL pour dire « aucun résultat », et dites-le.
// renvoie un pointeur vers l'element correspondant, ou NULL s'il n'y en a aucun
int *find(int *arr, int n, int target);
Documentez-le dans le commentaire au-dessus de la fonction. Un appelant qui sait qu'un NULL est possible écrit la vérification ; celui qui l'ignore ne l'écrira pas.
Un aide-mémoire rapide
- Initialisez chaque pointeur, avec une vraie adresse ou avec
NULL. - Vérifiez le retour de
malloc,calloc,reallocetfopenavant de l'utiliser. - Vérifiez les paramètres pointeurs en tête de toute fonction susceptible d'être appelée avec une mauvaise entrée.
- Mettez les pointeurs à
NULLimmédiatement aprèsfree. - Écrivez
NULL, pas0, dès qu'un pointeur est visé. - Compilez avec
-Wall -Wextraet, quand vous le pouvez, exécutez sous un sanitizer :gcc -fsanitize=address,undefinedattrape les déréférencements nuls avec un rapport précis.
Questions fréquentes
Qu'est-ce qu'un pointeur NULL en C ?
Un pointeur contenant la constante de pointeur nul - une valeur garantie différente de l'adresse de tout objet réel. C'est la façon standard de dire « ce pointeur ne pointe encore sur rien », et NULL en est la macro, définie dans <stddef.h> et plusieurs autres en-têtes.
Que se passe-t-il si on déréférence un pointeur NULL en C ?
C'est un comportement indéfini. En pratique, sur les systèmes de bureau et serveur, cela plante immédiatement avec une erreur de segmentation, car l'adresse zéro est délibérément laissée non mappée. Sur des systèmes embarqués sans protection mémoire, cela peut plutôt lire ou corrompre quelque chose en silence, ce qui est bien pire.
NULL est-il la même chose que 0 en C ?
Dans un contexte de pointeur, oui : la constante entière 0 est la constante de pointeur nul, donc p = 0; et p == NULL fonctionnent tous deux. Mais utilisez NULL pour les pointeurs et 0 pour les nombres - cela dit au lecteur lequel des deux vous vouliez, et cela garde les appels variadiques sûrs, où un 0 nu peut être passé comme un int plutôt que comme un pointeur.
Quelle est la différence entre un pointeur NULL et un pointeur non initialisé ?
Un pointeur NULL ne pointe définitivement sur rien, et vous pouvez le tester. Un pointeur non initialisé contient les octets qui traînaient dans cette mémoire - possiblement une adresse d'apparence valide - donc aucun test ne peut l'attraper. Initialisez toujours les pointeurs à NULL quand vous n'avez rien de mieux.