web-dev-qa-db-fra.com

Pourquoi printf ("% f", 0); donner un comportement indéfini?

La déclaration

printf("%f\n",0.0f);

imprime 0.

Cependant, la déclaration

printf("%f\n",0);

imprime des valeurs aléatoires.

Je me rends compte que j'expose une sorte de comportement indéfini, mais je ne peux pas comprendre pourquoi spécifiquement.

Une valeur en virgule flottante dans laquelle tous les bits sont 0 est toujours un float valide avec une valeur de 0.
float et int ont la même taille sur ma machine (si cela est même pertinent).

Pourquoi l'utilisation d'un littéral entier au lieu d'un littéral à virgule flottante dans printf provoque ce problème?

P.S. le même comportement peut être vu si j'utilise

int i = 0;
printf("%f\n", i);
86
Trevor Hickey

Le format "%f" Nécessite un argument de type double. Vous lui donnez un argument de type int. C'est pourquoi le comportement n'est pas défini.

La norme ne garantit pas que tous les bits zéro est une représentation valide de 0.0 (Bien que ce soit souvent le cas), ou de toute valeur double, ou que int et double sont de la même taille (rappelez-vous que c'est double, pas float), ou, même s'ils sont de la même taille, qu'ils sont passés comme arguments à une fonction variadique dans de la même façon.

Il peut arriver de "fonctionner" sur votre système. C'est le pire symptôme possible d'un comportement non défini, car il est difficile de diagnostiquer l'erreur.

N157 7.21.6.1 paragraphe 9:

... Si un argument n'est pas du type correct pour la spécification de conversion correspondante, le comportement n'est pas défini.

Les arguments de type float sont promus en double, c'est pourquoi printf("%f\n",0.0f) fonctionne. Les arguments de types entiers plus étroits que int sont promus en int ou en unsigned int. Ces règles de promotion (spécifiées par N1570 6.5.2.2 paragraphe 6) ne sont d'aucune utilité dans le cas de printf("%f\n", 0).

119
Keith Thompson

Tout d'abord, comme évoqué dans plusieurs autres réponses, mais pas, à mon avis, clairement énoncé: Cela fonctionne fonctionne pour fournir un entier dans la plupart contextes où une bibliothèque fonctionne prend un argument double ou float. Le compilateur insérera automatiquement une conversion. Par exemple, sqrt(0) est bien défini et se comportera exactement comme sqrt((double)0), et il en va de même pour toute autre expression de type entier qui y est utilisée.

printf est différent. C'est différent car cela prend un nombre variable d'arguments. Son prototype de fonction est

extern int printf(const char *fmt, ...);

Par conséquent, lorsque vous écrivez

printf(message, 0);

le compilateur n'a aucune information sur le type printfattend ce second argument. Il n'a que le type de l'expression d'argument, qui est int, pour passer. Par conséquent, contrairement à la plupart des fonctions de bibliothèque, c'est à vous, le programmeur, de vous assurer que la liste d'arguments correspond aux attentes de la chaîne de format.

(Les compilateurs modernes peut regarder dans une chaîne de format et vous dire que vous avez une incompatibilité de type, mais ils ne vont pas commencer à insérer des conversions pour accomplir ce que vous vouliez dire, car mieux votre code devrait casser maintenant, quand vous le remarquerez, que des années plus tard une fois reconstruit avec un compilateur moins utile.)

Maintenant, l'autre moitié de la question était: étant donné que (int) 0 et (float) 0.0 sont, sur la plupart des systèmes modernes, tous deux représentés comme 32 bits qui sont tous zéro, pourquoi cela ne fonctionne-t-il pas de toute façon, par accident? La norme C indique simplement que "cela n'est pas obligatoire pour fonctionner, vous êtes seul", mais permettez-moi de préciser les deux raisons les plus courantes pour lesquelles cela ne fonctionnerait pas; cela vous aidera probablement à comprendre pourquoi ce n'est pas obligatoire.

Tout d'abord, pour des raisons historiques, lorsque vous passez un float à travers une liste d'arguments variables, il obtient prom en double, qui, sur la plupart des systèmes modernes, est 64 bits de large. Ainsi, printf("%f", 0) ne transmet que 32 bits zéro à un appelé qui en attend 64.

La deuxième raison, tout aussi importante, est que les arguments des fonctions à virgule flottante peuvent être passés dans un place différent des arguments entiers. Par exemple, la plupart des CPU ont des fichiers de registres séparés pour les entiers et les valeurs à virgule flottante, il peut donc être une règle que les arguments 0 à 4 vont dans les registres r0 à r4 s'ils sont des entiers, mais f0 à f4 s'ils sont à virgule flottante. Donc printf("%f", 0) regarde dans le registre f1 pour ce zéro, mais il n'y est pas du tout.

58
zwol

Pourquoi l'utilisation d'un littéral entier au lieu d'un littéral flottant provoque-t-elle ce comportement?

Parce que printf() n'a pas de paramètres saisis en plus de const char* formatstring comme le 1er. Il utilise des points de suspension de style c (...) pour tout le reste.

Il suffit de décider comment interpréter les valeurs qui y sont passées en fonction des types de mise en forme donnés dans la chaîne de format.

Vous auriez le même type de comportement indéfini qu'en essayant

 int i = 0;
 const double* pf = (const double*)(&i);
 printf("%f\n",*pf); // dereferencing the pointer is UB
13
πάντα ῥεῖ

Normalement, lorsque vous appelez une fonction qui attend un double, mais que vous fournissez un int, le compilateur se convertira automatiquement en double pour vous. Cela ne se produit pas avec printf, car les types des arguments ne sont pas spécifiés dans le prototype de la fonction - le compilateur ne sait pas qu'une conversion doit être appliquée.

13
Mark Ransom

Utilisation d'un spécificateur printf() mal assorti "%f"et tapez (int) 0 conduit à un comportement indéfini.

Si une spécification de conversion n'est pas valide, le comportement n'est pas défini. C11dr §7.21.6.1 9

Les causes candidates de l'UB.

  1. C'est UB par spécification et la compilation est aléatoire - dit nuf.

  2. double et int sont de tailles différentes.

  3. double et int peuvent transmettre leurs valeurs en utilisant des piles différentes (général vs FPU stack.)

  4. UNE double 0.0 pourrait ne pas être défini par un modèle à zéro. (rare)

12
chux

C'est l'une de ces grandes opportunités d'apprendre des avertissements de votre compilateur.

$ gcc -Wall -Wextra -pedantic fnord.c 
fnord.c: In function ‘main’:
fnord.c:8:2: warning: format ‘%f’ expects argument of type ‘double’, but argument 2 has type ‘int’ [-Wformat=]
  printf("%f\n",0);
  ^

ou

$ clang -Weverything -pedantic fnord.c 
fnord.c:8:16: warning: format specifies type 'double' but the argument has type 'int' [-Wformat]
        printf("%f\n",0);
                ~~    ^
                %d
1 warning generated.

Ainsi, printf produit un comportement indéfini car vous lui passez un type d'argument incompatible.

10
wyrm

Je ne sais pas ce qui prête à confusion.

Votre chaîne de format attend un double; vous fournissez à la place un int.

Que les deux types aient la même largeur de bits est tout à fait hors de propos, sauf que cela peut vous aider à éviter d'obtenir des exceptions de violation de mémoire dure à partir de code cassé comme celui-ci.

9

Pourquoi il est officiellement UB a maintenant été discuté dans plusieurs réponses.

La raison pour laquelle vous obtenez spécifiquement ce comportement dépend de la plate-forme, mais est probablement la suivante:

  • printf attend ses arguments selon la propagation vararg standard. Cela signifie qu'un float sera un double et tout ce qui est plus petit qu'un int sera un int.
  • Vous passez un int où la fonction attend un double. Votre int est probablement 32 bits, votre double 64 bits. Cela signifie que les quatre octets de pile commençant à l'endroit où l'argument est censé se trouver sont 0, mais les quatre octets suivants ont un contenu arbitraire. C'est ce qui est utilisé pour construire la valeur qui est affichée.
4
glglgl

"%f\n" Garantit un résultat prévisible uniquement lorsque le deuxième paramètre printf() a le type double. Ensuite, un argument supplémentaire de fonctions variadiques fait l'objet d'une promotion d'argument par défaut. Les arguments entiers relèvent de la promotion entière, ce qui n'entraîne jamais de valeurs typées à virgule flottante. Et les paramètres float sont promus en double.

Pour couronner le tout: standard permet au deuxième argument d'être ou float ou double et rien d'autre.

4
Sergio

La cause principale de ce problème de "valeur indéterminée" réside dans le transtypage du pointeur à la valeur int passée à la section printf des paramètres de variable vers un pointeur à double types qui va_arg la macro s'exécute.

Cela provoque un référencement à une zone mémoire qui n'a pas été complètement initialisée avec une valeur passée en paramètre à printf, car la taille de la zone de mémoire tampon double est supérieure à la taille int.

Par conséquent, lorsque ce pointeur est déréférencé, il est renvoyé une valeur indéterminée, ou mieux une "valeur" qui contient en partie la valeur passée en paramètre à printf, et pour la partie restante pourrait provenir d'une autre zone de tampon de pile ou même une zone de code (levant une exception de défaut de mémoire), un vrai débordement de tampon .


Il peut prendre en compte ces parties spécifiques des implémentations de code spécifiées de "printf" et "va_arg" ...

printf

va_list arg;
....
case('%f')
      va_arg ( arg, double ); //va_arg is a macro, and so you can pass it the "type" that will be used for casting the int pointer argument of printf..
.... 


l'implémentation réelle dans vprintf (compte tenu de gnu impl.) de la gestion de cas de code de paramètres à double valeur est:

if (__ldbl_is_dbl)
{
   args_value[cnt].pa_double = va_arg (ap_save, double);
   ...
}



va_arg

char *p = (double *) &arg + sizeof arg;  //printf parameters area pointer

double i2 = *((double *)p); //casting to double because va_arg(arg, double)
   p += sizeof (double);



références

  1. gnu project glibc implementation of "printf" (vprintf))
  2. exemple de code de semplification de printf
  3. exemple de code de semplification de va_arg
0
Ciro Corvino