web-dev-qa-db-fra.com

Résultat non intuitif de l'affectation d'un nombre à double précision à une variable int dans C

Quelqu'un pourrait-il m'expliquer pourquoi j'obtiens deux numéros différents, resp. 14 et 15, en tant que sortie du code suivant?

#include <stdio.h>  

int main()
{
    double Vmax = 2.9; 
    double Vmin = 1.4; 
    double step = 0.1; 

    double a =(Vmax-Vmin)/step;
    int b = (Vmax-Vmin)/step;
    int c = a;

    printf("%d  %d",b,c);  // 14 15, why?
    return 0;
}

Je m'attends à obtenir 15 dans les deux cas, mais il semble que je manque certains principes fondamentaux de la langue.

Je ne sais pas si c'est pertinent mais je faisais le test dans CodeBlocks. Cependant, si je tape les mêmes lignes de code dans un compilateur en ligne ( celui-ci par exemple ) j'obtiens une réponse de 15 pour les deux variables imprimées.

47
GeorgiD

... pourquoi j'obtiens deux numéros différents ...

Mis à part les problèmes habituels de virgule flottante, les chemins de calcul vers b et c sont arrivés de différentes manières. c est calculé en enregistrant d'abord la valeur sous double a.

double a =(Vmax-Vmin)/step;
int b = (Vmax-Vmin)/step;
int c = a;

C permet de calculer des mathématiques en virgule flottante intermédiaires en utilisant des types plus larges. Vérifiez la valeur de FLT_EVAL_METHOD Dans <float.h>.

Sauf pour l'affectation et la distribution (qui suppriment toute la plage et la précision supplémentaires), ...

-1 indéterminable;

0 évaluer toutes les opérations et constantes uniquement à la plage et à la précision du type;

1 évalue les opérations et les constantes de type float et double à la plage et à la précision du type double, évalue long double Aux opérations et aux constantes à la plage et à la précision du type long double;

2 évaluez toutes les opérations et constantes de la plage et de la précision du type long double.

C11dr §5.2.4.2.2 9

OP rapporté 2

En enregistrant le quotient dans double a = (Vmax-Vmin)/step;, la précision est forcée à double tandis que int b = (Vmax-Vmin)/step; pourrait calculer comme long double.

Cette différence subtile résulte du fait que (Vmax-Vmin)/step (Calculé peut-être comme long double) Est enregistré en tant que double par rapport à un long double. L'un comme 15 (ou juste au-dessus), et l'autre juste en dessous de 15. int la troncature amplifie cette différence à 15 et 14.

Sur un autre compilateur, les résultats peuvent avoir tous deux été les mêmes en raison de FLT_EVAL_METHOD < 2 Ou d'autres caractéristiques à virgule flottante.


La conversion en int d'un nombre à virgule flottante est sévère avec des nombres proches d'un nombre entier. Souvent préférable de round() ou lround(). La meilleure solution dépend de la situation.

42
chux

C'est en effet une question intéressante, voici ce qui se passe précisément dans votre matériel. Cette réponse donne les calculs exacts avec la précision des flottants de précision IEEE double, soit une mantisse de 52 bits plus un bit implicite. Pour plus de détails sur la représentation, voir article wikipedia .

Ok, donc vous définissez d'abord quelques variables:

double Vmax = 2.9;
double Vmin = 1.4;
double step = 0.1;

Les valeurs respectives en binaire seront

Vmax =    10.111001100110011001100110011001100110011001100110011
Vmin =    1.0110011001100110011001100110011001100110011001100110
step = .00011001100110011001100110011001100110011001100110011010

Si vous comptez les bits, vous verrez que j'ai donné le premier bit réglé plus 52 bits à droite. C'est exactement la précision à laquelle votre ordinateur stocke un double. Notez que la valeur de step a été arrondie.

Maintenant, vous faites quelques calculs sur ces chiffres. La première opération, la soustraction, donne le résultat précis:

 10.111001100110011001100110011001100110011001100110011
- 1.0110011001100110011001100110011001100110011001100110
--------------------------------------------------------
  1.1000000000000000000000000000000000000000000000000000

Ensuite, vous divisez par step, qui a été arrondi par votre compilateur:

   1.1000000000000000000000000000000000000000000000000000
 /  .00011001100110011001100110011001100110011001100110011010
--------------------------------------------------------
1110.1111111111111111111111111111111111111111111111111100001111111111111

En raison de l'arrondissement de step, le résultat est un peu inférieur à 15. Contrairement à avant, je n'ai pas arrondi immédiatement, car c'est précisément là que se passe la chose intéressante: votre CPU peut en effet stocker des nombres à virgule flottante d'une plus grande précision qu'un double, donc l'arrondi n'a pas lieu immédiatement.

Ainsi, lorsque vous convertissez le résultat de (Vmax-Vmin)/step Directement en un int, votre CPU coupe simplement les bits après le point fractionnel (c'est ainsi que la conversion implicite double -> int Est définie selon les normes linguistiques):

               1110.1111111111111111111111111111111111111111111111111100001111111111111
cutoff to int: 1110

Cependant, si vous enregistrez d'abord le résultat dans une variable de type double, l'arrondi a lieu:

               1110.1111111111111111111111111111111111111111111111111100001111111111111
rounded:       1111.0000000000000000000000000000000000000000000000000
cutoff to int: 1111

Et c'est précisément le résultat que vous avez obtenu.

24
cmaster

La réponse "simple" est que ces nombres apparemment simples 2.9, 1.4 et 0.1 sont tous représentés en interne sous forme de virgule flottante binaire, et en binaire, le nombre 1/10 est représenté comme la fraction binaire à répétition infinie 0.00011001100110011 ... [ 2] . (Ceci est analogue à la façon dont 1/3 en décimal finit par être 0,333333333 ....) Convertis en décimal, ces nombres originaux finissent par être des choses comme 2,8999999999, 1,3999999999 et 0,0999999999. Et lorsque vous effectuez des calculs supplémentaires sur eux, ces .0999999999 ont tendance à proliférer.

Et puis le problème supplémentaire est que le chemin par lequel vous calculez quelque chose - que vous le stockiez dans des variables intermédiaires d'un type particulier, ou que vous le calculiez "tout à la fois", ce qui signifie que le processeur peut utiliser des registres internes avec une plus grande précision que le type double - peut finir par faire une différence significative.

En fin de compte, lorsque vous convertissez un double en int, vous voulez presque toujours arrondir , pas tronqué. Ce qui s'est passé ici, c'est que (en fait) un chemin de calcul vous a donné 15,0000000001 qui a été tronqué à 15, tandis que l'autre vous a donné 14,999999999 qui a été tronqué jusqu'à 14.

Voir aussi question 14.4a dans la C FAQ list .

21
Steve Summit

Un problème équivalent est analysé dans analyse des programmes C pour FLT_EVAL_METHOD == 2 .

Si FLT_EVAL_METHOD==2:

double a =(Vmax-Vmin)/step;
int b = (Vmax-Vmin)/step;
int c = a;

calcule b en évaluant un long double expression puis la tronquant en int, tandis que pour c elle évalue à partir de long double, en le tronquant à double puis à int.

Les deux valeurs ne sont donc pas obtenues avec le même processus, ce qui peut conduire à des résultats différents car les types flottants ne fournissent pas l'arithmétique exacte habituelle.

6
Jean-Baptiste Yunès