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

Cours complet pour apprendre à programmer en D


précédentsommairesuivant

16. Nombres entiers et opérations arithmétiques

Nous avons vu que les instructions if et while permettent aux programmes de prendre des décisions en utilisant le type bool sous forme d'expressions logiques. Dans ce chapitre, nous allons voir les opérations arithmétiques sur les types entiers du D. Ces fonctionnalités nous permettront d'écrire des programmes beaucoup plus utiles.

Même si les opérations arithmétiques font partie de notre vie quotidienne et sont en fait simples, il y a des concepts très importants dont un programmeur doit être conscient pour produire des programmes corrects : taille en bits d'un type, débordements, soupassements, et troncations.

Avant d'aller plus loin, je voudrais résumer les opérations arithmétiques dans le tableau suivant comme référence :

Opérateur

Effet

Exemple

++

incrémente

++variable

--

décrémente

--variable

+

fait la somme de deux valeurs

premier + second

-

soustrait second à premier

premier - second

*

fait le produit de deux valeurs

premier * second

/

divise premier par second

premier / second

%

donne le reste de la division de premier par second

premier % second

^^

élève premier à la puissance second (multiplie premier par lui-même second fois)

premier ^^ second

La plupart de ces opérateurs ont leurs homologues qui ont un signe = collé à eux : +=, -=, *=, %=, ^^=. La différence avec ces opérateurs est qu'ils affectent le résultat à la variable située à gauche de l'opérateur :

 
Sélectionnez
variable += 10;

Cette expression ajoute la valeur de variable et 10 et enregistre le résultat dans variable. À la fin, la valeur de variable est augmentée de 10. C'est l'équivalent de l'expression :

 
Sélectionnez
variable = variable + 10;

Je souhaiterai aussi résumer ici trois concepts importants avant de les développer plus.

  • Dépassement : toutes les valeurs ne peuvent pas entrer dans un type, dans certain cas il y a dépassement (ou débordement). Par exemple, une variable de type ubyte ne peut contenir que des valeurs entre 0 et 255 ; quand on lui affecte 260, la valeur déborde et la variable contient 4.
  • Soupassement : de façon similaire, des valeurs ne peuvent pas être plus petites que la valeur minimum qu'un type peut contenir.
  • Troncation : les types entiers ne peuvent pas avoir de valeurs contenant des parties fractionnaires. Par exemple, la valeur de l'expression entière 3/2 n'est pas 1,5 mais 1.

16-1. Information supplémentaire

Nous rencontrons des opérations arithmétiques quotidiennement sans trop de surprises : si une baguette vaut 1 €, deux baguettes valent 2 € ; si quatre sandwichs valent 15 €, un sandwich vaut 3,75 €, etc.

Malheureusement, les choses ne sont pas aussi simples avec les opérations arithmétiques des ordinateurs. Si on ne comprend pas comment les valeurs sont stockées dans un ordinateur, on peut être surpris de voir que la dette d'une compagnie est réduite à 1,7 milliard de dollars quand elle emprunte 3 nouveaux milliards en plus de sa dette existante de 3 milliards de dollars !
Ou quand, alors qu'une boîte de glace satisfait quatre enfants, une opération arithmétique peut prétendre que deux boîtes de glace suffiraient à onze enfants !

Les programmeurs doivent comprendre comment les entiers sont stockés dans un ordinateur.

16-1-1. Types entiers

Les types entiers sont les types qui ne peuvent garder que des valeurs entières comme -2, 0, 10, etc. Ces types ne peuvent pas avoir de parties fractionnaires comme dans le nombre 2,5. Les types entiers que nous avons vus dans le chapitre sur les types fondamentauxTypes fondamentaux sont :

Type

Nombre de bits

Valeur initiale

byte

8

0

ubyte

8

0

short

16

0

ushort

16

0

Int

32

0

uint

32

0

long

64

0L

ulong

64

0L

Le u au début du type veut dire « unsigned » (non signé) et indique que ces types ne peuvent pas contenir de valeurs inférieures à zéro.

16-1-2. Nombre de bits d'un type

