IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Cours complet pour apprendre à programmer en D


précédentsommairesuivant

17. Types à virgule flottante

Dans le chapitre précédent, nous avons vu que malgré leur facilité d'utilisation, les opérations arithmétiques sur les entiers sont sujettes à des erreurs de programmation à cause des débordements, des soupassements et des troncations. Nous avons aussi vu que les entiers ne peuvent pas représenter des valeurs avec des parties fractionnaires comme 1,25.

Les types flottants sont spécialement conçus pour prendre en charge les parties fractionnaires. La « virgule » dans leur nom vient de la virgule qui sépare partie entière et partie fractionnaire et « flottant » fait référence à la manière avec laquelle ces types sont implémentés : la virgule flotte à droite ou à gauche de façon appropriée. (Ce détail n'est pas important pour l'utilisation de ces types.)

De même que pour les entiers, nous devons voir des détails importants dans ce chapitre. Avant tout, voici une liste de quelques aspects intéressants des types flottants :

  • ajouter 0,001 un millier de fois n'est pas pareil qu'ajouter 1 ;
  • utiliser les opérateurs == et != avec des types flottants est une erreur dans la plupart des cas ;
  • la valeur initiale d'un type flottant est .nan, et non 0. .nan ne peut pas être utilisé dans des expressions. Quand .nan est utilisé dans des opérations de comparaison, il n'est ni plus petit, ni plus grand que n'importe quelle valeur ;
  • la valeur de débordement est .infinity et la valeur de soupassement est .infinity négatif.

Même si les types flottants sont plus utiles dans certains cas, ils ont des singularités que tous les programmeurs doivent connaître. Par rapport aux entiers, ils sont très bons pour éviter les troncations parce que leur but principal est de prendre en charge les valeurs fractionnaires. Comme n'importe quel autre type, basés sur un certain nombre de bits, ils sont également sujets aux dépassements et soupassements, mais comparés aux entiers, l'ensemble des valeurs qu'ils prennent en charge est vaste.

De plus, au lieu d'être silencieux dans le cas de dépassements et de soupassements, ils prennent les valeurs spéciales d'infini positif ou négatif.

Pour rappel, voici les types flottants :

Type

Nombre de bits

Valeur initiale

float

32

float.nan

double

64

double.nan

real

Au moins 64, peut-être plus (par exemple 80, selon ce que le matériel prend en charge)

real.nan

17-1. Attributs des types flottants

Les types flottants ont plus d'attributs que les autres types :

  • .stringof est le nom du type ;
  • .sizeof est la longueur du type en octets (pour avoir le nombre de bits, il faut multiplier cette valeur par 8) ;
  • .max est la valeur maximale qu'un type peut stocker ; l'opposé de cette valeur est la valeur minimale que le type peut avoir ;
  • .min_normal est la plus petite valeur normalisée que ce type peut avoir (le type peut stocker des valeurs plus petites que .min_normal, mais la précision de ces valeurs est moins grande que la précision normale du type) ;
  • .dig (pour digits (chiffres)) indique le nombre de chiffres décimaux significatifs pour la précision du type ;
  • .infinity est la valeur spéciale utilisée pour indiquer un dépassement ou un soupassement.

Notez que la valeur minimale d'un type flottant n'est pas .min, mais l'opposé de .max. Par exemple, la valeur minimale de double est -double.max.

D'autres attributs des types flottants sont utilisés moins souvent. Vous pouvez les voir tous sur cette page (en anglais) : Properties for Floating Point Types.

Les propriétés des types flottants et leurs relations peuvent être vues sur un axe comme celui-ci :

Image non disponible

Les parties avec des tirets sont à l'échelle : le nombre de valeurs qui peuvent être représentées entre min_normal et 1 est égal au nombre de valeurs qui peuvent être représentées entre 1 et max. Cela veut dire que la précision de la partie fractionnaire des valeurs qui sont entre min_normal et 1 est très grande (c'est également vrai pour le côté négatif).

17-2. .nan

Nous avons déjà vu que .nan est la valeur par défaut des variables flottantes. .nan peut apparaître comme résultat d'une expression flottant n'ayant pas de sens. Par exemple, les expressions flottantes du programme suivant produisent toutes double.nan :

 
Sélectionnez
import std.stdio;
 
void main()
{
   double zero = 0;
   double infini = double.infinity;
 
   writeln("n'importe quelle expression avec nan : ", double.nan + 1);
   writeln("zero / zero                          : ", zero / zero);
   writeln("zero * infini                      : ", zero * infini);
   writeln("infini / infini                  : ", infini / infini);
   writeln("infini - infini                  : ", infini - infini);
}

