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 :
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 :
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 :
import
std.stdio;
void
main
(
)
{
int
nombre =
10
;
++
nombre;
writeln
(
"Nouvelle valeur : "
, nombre);
}
Sortie :
Nouvelle valeur : 11
L'opérateur d'incrémentation est équivalent à l'utilisation de l'opérateur ajouter-et-affecter avec la valeur 1 :
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 :
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 :
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 :
--
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 :
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 :
import
std.stdio;
void
main
(
)
{
int
premier =
12
;
int
second =
100
;
writeln
(
"Résultat : "
, premier +
second);
writeln
(
"Avec une expression constante : "
, 1000
+
second);
}
Sortie :
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 :
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 :
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 :
import
std.stdio;
void
main
(
)
{
int
nombre_1 =
10
;
int
nombre_2 =
20
;
writeln
(
nombre_1 -
nombre_2);
writeln
(
nombre_2 -
nombre_1);
}
Sortie :
-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 :
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 :
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 :
import
std.stdio;
void
main
(
)
{
uint
nombre_1 =
6
;
uint
nombre_2 =
7
;
writeln
(
nombre_1 *
nombre_2);
}
Sortie :
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.
import
std.stdio;
void
main
(
)
{
writeln
(
7
/
2
);
}
Sortie :
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 :
import
std.stdio;
void
main
(
)
{
writeln
(
10
%
6
);
}
Sortie :
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 :
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 :
import
std.stdio;
void
main
(
)
{
writeln
(
3
^^
4
); // équivalent à 3*3*3*3
}
Sortie :
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 :
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 :
64
16-1-16. Négation : -▲
Cet opérateur change le signe de la valeur de l'expression (négatif devient positif et inversement) :
import
std.stdio;
void
main
(
)
{
int
nombre_1 =
1
;
int
nombre_2 =
-
2
;
writeln
(-
nombre_1);
writeln
(-
nombre_2);
}
Sortie :
-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 :
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 :
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 :
import
std.stdio;
void
main
(
)
{
int
nombre_1 =
1
;
int
nombre_2 =
-
2
;
writeln
(+
nombre_1);
writeln
(+
nombre_2);
}
Sortie :
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 :
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 :
2
2
1
2
L'instruction writeln(post_incrémenté++); ci-dessus est équivalente à ce code :
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 :
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 :
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 :
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 :
int
arbresNecessaires =
largeur /
airParArbre *
longueur ;
Le résultat est maintenant correct :
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 :
import
std.stdio;
void
main
(
)
{
writeln
(
10
/
9
*
9
);
}
Sortie :
9
Le résultat est correct quand la troncation est évitée en changeant l'ordre des opérations :
writeln
(
10
*
9
/
9
);
Sortie :
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 :
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 :
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électionnezimport
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.