Dans les systèmes informatiques actuels, la plus petite unité d'information est appelée un bit. Au niveau physique, un bit est représenté par des signaux électriques à certains endroits des circuits d'un ordinateur. Un bit peut être dans un ou deux états qui correspondent à la présence et à l'absence de signaux électriques au point qui définit un bit particulier. Ces deux états sont définis arbitrairement aux valeurs 0 et 1. De ce fait, un bit peut avoir une de ces deux valeurs.

Comme il n'existe pas trente-six choses pouvant être représentées par deux états seulement, le type bit n'est pas très utile. Il ne peut l'être que pour des concepts ayant deux états comme « pile ou face » ou une lumière qui peut être allumée ou éteinte.

Si on considère deux bits à la fois, le nombre total d'informations qui peuvent être représentées est multiplié. Comme chaque bit vaut 0 ou 1, il y a un nombre total de quatre états possibles. En supposant que le chiffre de gauche représente le premier bit et que le chiffre de droite représente le deuxième bit, ces états sont 00, 01, 10 et 11. Ajoutons encore un bit pour mieux voir cet effet : trois bits peuvent être dans huit états différents : 000, 001, 010, 011, 100, 101, 110, 111. Comme on peut le voir, chaque bit ajouté double le nombre total d'états qui peuvent être représentés.

Les valeurs auxquelles ces huit états correspondent sont définies par convention. Le tableau suivant montre ces valeurs pour les représentations signées et non signées de 3 bits :

Etat binaire

Valeur non signée

Valeur signée

000

0

0

001

1

1

010

2

2

011

3

3

100

4

-4

101

5

-3

110

6

-2

111

7

-1

On peut écrire le tableau suivant en ajoutant plus de bits :

Bits

nombre de valeurs distinctes

Type D

Valeur minimale

Valeur maximale

1

2

     

2

4

     

3

8

     

4

16

     

5

32

     

6

64

     

7

128

     

8

256

byte

-128

127

ubyte

0

255

16

65 536

short

-32768

32767

ushort

0

65535

32

4 294 967 296

int

-2147483648

2147483647

uint

0

4294967295

64

18 446 744 073 709 551 616

long

-9223372036854775808

9223372036854775807

ulong

0

18446744073709551615

J'ai sauté beaucoup de lignes dans le tableau, et indiqué les versions signées et non signées des type D qui ont le même nombre de bits sur la même ligne (par exemple int et uint sont tous les deux sur la ligne des 32 bits).

16-1-3. Choisir un type

Comme un type 3 bits ne peut avoir que 8 valeurs distinctes, il ne peut représenter que des variables comme une face d'un dé ou le nombre de jours dans une semaine. (Ce n'est qu'un exemple, il n'y a pas de type 3 bits en D).

D'un autre côté, même si uint est un type très grand, il ne peut pas représenter un numéro qui identifierait chaque personne vivante puisque sa valeur maximum est inférieure à la population mondiale de sept milliards d'habitants. long et ulong sont eux plus que suffisants pour représenter beaucoup de quantités.

En règle générale, tant qu'il n'y a aucune raison spécifique de ne pas le faire, vous pouvez utiliser int pour les valeurs entières.

16-1-4. Dépassement

Le fait que les types ne peuvent avoir qu'un nombre limité de valeurs peut causer des effets inattendus. Par exemple, même si ajouter deux uint valant 3 milliards chacun devrait donner 6 milliards, comme la somme est plus grande que la valeur maximale qu'une variable uint peut stocker (environ 4 milliards), cette somme déborde. Sans aucun avertissement, seule la différence entre 6 et 4 milliards est stockée (un peu plus précisément, 6 moins 4,3 milliards).

16-1-5. Troncation

Comme les entiers ne peuvent pas avoir de valeurs contenant des parties fractionnaires, ils perdent la partie située après la virgule. Par exemple, en supposant qu'une boîte de glace suffit à quatre enfants, même si onze enfants avaient besoin de 2,75 boîtes, 2,75 ne peut être stocké que comme 2 dans un type entier.

