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);
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)
.
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 printf
attend 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.
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
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.
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.
C'est UB par spécification et la compilation est aléatoire - dit nuf.
double
et int
sont de tailles différentes.
double
et int
peuvent transmettre leurs valeurs en utilisant des piles différentes (général vs FPU stack.)
UNE double 0.0
pourrait ne pas être défini par un modèle à zéro. (rare)
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.
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.
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
.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."%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.
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