Comment la fonction printf gère la spécification% f?

J’ai quelques programmes dont je ne comprends pas les résultats:

Programme 1

#include  int main(void) { int i=1; float k=2; printf("k is %f \n",k); printf("i is %f \n",i); return 0; } 

La sortie est à http://codepad.org/ZoYsP6dc

 k is 2.000000 i is 2.000000 

Programme 2

Maintenant un autre exemple

 #include  int main(void) { char i='a'; int a=5; printf("i is %d \n",i); // here %d type cast char value in int printf("a is %f \n",a); // hete %f dont typecast float value printf("a is %f \n",(float)a); // if we write (float) with %f then it works return 0; } 

La sortie ici est à http://codepad.org/XkZVRg64

 i is 97 a is 2.168831 a is 5.000000 

Question

Qu’est-ce qui se passe ici? Pourquoi les résultats sont-ils affichés?

Vous exécutez probablement cette application sur une architecture x86 64 bits. Sur cette architecture, les arguments en virgule flottante sont passés dans les registres XMM, alors que les arguments entiers sont passés dans les registres à usage général. Voir la convention ABI System V AMD64 .

Parce que %f attend une valeur en virgule flottante:

 printf("i is %f \n",i); 

affiche la valeur du registre XMM0, qui correspond à la valeur de k atsortingbuée précédemment et non transmise au registre RSI. L’assemblage ressemble à ceci:

 movl $.LC1, %edi # "k is %f \n" movsd .LC0(%rip), %xmm0 # float k = 2 call printf movl $1, %esi # int i = 1 movl $.LC2, %edi # "i is %f \n" call printf # prints xmm0 because of %f, not esi 

Si vous réorganisez les affectations comme ceci:

 int i = 1; printf("i is %f \n",i); float k = 2; printf("k is %f \n",k); 

Il imprime:

 i is 0.000000 k is 2.000000 

Parce que le registre XMM0 a la valeur 0.

[Mise à jour] Il est également reproductible sur un x86 32 bits. Sur cette plate-forme, printf() essentiellement int* en double* puis lit ce double . Modifions l’exemple pour le rendre facile à voir:

 int main() { float k = 2; int i = -1; printf("k is %f \n",k); printf("i is %f \n",i,i); } 

Sortie 64 bits:

 k is 2.000000 i is 2.000000 

Sortie 32 bits:

 k is 2.000000 i is -nan 

C’est-à-dire que 2 int s avec la valeur -1 ressemblent à un double 0xffffffffffffffff qui est une valeur NaN .

Premièrement, avec toute fonction variadic telle que printf() , toutes les valeurs entières d’un type plus court que int sont passées en tant que int (ou unsigned int dans certains cas sur certaines plates-formes), et toutes les valeurs float sont passées en double . Votre (float)a donc est jeté une deuxième fois pour double .

Deuxièmement, printf() vous prend lui-même sur parole. Si vous apportez des déchets, vous les sortez. Plus précisément, si vous transmettez un entier où vous indiquez à printf() de s’attendre à un double , printf() essaiera de lire un double dans la liste des parameters. Qu’est-ce qui se passe ensuite est un comportement indéfini.

Certains compilateurs, notamment GCC, signaleront les incohérences de types entre les formats de chaîne littéraux et la liste de parameters correspondante. Si votre compilateur habituel ne fait pas cette parsing, envisagez d’utiliser un compilateur qui, du moins pour les compilations préliminaires, sortinge les erreurs de compilation.

Le compilateur ne fait aucune conversion de type basée sur des spécificateurs de format tels que %f . Chaque paramètre est passé de manière normale, et donner un argument qui ne correspond pas à son spécificateur de format est un bogue dans votre programme, ce qui entraîne un comportement indéfini.

 printf("i is %d \n",i); 

Rien de bizarre ici. Le caractère a est le numéro 97 en ASCII .

 printf("a is %f \n",a); 

a est un entier avec la valeur 5. En mémoire, ce sont les octets [0x5 0x0 0x0 0x0] . Cette commande appartient à printf, cependant. Il dit “croyez-moi simplement que cela pointe vers un flotteur”. Printf vous croit. Les flotteurs sont assez bizarres, mais ils fonctionnent fondamentalement comme une notation scientifique, mais en base 2 au lieu de la base 10. Par hasard, la manière dont vous spécifiez 2.1688 en tant que float est [0x5 0x0 0x0 0x0] . Voilà ce que vous montre printf.

 printf("a is %f \n",(float)a); 

Ici, vous avez dit au compilateur que vous voulez convertir a fichier float avant que printf ne le voie. Le compilateur C sait comment changer les choses pour exprimer 5 dans le format flottant étrange. Donc, printf obtient ce à quoi il s’attend, et vous voyez 5.0000

Expérience cool de suivi: vous voulez voir autre chose de chouette ? Essayer

 union data { int i; float f; char ch[4]; }; union data d; di = 5; printf("How 5 is represented in memory as an integer:\n"); printf("0x%X 0x%X 0x%X 0x%X\n", v.ch[0], v.ch[1], v.ch[2], v.ch[3]); df = 5.0; printf("How 5 is represented in memory as a float:\n"); printf("0x%X 0x%X 0x%X 0x%X\n", v.ch[0], v.ch[1], v.ch[2], v.ch[3]); // OUTPUT: // How 5 is represented in memory as an integer: // 0x5 0x0 0x0 0x0 // How 5 is represented in memory as a float: // 0x0 0x0 0xFFFFFFA0 0x40 

Vous pouvez réellement voir comment le compilateur C modifie les données pour vous (en supposant que cela fonctionne …)

C’est encore plus intéressant. Je l’ai testé sur trois machines Linux différentes et obtenu deux résultats différents pour la deuxième ligne de sortie:

 gcc 4.5.1 32bit: a is -0.000000 gcc 4.5.1 -Ox 32bit: a is 0.000000 gcc 4.5.1 64bit: a is 0.000000 gcc 4.6.2 64bit: a is 0.000000