Nous verrons des techniques basiques pour aider à réduire les effets de débordements, soupassements et troncations plus loin dans le chapitre.

16-1-6. .min et .max

Nous utiliserons les propriétés min et max plus loin, celles que nous avons vues dans le chapitre sur les Types Fondamentaux . Ces propriétés donnent les valeurs minimales et maximales qu'un type entier peut avoir.

16-1-7. Incrémentation: ++

Cet opérateur est utilisé avec une seule variable (plus généralement, avec une seule expression) et est écrit avant le nom de la variable.

Il incrémente la valeur de cette variable de 1 :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int nombre = 10;
   ++nombre;
   writeln("Nouvelle valeur : ", nombre);
}

Sortie :

 
Sélectionnez
Nouvelle valeur : 11

L'opérateur d'incrémentation est équivalent à l'utilisation de l'opérateur ajouter-et-affecter avec la valeur 1 :

 
Sélectionnez
nombre += 1;      // pareil que ++nombre

Si le résultat de l'opérateur d'incrémentation est plus grand que la valeur maximum du type de la variable, le résultat déborde et devient la valeur minimale. On peut voir cet effet en incrémentant une variable qui a la valeur int.max :

 
Sélectionnez
import std.stdio;
 
void main()
{
    writeln("valeur int maximum   : ", int.min);
     writeln("valeur int maximum   : ", int.max);
 
    int nombre = int.max;
    writeln("avant l'incrémentation : ", nombre);
    ++nombre;
    writeln("après l'incrémentation : ", nombre);
}

La valeur devient int.min après l'incrémentation.

Sortie :

 
Sélectionnez
valeur int minimum   : -2147483648
valeur int maximum   : 2147483647
avant l'incrémentation : 2147483647
après l'incrémentation : -2147483648

C'est une observation très importante parce que la valeur passe du maximum au minimum après une incrémentation sans aucun avertissement ! Ce phénomène est appelé débordement (overflow). Nous verrons des effets similaires avec d'autres opérations.

16-1-8. Décrémentation : --

Cet opérateur est similaire à l'opérateur d'incrémentation ; la différence est que la valeur est diminuée de 1 :

 
Sélectionnez
--nombre;   // La valeur est diminuée de 1

L'opération de décrémentation est équivalente à l'utilisation de l'opérateur soustraire-puis-affecter avec la valeur 1 :

 
Sélectionnez
nombre -= 1;      // pareil que --nombre

De manière similaire à l'opérateur ++, si la valeur de la variable est la valeur minimum, elle devient la valeur maximum. Ce phénomène est appelé soupassement (underflow).

16-1-9. Addition : +

Cet opérateur est utilisé avec deux expressions et ajoute leur valeur :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int premier = 12;
   int second = 100;
 
   writeln("Résultat : ", premier + second);
   writeln("Avec une expression constante : ", 1000 + second);
}

Sortie :

 
Sélectionnez
Résultat : 112
Avec une expression constante : 1100

De même, si le résultat est plus grand que la somme des deux expressions, il déborde et devient inférieur à la somme des deux expressions :

 
Sélectionnez
import std.stdio;
 
void main()
{
   // 3 milliards chacun
   uint premier = 3000000000;
   uint second = 3000000000;
 
   writeln("valeur minimum de uint : ", uint.max);
   writeln("               premier : ", premier);
   writeln("                second : ", second);
   writeln("                 somme : ", premier + second);
   writeln("DÉBORDEMENT ! Le résultat n'est pas 6 milliards !");
}

Sortie :

 
Sélectionnez
valeur minimum de uint : 4294967295
               premier : 3000000000
                second : 3000000000
                 somme : 1705032704
DÉBORDEMENT ! Le résultat n'est pas 6 milliards !

16-1-10. Soustraction : -

Cet opérateur est utilisé avec deux expressions et donne la différence entre la première et la seconde :

 
Sélectionnez
import std.stdio;
 
void main()
{
    int nombre_1 = 10;
    int nombre_2 = 20;
 
    writeln(nombre_1 - nombre_2);
    writeln(nombre_2 - nombre_1);
}

