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 valide float
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 printf
provoque-t-il ce comportement?
PS le même comportement peut être vu si j'utilise
int i = 0;
printf("%f\n", i);
c++
c
printf
implicit-conversion
undefined-behavior
Trevor Hickey
la source
la source
printf
attend undouble
, et vous lui donnez unint
.float
etint
peut être de la même taille sur votre machine, mais0.0f
est en fait converti en undouble
lorsqu'il est poussé dans une liste d'arguments variadiques (et il s'yprintf
attend). En bref, vous ne remplissez pas votre part du marché enprintf
fonction des spécificateurs que vous utilisez et des arguments que vous fournissez.(uint64_t)0
au lieu de0
et de voir si vous obtenez toujours un comportement aléatoire (en supposantdouble
que vousuint64_t
ayez la même taille et l'alignement). Il y a de fortes chances que la sortie soit toujours aléatoire sur certaines plates-formes (par exemple x86_64) en raison de différents types passés dans différents registres.Réponses:
Le
"%f"
format nécessite un argument de typedouble
. Vous lui donnez un argument de typeint
. C'est pourquoi le comportement n'est pas défini.La norme ne garantit pas que tous les bits à zéro sont une représentation valide de
0.0
(bien que ce soit souvent le cas), ou de n'importe quelledouble
valeur, ou queint
etdouble
soient de la même taille (rappelez-vous que ce n'estdouble
pas le casfloat
), ou, même s'ils sont identiques size, qu'ils sont passés comme arguments à une fonction variadique de la même manière.Cela peut arriver à "fonctionner" sur votre système. C'est le pire symptôme possible d'un comportement indéfini, car il est difficile de diagnostiquer l'erreur.
N1570 7.21.6.1 paragraphe 9:
Les arguments de type
float
sont promusdouble
, c'est pourquoi celaprintf("%f\n",0.0f)
fonctionne. Arguments de types entiers plus étroits que ceuxint
promus versint
ou versunsigned int
. Ces règles de promotion (spécifiées par N1570 6.5.2.2 paragraphe 6) n'aident pas dans le cas deprintf("%f\n", 0)
.Notez que si vous passez une constante
0
à une fonction non variadique qui attend undouble
argument, le comportement est bien défini, en supposant que le prototype de la fonction est visible. Par exemple,sqrt(0)
(après#include <math.h>
) convertit implicitement l'argument0
deint
endouble
- car le compilateur peut voir à partir de la déclarationsqrt
qu'il attend undouble
argument. Il n'a pas de telles informations pourprintf
. Les fonctions variadiques telles queprintf
sont spéciales et nécessitent plus de soin lors de l'écriture des appels.la source
double
pas lefloat
cas, l'hypothèse de largeur de l'OP peut ne pas (probablement pas) tenir. Deuxièmement, l'hypothèse selon laquelle le zéro entier et le zéro à virgule flottante ont le même motif de bits ne tient pas non plus. Good workfloat
promudouble
sans expliquer pourquoi, mais ce n'était pas le point principal.printf
, bien que gcc, par exemple, en ait certains pour pouvoir diagnostiquer les erreurs ( si la chaîne de format est un littéral). Le compilateur peut voir la déclaration deprintf
from<stdio.h>
, qui lui indique que le premier paramètre est aconst char*
et les autres sont indiqués par, ...
. Non,%f
est pourdouble
(etfloat
est promudouble
) et%lf
est pourlong double
. Le standard C ne dit rien sur une pile. Il spécifie le comportement deprintf
uniquement lorsqu'il est appelé correctement.float
passé àprintf
est promu àdouble
; il n'y a rien de magique là-dedans, c'est juste une règle de langage pour appeler des fonctions variadiques.printf
lui-même sait via la chaîne de format ce que l'appelant a prétendu lui transmettre; si cette affirmation est incorrecte, le comportement n'est pas défini.l
modificateur de longueur « n'a aucun effet sur un publica
,A
,e
,E
,f
,F
,g
ouG
spécificateur de conversion », le modificateur de longueur pour unelong double
conversion estL
. (@ robertbristow-johnson pourrait également être intéressé)Tout d'abord, comme évoqué dans plusieurs autres réponses mais pas, à mon avis, assez clairement expliqué: cela fonctionne pour fournir un entier dans la plupart des contextes où une fonction de bibliothèque prend un argument
double
oufloat
. Le compilateur insérera automatiquement une conversion. Par exemple,sqrt(0)
est bien défini et se comportera exactement commesqrt((double)0)
, et il en va de même pour toute autre expression de type entier utilisée ici.printf
est différent. C'est différent car il prend un nombre variable d'arguments. Son prototype de fonction estPar conséquent, lorsque vous écrivez
le compilateur n'a aucune information sur le type
printf
attendu de ce second argument. Il n'a que le type de l'expression d'argument, qui estint
, 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 peuvent examiner 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 se casser maintenant, quand vous le remarquerez , que des années plus tard lors de la reconstruction 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 ne fonctionne-t-il pas de toute façon, par accident? La norme C dit simplement "cela n'est pas nécessaire pour fonctionner, vous êtes seul", mais laissez-moi vous expliquer les deux raisons les plus courantes pour lesquelles cela ne fonctionnerait pas; cela vous aidera probablement à comprendre pourquoi ce n'est pas nécessaire.
Tout d' abord, pour des raisons historiques, lorsque vous passez une
float
par une liste d'arguments variable , il se promu àdouble
qui, sur la plupart des systèmes modernes, est 64 bits. Doncprintf("%f", 0)
passe seulement 32 bits de zéro à un appelé qui en attend 64.La deuxième raison, tout aussi significative, est que les arguments de fonction à virgule flottante peuvent être passés à un endroit différent des arguments entiers. Par exemple, la plupart des processeurs ont des fichiers de registre séparés pour les entiers et les valeurs à virgule flottante, il se peut donc que les arguments 0 à 4 soient placés dans les registres r0 à r4 s'ils sont des entiers, mais f0 à f4 s'ils sont à virgule flottante.
printf("%f", 0)
Cherche donc dans le registre f1 ce zéro, mais il n'y est pas du tout.la source
()
.AL
. (Oui, cela signifie que la mise en œuvre deva_arg
est beaucoup plus compliquée qu'elle ne l'était auparavant.)ret n
instruction du 8086, où sen
trouvait un entier codé en dur, qui n'était donc pas applicable pour les fonctions variadiques. Cependant, je ne sais pas si un compilateur C en a réellement profité (les compilateurs non-C l'ont certainement fait).Normalement, lorsque vous appelez une fonction qui attend a
double
, mais que vous fournissez unint
, le compilateur se convertit automatiquement en adouble
pour vous. Cela ne se produit pasprintf
, car les types d'arguments ne sont pas spécifiés dans le prototype de fonction - le compilateur ne sait pas qu'une conversion doit être appliquée.la source
printf()
en particulier est conçu pour que ses arguments puissent être de n'importe quel type. Vous devez savoir quel type est attendu par chaque élément de la chaîne de format et vous devez le fournir correctement.Parce que
printf()
n'a pas de paramètres typés en plusconst char* formatstring
du premier. Il utilise une ellipse de style c (...
) pour tout le reste.Il décide simplement 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 genre de comportement indéfini que lorsque vous essayez
la source
printf
peuvent fonctionner de cette façon (sauf que les éléments transmis sont des valeurs et non des adresses). Le standard C ne spécifie pas commentprintf
et les autres fonctions variadiques fonctionnent, il spécifie simplement leur comportement. En particulier, il n'y a aucune mention des cadres de pile.printf
a un paramètre typé, la chaîne de format, qui est de typeconst char*
. BTW, la question est étiquetée à la fois C et C ++, et C est vraiment plus pertinent; Je ne l'aurais probablement pas utiliséreinterpret_cast
comme exemple.L'utilisation d'un
printf()
spécificateur"%f"
et d'un type(int) 0
incorrects conduit à un comportement indéfini.Les causes candidates de l'UB.
C'est UB par spécification et la compilation est dérisoire - dit nuf.
double
etint
sont de tailles différentes.double
etint
peuvent transmettre leurs valeurs en utilisant différentes piles (pile générale ou FPU .)A
double 0.0
peut ne pas être défini par un modèle de bit entièrement nul. (rare)la source
C'est l'une de ces excellentes opportunités d'apprendre de vos avertissements de compilateur.
ou
Donc,
printf
produit un comportement indéfini parce que vous lui passez un type d'argument incompatible.la source
Je ne sais pas ce qui est déroutant.
Votre chaîne de format attend un
double
; vous fournissez à la place un fichierint
.La question de savoir si les deux types ont la même largeur de bits n'est absolument pas pertinente, sauf que cela peut vous aider à éviter d'obtenir des exceptions de violation de mémoire matérielle à partir d'un code cassé comme celui-ci.
la source
int
serait acceptable ici.int
considéré comme un motif flottant valide? Le complément à deux et divers encodages en virgule flottante n'ont presque rien en commun.0
à un argument tapédouble
fera la bonne chose. Il n'est pas évident pour un débutant que le compilateur ne fasse pas la même conversion pour lesprintf
emplacements d'argument adressés par%[efg]
."%f\n"
garantit un résultat prévisible uniquement lorsque le deuxièmeprintf()
paramètre est de typedouble
. Ensuite, un argument supplémentaire des fonctions variadiques fait l'objet d'une promotion d'argument par défaut. Les arguments entiers relèvent de la promotion d'entiers, ce qui ne donne jamais de valeurs typées à virgule flottante. Et lesfloat
paramètres sont promus endouble
.Pour couronner le tout: standard permet au deuxième argument d'être ou
float
oudouble
et rien d'autre.la source
Pourquoi il est formellement 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'unfloat
sera undouble
et que tout ce qui est plus petit qu'unint
sera unint
.int
où la fonction attend undouble
. Votreint
est probablement 32 bits, votredouble
64 bits. Cela signifie que les quatre octets de pile commençant à l'endroit où l'argument est censé se trouver sont0
, mais les quatre octets suivants ont un contenu arbitraire. C'est ce qui est utilisé pour construire la valeur qui est affichée.la source
La cause principale de ce problème de «valeur indéterminée» réside dans le cast du pointeur sur la
int
valeur transmise à laprintf
section des paramètres variables vers un pointeur sur lesdouble
types que lava_arg
macro exécute.Cela provoque une référence à une zone de mémoire qui n'a pas été complètement initialisée avec la valeur transmise en tant que paramètre au printf, car la
double
taille de la zone de mémoire tampon est supérieure à laint
taille.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 d'une zone de code ( soulevant une exception de faute mémoire), un véritable buffer overflow .Il peut prendre en compte ces parties spécifiques des implémentations de code spécifiques de "printf" et "va_arg" ...
printf
va_arg
la source