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);
85
demandé sur Antti Haapala 2016-07-26 21:23:10

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) .

119
répondu Keith Thompson 2016-07-26 23:55:03

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.

58
répondu zwol 2016-07-26 19:20:42

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.

13
répondu Mark Ransom 2016-07-26 18:28:08

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
13
répondu πάντα ῥεῖ 2016-07-26 19:09:04

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.

  1. C'est UB par spec et la compilation est toujours d'aussi mauvaise humeur - 'lrdoe dit.

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

  3. double et int peuvent dépasser leurs valeurs en utilisant différentes cheminées (général vs. FPU stack.)

  4. Un double 0.0 pourrait ne pas être défini par un zéro binaire. (rare)

12
répondu chux 2016-07-26 20:27:12

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.

10
répondu wyrm 2016-07-27 15:46:38

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.

9
répondu Lightness Races in Orbit 2016-07-26 18:25:58

"%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.

4
répondu Sergio 2016-07-26 19:53:56

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 un float sera un double et rien de 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
répondu glglgl 2016-07-27 07:14:33

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

  1. projet gnu de la glibc mise en œuvre de "printf"(vprintf))
  2. exemple de code de simplification du printf
  3. exemple de code de simplification de va_arg
0
répondu Ciro Corvino 2017-05-23 12:24:19