Sortie :

 
Sélectionnez
-10
10

Si le résultat d'une soustraction est inférieur à zéro, ce qu'on obtient en stockant ce résultat dans un type non signé est encore une fois surprenant. Réécrivons le programme en utilisant le type uint :

 
Sélectionnez
import std.stdio;
 
void main()
{
   uint nombre_1 = 10;
   uint nombre_2 = 20;
 
   writeln("PROBLÈME ! uint ne peut pas stocker de valeurs négatives :");
   writeln(nombre_1 - nombre_2);
   writeln(nombre_2 - nombre_1);
}

Sortie :

 
Sélectionnez
PROBLÈME ! uint ne peut pas stocker de valeur négatives :
4294967286
10

On peut recommander d'utiliser des types signés pour représenter des choses qui pourraient être soustraites. Tant qu'il n'y a pas de raison spécifique de ne pas le faire, vous pouvez choisir int.

16-1-11. Multiplication : *

Cet opérateur multiplie les valeurs de deux expressions :

 
Sélectionnez
import std.stdio;
 
void main()
{
   uint nombre_1 = 6;
   uint nombre_2 = 7;
 
   writeln(nombre_1 * nombre_2);
}

Sortie :

 
Sélectionnez
42

Le résultat peut être, encore une fois, sujet au débordement.

16-1-12. Division : /

Cet opérateur divise la première expression par la seconde. Comme les types entiers ne peuvent pas avoir de partie fractionnaire, la partie fractionnaire est abandonnée. Cet effet est appelé troncation. De ce fait, le programme suivant affiche 3 et non 3.5.

 
Sélectionnez
import std.stdio;
 
void main()
{
   writeln(7 / 2);
}

Sortie :

 
Sélectionnez
3

Pour les calculs où les parties fractionnaires importent, les types à virgule flottante doivent être utilisés à la place des entiers. Nous verrons les types flottants dans le chapitre suivant.

16-1-13. Modulo : %

Cet opérateur divise la première expression par la seconde et donne le reste de cette division :

 
Sélectionnez
import std.stdio;
 
void main()
{
   writeln(10 % 6);
}

Sortie :

 
Sélectionnez
4

Une utilisation fréquente de cet opérateur est de déterminer si un nombre est pair ou impair. Comme le reste de la division d'un nombre par 2 est toujours 0 et le reste de la division d'un nombre impair par 2 est toujours 1, comparer la valeur à 0 est suffisant pour déterminer la parité d'un nombre :

 
Sélectionnez
if ((nombre % 2) == 0) {
   writeln("nombre pair");
 
} else {
   writeln("nombre impair");
}

16-1-14. Puissance : ^^

Cet opérateur élève la première expression à la puissance de la seconde. Par exemple, pour élever 3 à la puissance 4 :

 
Sélectionnez
import std.stdio;
 
void main()
{
   writeln(3 ^^ 4); // équivalent à 3*3*3*3
}

Sortie :

 
Sélectionnez
81

16-1-15. Opérations arithmétiques avec affectation

Tous les opérateurs qui prennent deux expressions ont un équivalent d'affectation. Ces opérateurs affectent le résultat à l'expression qui est à sa gauche :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int nombre = 10;
 
   nombre += 20;   // équivaut à nombre = nombre + 20 ; maintenant 30
   nombre -= 5;    // équivaut à nombre = nombre - 5 ;  maintenant 25
   nombre *= 2;    // équivaut à nombre = nombre * 2 ;  maintenant 50
   nombre /= 3;    // équivaut à nombre = nombre / 3 ;  maintenant 16
   nombre %= 7;    // équivaut à nombre = nombre % 7 ;  maintenant  2
   nombre ^^= 6;   // équivaut à nombre = nombre ^^ 6 ; maintenant 64
 
   writeln(nombre);
}

Sortie :

 
Sélectionnez
64

16-1-16. Négation : -

