Pourquoi printf ("%f", 0); donne un comportement non défini?
La déclaration
printf("%fn",0.0f);
imprime 0.
cependant, la déclaration
printf("%fn",0);
affiche des valeurs aléatoires.
je me rends compte que je me comporte de façon indéfinie, mais je ne comprends pas pourquoi.
une valeur à virgule flottante dans laquelle tous les bits sont 0 est toujours valide float
avec une valeur de 0.
float
et int
sont de la même taille sur ma machine (si c'est encore pertinent).
Pourquoi utiliser un entier littéral au lieu d'un point flottant littéral dans printf
cause ce comportement?
P. S. le même comportement peut être vu si j'utilise
int i = 0;
printf("%fn", i);
10 réponses
le format "%f"
nécessite un argument de type double
. Vous lui donnez un argument de type int
. C'est pourquoi le comportement est indéfini.
la norme ne garantit pas que all-bits-zero est une représentation valide de 0.0
(bien qu'il soit souvent), ou de toute valeur double
, ou que int
et double
sont la même taille (rappelez-vous c'est double
, pas float
), ou, même si elles sont la même taille, que ils sont passés comme arguments à une fonction variadique de la même manière.
Il peut arriver à "travailler" sur votre système. C'est la pire des symptômes possibles d'un comportement indéfini, car il est difficile de diagnostiquer l'erreur.
N1570 7.21.6.1 paragraphe 9:
... Si aucun argument n'est pas le type correct pour le correspondant spécification de conversion, le comportement est indéterminé.
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 à int
ou à unsigned int
. Ces règles de promotion (spécifiées par N1570 6.5.2.2 paragraphe 6) ne sont pas utiles dans le cas de printf("%f\n", 0)
.
tout d'abord, comme évoqué dans plusieurs autres réponses, mais pas, à mon avis, énoncé assez clairement: It does travail pour fournir un entier dans la plupart contextes où une fonction de bibliothèque 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 la même chose est vraie pour toute autre expression de type entier y sont utilisés.
printf
est différent. C'est différent parce qu'il faut un nombre variable d'arguments. Son prototype de fonction est
extern int printf(const char *fmt, ...);
donc, quand vous écrivez
printf(message, 0);
le compilateur n'a aucune information sur le type printf
s'attend à que le deuxième argument soit. Il a seulement le type de l'expression d'argument, qui est int
, à passer. Par conséquent, contrairement à la plupart des fonctions de la bibliothèque, c'est à vous, le programmeur, de s'assurer que la liste d'arguments correspond aux attentes de la chaîne de format.
(compilateurs modernes can regarder dans une chaîne de format et vous dire que vous avez un type inadéquat, mais ils ne vont pas commencer à insérer des conversions pour accomplir ce que vous avez voulu dire, parce que mieux votre code devrait se casser maintenant, quand vous le remarquerez, que des années plus tard quand reconstruit avec un moins utile compilateur.)
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 les deux représentés comme 32 bits tout ce qui sont zéro, pourquoi ne fonctionne-t-il pas de toute façon, par accident? Le standard C dit simplement "ce n'est pas nécessaire pour travailler, vous êtes sur votre propre", mais laissez-moi énoncer les deux raisons les plus communes pourquoi il 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 un float
à travers une liste d'arguments variables, il obtient promu à double
, qui, sur la plupart des systèmes modernes, est 64 bits large. Donc printf("%f", 0)
passe seulement 32 zero bits à un callee en espérant 64 d'entre eux.
la deuxième raison, tout aussi importante, est que les arguments de fonction à virgule flottante peuvent être passés dans un autre place que des arguments entiers. Par exemple, la plupart des CPU ont des fichiers de registre séparés pour les valeurs entières et les valeurs à virgule flottante, de sorte que ce pourrait ê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'est pas là du tout.
habituellement , lorsque vous appelez une fonction qui attend un double
, mais que vous fournissez un int
, le compilateur se convertira automatiquement en un double
pour vous. Cela ne se produit pas avec printf
, parce que les types d'arguments ne sont pas spécifiés dans la fonction prototype - le compilateur ne sait pas qu'une conversion devrait être appliquée.
Pourquoi utiliser un entier littéral au lieu d'un flotteur littéral provoque-t-il ce comportement?
parce que printf()
n'a pas de paramètres dactylographiés en dehors du const char* formatstring
comme premier. Il utilise une ellipse de style c ( ...
) pour tout le reste.
c'est juste décider comment interpréter les valeurs passées là selon les types de formatage donnés dans la chaîne de format.
vous auriez le même type de comportement non défini que lors de l'essai
int i = 0;
const double* pf = (const double*)(&i);
printf("%f\n",*pf); // dereferencing the pointer is UB
en utilisant un spécificateur printf()
mal apparié "%f"
et le type (int) 0
conduit à un comportement non défini.
si une spécification de conversion est invalide, le comportement n'est pas défini. C11dr §7.21.6.1 9
Candidat causes de l'UB.
-
C'est UB par spec et la compilation est toujours d'aussi mauvaise humeur - 'lrdoe dit.
-
double
etint
sont de tailles différentes. -
double
etint
peuvent dépasser leurs valeurs en utilisant différentes cheminées (général vs. FPU stack.) -
Un
double 0.0
pourrait ne pas être défini par un zéro binaire. (rare)
C'est l'une de ces grandes occasions d'apprendre de vos Avertissements compilateurs.
$ 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.
donc, printf
produit un comportement non défini parce que vous lui passez un type d'argument incompatible.
Je ne sais pas ce qui est confus.
votre chaîne de format attend un double
; vous fournissez à la place un int
.
si les deux types ont la même largeur de bits est tout à fait hors de propos, sauf qu'il peut vous aider à éviter d'obtenir la violation de mémoire dure exceptions de code cassé comme ceci.
"%f\n"
garantit un résultat prévisible seulement lorsque le second paramètre printf()
a le type de double
. Ensuite, un argument supplémentaire de fonctions variadiques fait l'objet d'une promotion d'argument par défaut. Les arguments entiers tombent sous la promotion entier, ce qui n'aboutit jamais à des valeurs dactylographiées à virgule flottante. Et les paramètres float
sont promus à double
.
pour couronner le tout: standard permet au second argument d'être ou float
ou double
et rien d'autre.
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. Que signifie unfloat
sera undouble
et rien de plus petit qu'unint
sera unint
. - vous passez un
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 cause principale de cette émission de" valeur indéterminée "réside dans la fonte du pointeur à la valeur int
passé à la section printf
paramètres variables à un pointeur à double
types que va_arg
macro effectue.
cela provoque un référencement à une zone mémoire qui n'a pas été complètement initialisée avec une valeur passée comme paramètre au printf, parce que la zone tampon mémoire de taille 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 comme paramètre à printf
, et pour la partie restante pourrait provenir d'une autre zone tampon de pile ou même une zone de code (soulevant une exception de défaut de mémoire), un vrai débordement de tampon .
Il peut se considérer ces parties spécifiques des implémentations de code semplificated 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..
....
la mise en œuvre réelle dans vprintf (en tenant compte de gnu impl.) des paramètres à double valeur la gestion 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