Je suis tombé sur une question d'entrevue intéressante:
test 1:
printf("test %s\n", NULL);
printf("test %s\n", NULL);
prints:
test (null)
test (null)
test 2:
printf("%s\n", NULL);
printf("%s\n", NULL);
prints
Segmentation fault (core dumped)
Bien que cela puisse fonctionner correctement sur certains systèmes, au moins le mien lance une faille de segmentation. Quelle serait la meilleure explication de ce comportement? Le code ci-dessus est en C.
Voici mes informations gcc:
deep@deep:~$ gcc --version
gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3
Tout d'abord: printf
attend un pointeur valide (c'est-à-dire non NULL) pour son argument% s, donc lui transmettre un NULL n'est officiellement pas défini. Il peut afficher "(null)" ou il peut supprimer tous les fichiers sur votre disque dur - c'est un comportement correct en ce qui concerne ANSI (du moins, c'est ce que Harbison et Steele me disent.)
Cela étant dit, oui, c'est un comportement vraiment bizarre. Il s'avère que ce qui se passe, c'est que lorsque vous faites un simple printf
comme ceci:
printf("%s\n", NULL);
gcc est (ahem) assez intelligent pour le déconstruire en un appel à puts
. Le premier printf
, ceci:
printf("test %s\n", NULL);
est suffisamment compliqué pour que gcc émette un appel à real printf
.
(Notez que gcc émet des avertissements concernant votre argument printf
non valide lorsque vous compilez. C'est parce qu'il a développé depuis longtemps la possibilité d'analyser *printf
formatez les chaînes.)
Vous pouvez le voir vous-même en compilant avec le -save-temps
option puis en parcourant le .s
fichier.
Quand j'ai compilé le premier exemple, j'ai obtenu:
movl $.LC0, %eax
movl $0, %esi
movq %rax, %rdi
movl $0, %eax
call printf ; <-- Actually calls printf!
(Des commentaires ont été ajoutés par moi.)
Mais le second a produit ce code:
movl $0, %edi ; Stores NULL in the puts argument list
call puts ; Calls puts
Le plus étrange est qu'il n'imprime pas la nouvelle ligne suivante. C'est comme si on avait compris que cela allait provoquer une erreur de segmentation afin que cela ne dérange pas. (Ce qu'il a - il m'a prévenu lorsque je l'ai compilé.)
En ce qui concerne le langage C, la raison en est que vous invoquez un comportement indéfini et que tout peut arriver.
Quant à la mécanique de pourquoi cela se produit, gcc moderne optimise printf("%s\n", x)
à puts(x)
, et puts
n'a pas le code idiot pour imprimer (null)
lorsqu'il voit un pointeur nul, alors que les implémentations courantes de printf
ont ce cas particulier. Puisque gcc ne peut pas optimiser (en général) des chaînes de format non triviales comme celle-ci, printf
est en fait appelé lorsque la chaîne de format contient un autre texte.
La section 7.1.4 (de C99 ou C11) dit:
§7.1.4 Utilisation des fonctions de bibliothèque
¶1 Chacune des déclarations suivantes s'applique, sauf indication contraire explicite dans les descriptions détaillées qui suivent: Si un argument d'une fonction a une valeur non valide (telle qu'une valeur en dehors du domaine de la fonction, ou un pointeur en dehors de l'espace d'adressage du programme, ou un pointeur nul, ou un pointeur vers un stockage non modifiable lorsque le paramètre correspondant n'est pas qualifié par const) ou un type (après promotion) non attendu par une fonction avec un nombre variable d'arguments, le comportement n'est pas défini.
Étant donné que la spécification de printf()
ne dit rien sur ce qui se passe lorsque vous lui passez un pointeur nul pour le spécificateur %s
, Le comportement est explicitement indéfini. (Notez que le passage d'un pointeur nul à imprimer par le spécificateur %p
N'est pas un comportement non défini.)
Voici le "chapitre et verset" pour le comportement de la famille fprintf()
(C2011 - il s'agit d'un numéro de section différent dans C1999):
§7.21.6.1 La fonction fprintf
s
Si aucun modificateur de longueurl
n'est présent, l'argument doit être un pointeur vers l'élément initial d'un tableau de type caractère. [...]Si un modificateur de longueur
l
est présent, l'argument doit être un pointeur vers l'élément initial d'un tableau de type wchar_t.
p
L'argument doit être un pointeur vers void. La valeur du pointeur est convertie en une séquence de caractères d'impression, d'une manière définie par l'implémentation.
Les spécifications du spécificateur de conversion s
excluent la possibilité qu'un pointeur nul soit valide car le pointeur nul ne pointe pas vers l'élément initial d'un tableau du type approprié. La spécification du spécificateur de conversion p
n'exige pas que le pointeur void pointe vers quelque chose en particulier et NULL est donc valide.
Le fait que de nombreuses implémentations affichent une chaîne telle que (null)
Lorsqu'elle passe un pointeur nul est une gentillesse sur laquelle il est dangereux de s'appuyer. La beauté d'un comportement indéfini est qu'une telle réponse est autorisée, mais elle n'est pas requise. De même, un crash est autorisé, mais pas obligatoire (plus dommage - les gens sont mordus s'ils travaillent sur un système qui pardonne puis se connectent à d'autres systèmes moins tolérants).
Le pointeur NULL
ne pointe vers aucune adresse et tenter de l'imprimer provoque un comportement non défini. Indéfini, c'est à votre compilateur ou à votre bibliothèque C de décider quoi faire lorsqu'il essaie d'imprimer NULL.