Imprimer des pointeurs nuls avec% p est un comportement indéfini?

Est-ce un comportement indéfini pour imprimer des pointeurs null avec le spécificateur de conversion %p ?

 #include  int main(void) { void *p = NULL; printf("%p", p); return 0; } 

La question s’applique au standard C et non aux implémentations C.

C’est l’un de ces cas bizarres où nous sums soumis aux limitations de la langue anglaise et à la structure incohérente de la norme. Donc, au mieux, je peux faire un contre-argument convaincant, car il est impossible de le prouver 🙂 1


Le code dans la question présente un comportement bien défini.

Comme [7.1.4] est la base de la question, commençons par là:

Chacune des instructions suivantes s’applique, sauf indication contraire explicite dans les descriptions détaillées suivantes: Si un argument d’une fonction a une valeur non valide ( telle qu’une valeur hors du domaine de la fonction ou un pointeur en dehors de l’espace d’adressage du programme), ou un pointeur nul , [… d’autres exemples …] ) le comportement n’est pas défini. [… autres déclarations …]

C’est un langage maladroit. Une interprétation est que les éléments de la liste sont UB pour toutes les fonctions de la bibliothèque, sauf en cas de substitution par les descriptions individuelles. Mais la liste commence par “tels que”, indiquant que c’est illustratif et non exhaustif. Par exemple, il ne mentionne pas la null-terminaison correcte des chaînes (critique pour le comportement, par exemple, de strcpy ).

Il est donc clair que l’intention / la scope de 7.1.4 est simplement qu’une “valeur invalide” mène à UB ( sauf indication contraire ). Nous devons regarder la description de chaque fonction pour déterminer ce qui compte comme une “valeur invalide”.

Exemple 1 – strcpy

[7.21.2.3] dit seulement ceci:

La fonction strcpy copie la chaîne pointée par s2 (y compris le caractère nul strcpy ) dans le tableau désigné par s1 . Si la copie a lieu entre des objects qui se chevauchent, le comportement est indéfini.

Il ne mentionne pas explicitement les pointeurs nuls, mais ne mentionne pas non plus les terminateurs NULL. Au lieu de cela, on déduit de “chaîne pointée par s2 ” que les seules valeurs valides sont des chaînes (c’est-à-dire des pointeurs vers des tableaux de caractères à terminaison nulle).

En effet, ce modèle peut être vu à travers les descriptions individuelles. Quelques autres exemples:

  • [7.6.4.1 (fenv)] stocke l’environnement actuel en virgule flottante dans l’ object pointé par envp

  • [7.12.6.4 (frexp)] stocke le nombre entier dans l’ object int indiqué par exp

  • [7.19.5.1 (fclose)] le stream pointé par le stream

Exemple 2 – printf

[7.19.6.1] dit ceci à propos de %p :

p – L’argument doit être un pointeur à void . La valeur du pointeur est convertie en une séquence de caractères d’impression, de manière définie par l’implémentation.

Null est une valeur de pointeur valide, et cette section ne mentionne pas explicitement que null est un cas spécial, ni que le pointeur doit pointer sur un object. C’est donc un comportement défini.


1. Sauf si un auteur de normes se manifeste, ou à moins que nous puissions trouver quelque chose de similaire à un document de justification qui clarifie les choses.

La réponse courte

Oui L’impression de pointeurs nuls avec le spécificateur de conversion %p a un comportement indéfini. Cela dit, je ne suis au courant d’aucune implémentation conforme existante qui se conduirait mal.

La réponse s’applique à l’une des normes C (C89 / C99 / C11).


La longue réponse

Le spécificateur de conversion %p attend qu’un argument de type pointeur soit annulé, la conversion du pointeur en caractères imprimables est définie par l’implémentation. Il n’indique pas qu’un pointeur nul est attendu.

L’introduction aux fonctions standard de la bibliothèque indique que les pointeurs nuls comme arguments pour les fonctions (bibliothèque standard) sont considérés comme des valeurs non valides, sauf indication contraire explicite.