Cet opérateur change le signe de la valeur de l'expression (négatif devient positif et inversement) :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int nombre_1 = 1;
   int nombre_2 = -2;
  
   writeln(-nombre_1);
   writeln(-nombre_2);
}

Sortie :

 
Sélectionnez
-1
2

Le type du résultat de cette opération est le même que le type de l'expression. Comme les types non signés ne peuvent pas stocker de valeurs négatives, utiliser cet opérateur sur des types non signés peut mener à des résultats surprenants :

 
Sélectionnez
uint nombre = 1;
writeln("négation : ", -nombre);

le type de -nombre est uint également, et celui-ci ne peut pas représenter des valeurs négatives :

Sortie :

 
Sélectionnez
négation: 4294967295

16-1-17. Signe plus : +

Cet opérateur n'a pas d'effet et n'existe que pour avoir une certaine symétrie avec l'opérateur de négation. Les valeurs positives restent positives et les valeurs négatives restent négatives :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int nombre_1 = 1;
   int nombre_2 = -2;
 
   writeln(+nombre_1);
   writeln(+nombre_2);
}

Sortie :

 
Sélectionnez
1
-2

16-1-18. Postincrémentation : ++

Sauf s'il y a une très bonne raison de le faire, utilisez toujours l'opérateur d'incrémentation usuel (qui est également appelé opérateur de préincrémentation).

Contrairement à l'opérateur de préincrémentation, il est placé après l'expression. Il incrémente également la valeur de l'expression de 1. La différence est que l'opérateur de postincrémentation retourne l'ancienne valeur de l'expression, et non la nouvelle. Pour voir cette différence, comparons-la avec l'opérateur de préincrémentation :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int incrémenté_normalement = 1;
   writeln(++incrémenté_normalement);  // affiche 2
   writeln(incrémenté_normalement);    // affiche 2
 
   int post_incrémenté = 1;
 
   // incrémenté, mais l'ancienne valeur est utilisée :
   writeln(post_incrémenté++);         // affiche 1
   writeln(post_incrémenté);           // affiche 2
}

Sortie :

 
Sélectionnez
2
2
1
2

L'instruction writeln(post_incrémenté++); ci-dessus est équivalente à ce code :

 
Sélectionnez
int ancienne_valeur = post_incrémenté;
++post_incrémenté;
writeln(ancienne_valeur);   // affiche 1

16-1-19. Postdécrémentation : --

Sauf s'il y a une bonne raison, utilisez toujours l'opérateur de décrémentation usuel (aussi appelé opérateur de prédécrémentation).

Se comporte de la même manière que l'opérateur de postincrémentation, mais décrémente.

16-1-20. Priorité opératoire

Les opérateurs que nous avons vus jusqu'alors ont toujours été utilisés tout seuls, avec seulement une ou deux expressions. Cependant, comme les expressions logiques, il est fréquent de combiner ces opérateurs pour former des expressions arithmétiques plus complexes :

 
Sélectionnez
int valeur     = 77;
int résultat = (((valeur + 8) * 3) / (valeur - 1)) % 5;

Comme pour les opérateurs logiques, les opérateurs arithmétiques obéissent aussi à des règles de priorité. Par exemple, l'opérateur * est prioritaire sur l'opérateur +. Pour cette raison, quand il n'y a pas de parenthèses (par exemple dans l'expression valeur + 8 * 3), l'opérateur * est évalué avant l'opérateur +. De ce fait, cette expression vaut valeur + 24, ce qui est différent de (valeur + 8) * 3.

Utiliser des parenthèses sert à la fois à assurer des résultats corrects et à communiquer le but d'un code aux programmeurs qui pourraient à l'avenir travailler sur le code.

16-1-21. Solution potentielle contre les débordements

Si le résultat d'une opération ne peut pas être contenu dans le type du résultat, alors il n'y a rien qui puisse être fait. Parfois, alors que le résultat final pourrait être contenu dans un certain type, les résultats intermédiaires pourraient déborder et mener à des résultats incorrects.

Par exemple, supposons que nous ayons besoin de planter un pommier par 1000 m² d'une aire de 40 par 60 km. De combien de pommiers avons-nous besoin ?