17-3. Écrire des valeurs flottantes

Les valeurs flottantes peuvent simplement être écrites sans point décimal comme 123, ou avec un point décimal comme 12.3 (NDT Pour écrire un nombre à virgule, on n'utilise pas la virgule, mais le point).

Les valeurs flottantes peuvent aussi être écrites avec la syntaxe flottante: 1.23e+4. La partie e+ dans cette syntaxe peut être lue comme « fois 10 puissance ». On lit 1.23e+4 comme ceci « 1,23 fois 10 puissance 4 », qui est pareil que « 1,23 fois 104 », qui est en fait pareil que 1,23×10 000, ce qui vaut 12 300.

Si la valeur après e est négative, comme pour 5.67e-3, alors on lit « divisé par 10 puissance ». Ainsi, pour cet exemple, on lit « 5,67 divisé par 103 », ce qui est pareil que kitxmlcodeinlinelatexdvp\frac{5,67}{1000}finkitxmlcodeinlinelatexdvp, ce qui vaut 0,005 67.

Les valeurs qui sont affichées par le programme suivant sont toutes dans le format flottant. Il affiche les propriétés des trois types :

 
Sélectionnez
import std.stdio;
 
void main()
{
   writeln("Type                       : ", float.stringof);
   writeln("Précision                  : ", float.dig);
   writeln("Valeur minimale normalisée : ", float.min_normal);
   writeln("Valeur maximale            : ", float.max);
   writeln("Valeur minimale            : ", -float.max);
   writeln();
   writeln("Type                       : ", double.stringof);
   writeln("Précision                  : ", double.dig);
   writeln("Valeur minimale normalisée : ", double.min_normal);
   writeln("Valeur maximale            : ", double.max);
   writeln("Valeur minimale            : ", -double.max);
   writeln();
   writeln("Type                       : ", real.stringof);
   writeln("Précision                  : ", real.dig);
   writeln("Valeur minimale normalisée : ", real.min_normal);
   writeln("Valeur maximale            : ", real.max);
   writeln("Valeur minimale            : ", -real.max);
}

La sortie du programme est celle-ci dans mon environnement. Comme real dépend du matériel (NDT Et du système qui tourne dessus), vous pouvez obtenir autre chose.

Sortie :

 
Sélectionnez
Type                       : float
Précision                  : 6
Valeur minimale normalisée : 1.17549e-38
 Valeur maximale            : 3.40282e+38
Valeur minimale            : -3.40282e+38
 
Type                       : double
Précision                  : 15
Valeur minimale normalisée : 2.22507e-308
Valeur maximale            : 1.79769e+308
Valeur minimale            : -1.79769e+308
 
Type                       : real
Précision                  : 18
Valeur minimale normalisée : 3.3621e-4932
Valeur maximale            : 1.18973e+4932
Valeur minimale            : -1.18973e+4932

17-4. Observations

Comme on l'a vu dans le chapitre précédent ; la valeur maximum de ulong a 20 chiffres décimaux : 18 446 744 073 709 551 616. Cette valeur semble petite même comparée au plus petit des types flottants : float peut stocker des valeurs de l'ordre de 1038, par exemple 340 282 000 000 000 000 000 000 000 000 000 000 000.

La valeur maximale de real est de l'ordre de 104932, une valeur de plus de 4900 chiffres décimaux !

Regardons la valeur minimale que double peut représenter avec une précision de 15 chiffres décimaux : 0.000…(il y a 300 zéros additionnels ici)…0000222507385850720.

17-5. Les débordements et les soupassements

Malgré leur capacité de représenter des valeurs très grandes, les types flottants peuvent aussi déborder ou soupasser. Les types flottants sont plus sûrs que les types entiers dans ce domaine parce que les dépassements et les soupassements ne sont pas ignorés. Les valeurs qui débordent deviennent .infinity et les valeurs qui soupassent deviennent -.infinity. Pour l'observer, augmentons la valeur de .max de 10 %. Comme la valeur est déjà le maximum, ça débordera :

 
Sélectionnez
import std.stdio;
 
void main()
{
   real valeur = real.max;
 
   writeln("Avant        : ", valeur);
 
   // Ajouter 10 % est équivalent à multiplier par 1.1
   valeur *= 1.1;
   writeln("10% ajoutés  : ", valeur);
 
    // Essayons de la diminuer en divisant par 2 :
   valeur /= 2;
   writeln("Divisé par 2 : ", valeur);
}

Une fois que la valeur déborde et devient real.infinity, elle y reste même après avoir été divisée par 2.

Sortie :

 
Sélectionnez
Avant        : 1.18973e+4932
10% ajoutés  : inf
Divisé par 2 : inf

17-6. Précision

La précision est une idée omniprésente dans la vie de tous les jours, sans qu'on y prête toujours attention. C'est le nombre de chiffres qui sont utilisés pour écrire une valeur. Par exemple, quand on dit que le tiers de 100 est 33, la précision est de 2 parce que 33 a deux chiffres. Quand la valeur est notée plus précisément comme 33.33, la précision est alors de quatre chiffres.

Le nombre de bits qu'a chaque type flottant n'affecte pas seulement sa valeur maximale, mais aussi sa précision. Plus il y a de bits, plus la précision est grande.

17-7. Il n'y a pas de troncation lors d'une division

Comme nous l'avons vu dans le chapitre précédent, les divisions entières ne conservent pas la partie fractionnaire du résultat :

 
Sélectionnez
int premier = 3;
int second = 2;
writeln(premier / second);

Sortie :

 
Sélectionnez
1

Les types flottants n'ont pas ce problème de troncation ; ils sont spécialement conçus pour préserver les parties fractionnaires :

 
Sélectionnez
double premier = 3;
double second = 2;
writeln(premier / second);

Sortie :

 
Sélectionnez
30/12/99

La précision de la partie fractionnaire dépend de la précision du type : real a la plus grande précision et float la plus petite.

17-8. Quel type utiliser

Sauf s'il y a une raison spécifique de ne pas le faire, vous pouvez utiliser double pour les valeurs flottantes. float a une précision faible, mais parce qu'il est plus petit que les autres types, il peut être utile quand l'espace de stockage est limité.

D'un autre côté, comme la précision de real est plus grande que double sur le même matériel, il sera préférable pour des calculs de haute précision.

17-9. On ne peut pas représenter toutes les valeurs

On ne peut pas représenter certaines valeurs de la vie de tous les jours. Dans le système décimal que nous utilisons quotidiennement, les chiffres avant la virgule représentent les unités, les dizaines, les centaines, etc., et les chiffres après la virgule représentent les dixièmes, les centièmes, les millièmes, etc.

Si une valeur est une combinaison exacte de ces valeurs, elle peut être représentée précisément. Par exemple, du fait que 0.23 consiste en 2 dixièmes et 3 centièmes, cette valeur est représentée précisément. D'un autre côté, la valeur de kitxmlcodeinlinelatexdvp\frac{1}{3}finkitxmlcodeinlinelatexdvp ne peut pas être représenté précisément dans le système décimal, parce quel que soit le nombre de chiffres qui sont écrits, ce n'est jamais suffisant : 0,33333…

C'est très similaire pour les types flottants. Parce que ces types sont basés sur un certain nombre de bits, ils ne peuvent pas représenter toutes les valeurs.

La différence avec le système binaire que l'ordinateur utilise est que les chiffres avant la virgule sont les unités, les deuzaines, les quatraines, etc. et les chiffres après la virgule sont les moitiés, les quarts, les huitièmes, etc. Seules les valeurs qui sont des combinaisons exactes de ces chiffres peuvent être représentées précisément.

Par exemple, on ne peut représenter directement dans le système binaire utilisé par les ordinateurs une valeur telle que 0,1 (comme dans 10 centimes). Alors que cette valeur peut être représentée précisément dans le système décimal, sa représentation binaire ne se finit jamais et répète continuellement 4 chiffres : 0.0001100110011… (en utilisant l'écriture binaire, pas décimale). C'est toujours imprécis à un certain niveau, selon la précision du type flottant utilisé.

Le programme suivant montre ce problème. La valeur d'une variable est incrémentée de 0,001 un millier de fois dans une boucle. De façon étonnante, le résultat n'est pas 1 :

 
Sélectionnez
import std.stdio;
 
void main()
{
   float résultat = 0;
 
   // On s'attendrait à ce que résultat vaille 1 après avoir bouclé 1000 fois :
   while (résultat < 1) {
      résultat += 0.001;
   }
 
   // Vérifions
   if (résultat == 1) {
      writeln("Comme attendu: 1");
 
   } else {
      writeln("DIFFERENT: ", résultat);
   }
}

Sortie :

 
Sélectionnez
DIFFERENT: 1.00099

Parce que 0.001 ne peut pas être représenté précisément, cette imprécision affecte le résultat de multiples fois. La sortie suggère que la boucle a été répétée 1001 fois.

17-10. Comparer des valeurs flottantes

Nous avons vu les comparaisons suivantes pour les entiers : égal à (==), n'est pas égal à (!=), plus grand que (>), inférieur ou égal à (<=) et supérieur ou égal à (>=). Les types flottants ont beaucoup plus d'opérateurs de comparaison.

Comme la valeur spéciale .nan représente des valeurs flottantes invalides, il ne peut être comparé avec aucune autre valeur. Par exemple, il n'y a pas de sens à demander lequel de .nan ou de 1 est plus grand que l'autre.

Pour cette raison, les valeurs flottantes ne sont pas toutes ordonnées. Si deux valeurs sont non ordonnées, alors au moins une des valeurs est .nan

Le tableau suivant montre tous les opérateurs de comparaison sur les flottants. Tous les opérateurs sont binaires (ce qui veut dire qu'ils prennent deux opérandes) et sont utilisés comme dans gauche == droite). Les colonnes qui contiennent false et true sont les résultats des opérations de comparaison.

La dernière colonne indique si l'opération fait encore sens lorsque l'un des opérandes est .nan. Par exemple, même si le résultat de 1.2 < real.nan est false, ce résultat n'a pas de sens parce que l'un des opérandes est real.nan. Le résultat de la comparaison inverse real.nan < 1.2 est également fausse. L'abréviation « cg » veut dire « côté gauche », désignant l'expression qui est à gauche de chaque opérateur.

Opérateur

Signification

Si cg est plus grand

si cg est plus petit

S'ils sont égaux

Si l'un des deux est .nan

Sensé avec .nan

==

est égal à

false

false

true

false

oui

!=

n'est pas égal à

true

true

false

true

oui

>

est plus grand que

true

false

false

false

non

=

est supérieur ou égal à

true

false

true

false

non

<

est plut petit que

false

true

false

false

non

<=

est inférieur ou égal à

false

true

true

false

non

!<>=

n'est ni supérieur, ni inférieur, ni égal à

false

false

false

true

oui

<>

plus grand ou plus petit que

true

true

false

false

non

<>=

est inférieur, supérieur, ou égal à

true

true

true

false

non

!<=

n'est ni inférieur, ni égal à

true

false

false

true

oui

!<

n'est pas inférieur à

true

false

true

true

oui

!>=

n'est pas supérieur ou égal à

false

true

false

true

oui

!>

n'est pas plus grand que

false

true

true

true

oui

!<>

n'est ni inférieur, ni supérieur à

false

false

true

true

oui

Notez qu'il est sensé d'utiliser .nan avec n'importe quel opérateur qui contient « n'est pas » dans sa signification et le résultat est toujours vrai. .nan n'étant pas une valeur valide, le résultat de la plupart des comparaisons n'a pas de sens.

17-11. isnan() pour tester .nan

Parce que le résultat est toujours false pour l'égalité avec .nan selon le tableau précédent, il n'est pas possible d'utiliser l'opérateur == pour déterminer si la valeur d'une variable flottante est .nan :

 
Sélectionnez
if (variable == double.nan) {    // ← FAUX
   // ...
}

Pour cette raison, la fonction isnan() du module std.math doit être utilisée :

 
Sélectionnez
import std.math;
// ...
 
   if (isnan(variable)) {      // ← correct
      // ...
   }

De même, pour déterminer si une valeur n'est pas .nan, !isnan() doit être utilisé parce que l'opérateur != donnerait toujours true.

17-12. Exercices

  1. Modifiez la calculatrice du chapitre précédent pour prendre en charge les valeurs flottantes. La nouvelle calculatrice doit fonctionner plus précisément avec cette modification. Quand vous essayerez la calculatrice, vous pourrez entrer des valeurs flottantes dans des formats divers comme 1000, 1.23 et 1.23e4.
  2. Écrivez un programme qui lit cinq valeurs flottantes depuis l'entrée. Le programme doit afficher le double de ces valeurs et ensuite le cinquième de ces valeurs. Cet exercice est une introduction à l'idée des tableaux du chapitre suivant. Si vous écrivez ce programme avec ce que vous avez vu jusqu'à maintenant, vous comprendrez les tableaux et vous vous les approprierez plus facilement.

SolutionsTypes à virgule flottante - Correction.


précédentsommairesuivant