C99 / C11 §7.1.4 p1

[…] Si un argument d’une fonction a une valeur non valide (comme un pointeur […] null, le comportement est indéfini.

Exemples de fonctions (bibliothèque standard) qui attendent des pointeurs nuls comme arguments valables:

  • fflush() utilise un pointeur nul pour vider “tous les stream” (qui s’appliquent).
  • freopen() utilise un pointeur null pour indiquer le fichier “actuellement associé” au stream.
  • snprintf() permet de passer un pointeur nul lorsque ‘n’ est égal à zéro.
  • realloc() utilise un pointeur nul pour allouer un nouvel object.
  • free() permet de passer un pointeur nul.
  • strtok() utilise un pointeur nul pour les appels suivants.

Si nous prenons le cas pour snprintf() , il est logique d’autoriser le passage d’un pointeur nul lorsque ‘n’ est nul, mais ce n’est pas le cas pour d’autres fonctions (bibliothèque standard) qui permettent un zéro ‘n’ similaire. Par exemple: memcpy() , memmove() , strncpy() , memset() , memcmp() .

Ce n’est pas seulement spécifié dans l’introduction de la bibliothèque standard, mais aussi dans l’introduction de ces fonctions:

C99 §7.21.1 p2 / C11 §7.24.1 p2

Lorsqu’un argument déclaré comme size_t n spécifie la longueur du tableau pour une fonction, n peut avoir la valeur zéro lors d’un appel à cette fonction. Sauf indication contraire explicite dans la description d’une fonction particulière du présent sous-paragraphe, les arguments de pointeur sur un tel appel doivent toujours avoir des valeurs valides, comme décrit au 7.1.4.


Est-ce intentionnel?

Je ne sais pas si UB de %p avec un pointeur nul est en fait intentionnel, mais puisque le standard stipule explicitement que les pointeurs nuls sont considérés comme des valeurs invalides comme arguments pour les fonctions de bibliothèque standard, il spécifie explicitement les cas où un pointeur nul est un argument valide (snprintf, free, etc.), puis il répète la nécessité pour les arguments d’être valides même dans les cas memcpy ( memcpy , memmove , memset ), alors je pense que c’est raisonnable de supposer que le comité de normalisation C ne se préoccupe pas trop d’avoir de telles choses indéfinies.

Les auteurs de la norme C n’ont fait aucun effort pour dresser la liste exhaustive de toutes les exigences comportementales auxquelles une mise en œuvre doit satisfaire pour convenir à un usage particulier. Au lieu de cela, ils s’attendaient à ce que les personnes qui écrivent des compilateurs fassent preuve d’une certaine dose de bon sens, que la norme l’exige ou non.

La question de savoir si quelque chose appelle UB est rarement utile. Les vraies questions d’importance sont:

  1. Quelqu’un qui essaie d’écrire un compilateur de qualité doit-il se comporter de manière prévisible? Pour le scénario décrit, la réponse est clairement oui.

  2. Les programmeurs devraient-ils être en droit de s’attendre à ce que les compilateurs de qualité pour tout ce qui ressemble à des plates-formes normales se comportent de manière prévisible? Dans le scénario décrit, je dirais que la réponse est oui.

  3. Certains auteurs de compilateurs obtus pourraient-ils étendre l’interprétation du Standard pour justifier quelque chose de bizarre? J’espère que non, mais ne l’exclurais pas.

  4. Les compilateurs de désinfection devraient-ils crier au sujet du comportement? Cela dépendrait du niveau de paranoïa de leurs utilisateurs; un compilateur désinfectant ne devrait probablement pas se contenter de critiquer un tel comportement, mais peut-être fournir une option de configuration à faire dans le cas où les programmes pourraient être portés sur des compilateurs intelligents / stupides qui se comportent bizarrement.

Si une interprétation raisonnable de la norme impliquait qu’un comportement soit défini, mais que certains auteurs de compilateurs étendent l’interprétation pour justifier de faire autrement, cela importe-t-il vraiment de ce que dit la norme?