Quand on résout ce problème sur papier, on voit que le résultat est kitxmlcodeinlinelatexdvp\frac{40000x60000}{1000}finkitxmlcodeinlinelatexdvp, ce qui donne 2,4 millions de pommiers. Écrivons un programme qui effectue ce calcul :

 
Sélectionnez
import std.stdio;
 
void main()
{
   int largeur   = 40000;
   int longueur = 60000;
   int airParArbre = 1000;
 
   int arbresNecessaires = largeur * longueur / airParArbre;
 
   writeln("Nombre d'arbres nécessaires : ", arbresNecessaires);
}

Sortie :

 
Sélectionnez
Nombre d'arbres nécessaires : -1894967

Non seulement ce n'est pas proche du résultat, mais on obtient même un nombre négatif ! Dans ce cas précis, le calcul intermédiaire largeur * longueur déborde et le calcul suivant / airParArbre donne un résultat incorrect.

Une façon d'éviter le débordement dans cet exemple est de changer l'ordre des opérations :

 
Sélectionnez
int arbresNecessaires = largeur / airParArbre * longueur ;

Le résultat est maintenant correct :

 
Sélectionnez
Nombre d'arbres nécessaires : 2400000

La raison pour laquelle cette méthode fonctionne est le fait que chaque étape du calcul tient dans le type int.

Notez que ce n'est pas une solution satisfaisante parce que cette fois la valeur intermédiaire est sujette à troncation, qui pourrait affecter le résultat de façon significative dans certains ordres de calcul.

Une autre solution serait d'utiliser un type flottant à la place d'un type entier : float, double ou real.

16-1-22. Solution potentielle contre la troncation

Changer l'ordre des opérateurs peut aussi être une solution contre la troncation. Un exemple intéressant peut être observé en divisant et en multipliant une valeur avec le même nombre. On s'attendrait à ce que 10/9*9 donne 10, mais on obtient 9 :

 
Sélectionnez
import std.stdio;
 
void main()
{
   writeln(10 / 9 * 9);
}

Sortie :

 
Sélectionnez
9

Le résultat est correct quand la troncation est évitée en changeant l'ordre des opérations :

 
Sélectionnez
writeln(10 * 9 / 9);

Sortie :

 
Sélectionnez
10

Encore une fois, ce n'est pas une solution satisfaisante : cette fois, le calcul intermédiaire pourrait être sujet au débordement. Utiliser un type flottant peut être une autre solution à la troncation dans certains calculs.

16-1-23. Exercices

  • Écrire un programme qui demande deux entiers à l'utilisateur, affiche le quotient entier d'une division du premier et du second, et affiche aussi le reste. Par exemple, quand 7 et 3 sont entrés, le programme affiche l'équation suivante :
 
Sélectionnez
7 = 3 * 2 + 1
  • Modifier le programme pour donner une sortie plus courte quand le reste est 0. Par exemple, quand 10 et 5 sont entrés, il ne devrait pas afficher « 10 = 5 * 2 + 0 » mais ceci :
 
Sélectionnez
10 = 5 * 2
  • Écrire une calculette simple qui supporte les quatre opérations arithmétiques basiques. L'utilisateur choisit l'opération à effectuer à partir d'un menu et le programme effectue l'opération sur les deux valeurs qui ont été entrées. Vous pouvez ignorer les débordements et les troncations dans ce programme.
  • Écrire un programme qui affiche les nombres de 1 à 10, chacun sur une ligne propre, à l'exception de la valeur 7. N'utilisez pas des lignes répétées comme cela :

     
    Sélectionnez
    import std.stdio;
     
    void main()
    {
       // Ne faîtes pas cela !
       writeln(1);
       writeln(2);
       writeln(3);
       writeln(4);
       writeln(5);
       writeln(6);
       writeln(8);
       writeln(9);
       writeln(10);
    }
  • Utilisez plutôt une variable dont la valeur est incrémentée dans une boucle. Vous pourriez avoir besoin de l'opérateur != ici.

Solutions.


précédentsommairesuivant