Licence CC BY-NC-SA

Les boucles

Publié :

Nous avons terminé le chapitre sur les conditions qui était quelque peu ardu. Maintenant nous allons nous intéresser à un nouveau problème : nos programmes auront souvent besoin de répéter plusieurs fois la même action et il est hors de question de jouer du copier/coller, d'autant plus que le nombre de répétitions ne sera pas nécessairement connu à l'avance ! Notre code doit rester propre et clair.

C'est ce pourquoi ont été créées les boucles. Il s'agit d'une nouvelle instruction qui va répéter autant de fois qu'on le souhaite une même opération. Intéressé ? ;) Pour comprendre tout cela, nous allons créer un programme appelé Ligne qui afficherait une ligne de '#' (leur nombre étant choisi par l'utilisateur). La structure de notre programme devrait donc ressembler à cela :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
WITH Ada.Text_IO ;                 USE Ada.Text_IO ;
WITH Ada.Integer_Text_IO ;         USE Ada.Integer_Text_IO ; 

Procedure Ligne is
   Nb : integer ; 
begin

   Put("Combien de dièses voulez-vous afficher ?") ; 
   Get(Nb) ; Skip_line ; 

   --Bloc d'affichage

end Ligne ;

ligne.adb

La boucle Loop simple

Principe général

Avec ce que nous avons vu pour l'instant, nous aurions tendance à écrire notre bloc d'affichage ainsi :

1
2
3
4
5
6
7
8
9
case Nb is
   when 0 => Null ; --ne rien faire
   when 1 => Put("#") ; 
   when 2 => Put("##") ; 
   when 3 => Put("###") ; 
   when 4 => Put("####") ; 
   when 5 => Put("#####") ; 
   when others => Put("######") ; 
end case ;

Le souci c'est que l'utilisateur peut très bien demander 21 dièses ou 24 et que nous n'avons pas tellement envie d'écrire tous les cas possibles et imaginables. :colere: Nous allons donc prendre le parti de n'afficher les '#' que un par un, et de répéter l'action autant de fois qu'il le faudra. Pour cela, nous allons utiliser l'instruction LOOP que nous écrirons dans le bloc d'affichage.

1
2
3
LOOP          --début des instructions qu'il faut répéter
   Put('#') ; 
END LOOP ;    --indique la fin des instruction à répéter

Ceci définit une boucle qui répètera à l'infini tout ce qui est écrit entre LOOP et END LOOP. Plus exactement, on parle de boucle itérative, l'itération étant ici synonyme de répétition. Génial non ?

Arrêter une itération

Et elle s'arrête quand ta boucle ?

Euh… en effet, si vous avez tester ce code, vous avez du vous rendre compte que cela ne s'arrête jamais (ou presque). Ce qui est écrit au-dessus est une boucle infinie ! C'est la grosse erreur de débutant. :-° (Bien que les boucles infinies ne soient pas toujours inutiles.) Pour corriger cela, nous allons déclarer une variable appelée compteur.

1
Compteur : integer := 0 ;

Cette variable vaut 0 pour l'instant mais nous l'augmenterons de 1 à chaque fois que nous afficherons un dièse, et ce, jusqu'à ce qu'elle soit égale à la variable Nb où est enregistrée le nombre de dièses voulus. On dit que l'on incrémente la variable Compteur. Voici donc deux corrections possibles :

1
2
3
4
5
6
7
loop 
   if Compteur = Nb
      then exit ;      --si on a déjà affiché assez de dièses, alors on "casse" la boucle
      else Put('#') ;  --sinon on affiche un dièse et on incrémente Compteur
           Compteur := Compteur +1 ; 
   end if ;
end loop ;
1
2
3
4
5
6
loop 
   exit when Compteur = Nb ; --comme tout à l'heure, on sort si on a affiché assez de dièses
   Put('#') ;                --on peut tranquillement afficher un dièse, car si le nombre 
                             --d'affichages était atteint, la boucle serait déjà cassée
   Compteur := Compteur +1 ; --et on n'oublie pas d'incrémenter notre compteur
end loop ;

L'instruction EXIT permet de «casser» la boucle et ainsi d'y mettre fin. Dans ce cas, il est important de faire le test de sortie de boucle avant d'afficher et d'incrémenter, afin de permettre de tracer une ligne de zéros dièses. C'est aussi pour cela que Compteur est initialisé à 0 et pas à 1. Si l'on exige l'affichage d'au moins un dièse, alors l'ordre dans la boucle peut être modifié. Toutefois, vous devez réfléchir à l'ordre dans lequel les opérations seront effectuées : il est rarement anodin de décaler l'instruction de sortie d'une boucle de quelques lignes.

Voici un exemple si l'utilisateur tape 3 :

Valeur de Compteur

Test de sortie

Affichage

Incrémentation

0

Négatif

#

+1

1

Négatif

#

+1

2

Négatif

#

+1

3

Positif

STOP

STOP

On affichera bien 3 dièses (pas un de moins ou de plus). Vous voyez alors que si le test de sortie était effectué après l'affichage (ce qui reviendrait à inverser les deux colonnes du milieu), nous obtiendrions l'affichage d'un quatrième #. De même, si la variable compteur était initialisé à 1 (ce qui reviendrait à supprimer la ligne Compteur=0), nous n'aurions que deux # affichés. Prenez le temps de réfléchir ou tester les valeurs extrêmes de votre compteur : 0,1, la valeur maximale si elle existe…

Le second code a, ici, ma préférence IF, ELSIF, CASE… Il n'est d'ailleurs pas impossible d'écrire plusieurs instructions donnant lieu à l'instruction EXIT. Une autre variante (plus compacte) serait de ne pas utiliser de variable Compteur et de décrémenter la variable Nb au fur et à mesure (décrémenter = soustraire 1 à chaque tour de boucle) :

1
2
3
4
5
loop
   exit when Nb = 0 ;  -- Si Nb vaut 0 on arrête tout
   Nb := Nb - 1 ;      -- On n'oublie pas de décrémenter…
   Put('#') ;          -- … et surtout d'afficher notre dièse ! 
end loop ;

Cette dernière méthode est encore plus efficace. N'oubliez pas que lorsque vous créez une variable, elle va monopoliser de l'espace mémoire. Si vous pouvez vous passer d'une d'entre elles, c'est autant d'espace mémoire qui sera dégagé pour des tâches plus importantes de votre programme (ou d'un autre). Qui plus est, votre code gagnera également en lisibilité.

Nommer une boucle

Lorsque votre code sera plus complexe et comportera de nombreuses boucles de plusieurs dizaines ou centaines de lignes chacune, il sera sûrement nécessaire de nommer vos boucles pour vous y retrouver. Cela se fait en Ada de la même façon qu'une déclaration : devant le mot clé LOOP, il vous suffit d'écrire le nom de votre boucle suivi de deux points. En revanche, il faudra également nommer votre boucle lors du END LOOP et éventuellement après l'instruction EXIT.

1
2
3
4
5
Ma_Boucle : loop                 --On nomme la boucle
   exit Ma_Boucle when Nb = 0 ;  --Il n'est pas obligatoire de nommer la boucle ici
   Nb := Nb - 1 ;
   Put('#') ;
end loop Ma_Boucle ;             --En revanche il est obligatoire de la nommer ici

Pour ne pas confondre les noms de boucles avec les noms de variables, il est bon d'établir une nomenclature. Par exemple, vous pouvez introduire systématiquement les noms de boucle par un B_ (B_Affichage pour « Boucle d'affichage ») ou un L_ (L_Affichage pour « LOOP d'affichage »).

La boucle While

Mais il y a mieux encore. Pour nous éviter d'exécuter un test de sortie au milieu de notre boucle, il existe une variante à notre instruction LOOP : WHILE … LOOP ! En Anglais, «while» à de nombreux sens, mais dans ce cas de figure il signifie «tant que» ; cette boucle se répètera donc tant que le prédicat inscrit dans les pointillés sera vérifié. Voici un exemple.

1
2
3
4
while Compteur /= Nb loop
   Compteur := Compteur +1 ; 
   Put('#') ; 
end loop ;

Contrairement à tout à l'heure, le prédicat est «Compteur /= Nb» et plus «Compteur = Nb». WHILE indique la condition, non pas de sortie, mais de perpétuation de la boucle. C'est généralement une cause de l'échec d'une boucle ou de la création d'une boucle infinie.

Autre possibilité : décrémenter Nb afin de ne pas déclarer de variable Compteur.

1
2
3
4
while Nb /= 0 loop
   Nb := Nb - 1 ; 
   Put('#') ; 
end loop ;

Enfin, il est possible, comme avec les conditions IF, d'utiliser l'algèbre booléenne et les opérateurs NOT, AND, OR (voire XOR). Cela évitera l'usage d'instructions EXIT pour gérer des cas de sortie de boucle supplémentaires. Si vous n'avez plus que de vagues souvenirs de ces opérateurs, révisez le chapitre sur les booléens.

La boucle For

Les programmeurs sont des gens plutôt fainéants : s'ils peuvent limiter les opérations à effectuer, ils les limitent ! Et ça tombe bien : nous aussi ! :D C'est pourquoi ils ont inventé une autre variante de LOOP qui leur permet de ne pas avoir à incrémenter eux même et de s'épargner la création d'une variable Compteur : la boucle FOR … LOOP ! L'exemple en image :

1
2
3
for i in 1..Nb loop
   put("#") ;
end loop ;

Ça c'est du condensé, hein ? Mais que fait cette boucle ? Elle se contente d'afficher des # autant de fois qu'il y a de nombres entre 1 et Nb (les nombres 1 et Nb étant inclus). Notez bien l'écriture 1..Nb (avec seulement deux points, pas trois), elle indique un intervalle entier, c'est à dire une liste de nombres compris entre deux valeurs.

Ce n'est pas une boucle infinie ?

Et non ! Car elle utilise une variable qui lui sert de compteur : i. Ce qui présente plusieurs avantages : tout d'abord cette variable i n'a pas besoin d'être déclarée, elle est automatiquement créée puis détruite pour cette boucle (et uniquement pour cette boucle, i sera inutilisable en dehors). Pas besoin non plus de l'initialiser ou de l'incrémenter (c'est-à-dire écrire une ligne «i := i +1») la boucle s'en charge toute seule, contrairement à beaucoup de langage d'ailleurs (comme le C). Pas besoin enfin d'effectuer des tests avec IF, WHEN ou CASE qui alourdissent le code. Je parle parfois pour la boucle FOR de boucle-compteur.

Vous vous demandez comment tout cela fonctionne ? C'est simple. Lorsque l'on écrit « FOR i IN 1..Nb LOOP » :

  • FOR indique le type de boucle
  • i est le nom de la variable remplaçant Compteur (je n'ai pas réutilisé la variable Compteur car celle-ci nécessitait d'être préalablement déclarée, ce qui n'est pas le cas de i, je le rappelle).
  • IN 1..Nb indique «l'intervalle» dans lequel la variable i va varier. Elle commencera avec la valeur 1, puis à la fin de la boucle elle prendra la valeur 2, puis 3… jusqu'à la valeur Nb avec laquelle elle effectuera la dernière boucle. (Pour les matheux : je mets «Intervalle» entre guillemets car il s'agit d'un «intervalle formé d'entiers seulement» et non de nombres réels).
  • LOOP indique enfin le début de la boucle

Tu nous as dit de vérifier les valeurs extrèmes, mais que se passera-t-il si Nb vaut 0 ?

C'est simple, il ne se passera rien. La boucle ne commencera jamais. En effet, «l'intervalle» 1..Nb est ce que l'on appelle un « integer'range » en Ada et les bornes 1 et Nb ne sont pas interchangeables dans un integer'range (intervalle fait d'entiers). La borne inférieure est forcément 1, la borne supérieur est forcément Nb. Si Nb vaut 0, alors la boucle FOR passera en revue tous les nombres entiers (integer) supérieurs à 1 et inférieurs à 0. Comme il n'en n'existe aucun, le travail sera vite fini et le programme n'entrera jamais dans la boucle.

Quant au cas Nb vaudrait 1, l'integer'range 1..1 contient un nombre : 1 ! Donc la boucle FOR ne fera qu'un seul tour de piste ! Magique non ?

Et si je voulais faire le décompte «en sens inverse» ? En décrémentant plutôt qu'en incrémentant ?

En effet, comme vous l'avez sûrement compris, la boucle FOR parcourt l'intervalle 1..Nb en incrémentant sa variable i, jamais en la décrémentant. Ce qui fait que si Nb est plus petit que 1, alors la boucle n'aura pas lieu. Si vous souhaitez faire «l'inverse», parcourir l'intervalle en décrémentant, c'est possible, il suffira de le spécifier avec l'instruction REVERSE. Cela indiquera que vous souhaitez parcourir l'intervalle «à rebours». Voici un exemple :

1
2
3
for i in reverse 1..nb loop
   ...
end loop ;

Attention, si Nb est plus petit que 1, la boucle ne s'exécutera toujours pas !

Les antiquités : l'instruction goto

Nous allons, dans cette partie, remonter aux origines des boucles et voir une instruction qui existait bien avant l'apparition des LOOP. C'est un héritage d'anciens langages, qui n'est guère utilisé aujourd'hui. C'est donc à une séance d'archéologie (voire de paléontologie) informatique à laquelle je vous invite dans cette partie, en allant à la découverte de l'instruction GOTO (ou gotosaurus). :lol:

Cette instruction GOTO est la contraction des deux mots anglais «go to» qui signifient «aller à». Elle va renvoyer le programme à une balise que le programmeur aura placé en amont dans son code. Cette balise est un mot quelconque que l'on aura écrit entre chevrons << … >>. Par exemple :

1
2
3
<<debut>>      --on place une balise appelée "debut"
   Put('#') ;
goto debut ;   --cela signifie "maintenant tu retournes à <<debut>>"

Bien sûr il est préférable d'utiliser une condition pour éviter de faire une boucle infinie :

1
2
3
4
5
6
<<debut>>
if Compteur <= Nb 
   then Put('#') ;
      Compteur := Compteur + 1 ;
      goto debut ; 
end if ;

Cette méthode est très archaïque. Elle était présente en Basic en 1963 et fut très critiquée car elle générait des codes de piètre qualité. À noter que dans le dernier exemple l'instruction GOTO se trouve au beau milieu de notre bloc IF ! Difficile de voir rapidement où seront les instructions à répéter, d'autant plus qu'il n'existe pas de « END GOTO » : cette instruction ne fonctionne pas par bloc ce qui dégrade la lisibilité du code. Cette méthode est donc à proscrire autant que possible. Préférez-lui une belle boucle LOOP, WHILE ou FOR (quoique GOTO m'ait déjà sauvé la mise deux ou trois fois :-° ).

Boucles imbriquées

Nous voudrions maintenant créer un programme appelé Carre qui afficherait un carré de '#' et non plus seulement une ligne.

Méthode avec une seule boucle (plutôt mathématique)

Une première possibilité est de se casser la tête. Si on veut un carré de côté n, alors cela veut dire que le carré aura n lignes et n colonnes, soit un total de n x n = n² dièses.

Sur un exemple : un carré de côté 5 aura 5 lignes comprenant chacune 5 dièses soit un total de 5 x 5 = 25 dièses. On dit que 25 est le carré de 5 et on note 5² (c'est la multiplication de 5 par lui-même). De même, 3² = 9, 6² = 36, 10² = 100…

Donc nous pourrions utiliser une boucle allant de 1 à Nb² (de 1 à 25 pour notre exemple) et qui afficherait un retour à la ligne de temps en temps.

Pour un carré de côté 5, il faut aller à la ligne au bout de 5, 10, 15, 20… dièses soit après chaque multiple de 5. Pour savoir si un nombre X est multiple de 5, nous utiliserons le calcul « X mod 5 » qui donne le reste de la division de X par 5. Si X est un multiple de 5, ce calcul doit donner 0.

Il faudra donc une boucle du type « FOR i IN 1..Nb**2 »plus un test du type « IF i MOD Nb = 0 » dans la boucle pour savoir si l'on doit aller à la ligne. À vous d'essayer, je vous fournis une solution possible ci-dessous :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
with ada.Text_IO, ada.Integer_Text_IO ; 
use ada.Text_IO, ada.Integer_Text_IO ; 

procedure Carre is
   Nb : integer ; 
Begin

   Put("Quel est le cote de votre carre ? ") ; 
   Get(Nb) ; Skip_line ; 

   for i in 1..Nb**2 loop  --Nb**2 est la notation du carré, on peut aussi écrire Nb*Nb
      Put('#') ;           --on affiche le dièse
      if i mod Nb = 0      --Si i est multiple de Nb = Si on a déjà affiché toute une ligne
         then New_line ;   --On retourne à la ligne
      end if ; 
   end loop ; 
end Carre ;

Vous avez :

  • compris ? Très bien, vous devez avoir l'esprit scientifique.
  • absolument rien compris ? Pas grave car nous allons justement ne pas faire cela ! C'est en effet un peu compliqué et ce n'est pas non plus naturel comme façon de coder ni reproductible.

Méthode avec deux boucles (plus naturelle)

La méthode précédente est quelque peu tirée par les cheveux mais constitue toutefois un bon exercice pour manipuler les opérations puissance (Nb**2) et modulo (i MOD Nb). Voici une méthode plus intuitive, elle consiste à imbriquer deux boucles l'une dans l'autre de la manière suivante :

1
2
3
4
Boucle1
   Boucle2
   fin Boucle2
fin Boucle1

La Boucle2 sera chargée d'afficher les dièses d'une seule ligne. La Boucle1 sera chargée de faire défiler les lignes. À chaque itération, elle relancera la Boucle2 et donc affichera une nouvelle ligne. D'où le code suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
with ada.Text_IO, ada.Integer_Text_IO ; 
use ada.Text_IO, ada.Integer_Text_IO ; 

procedure Carre is
   Nb : integer ; 
Begin

   Put("Quel est le cote de votre carre ? ") ; 
   Get(Nb) ; Skip_line ; 

   for i in 1..Nb loop      --Boucle chargée d'afficher toutes les lignes
      for j in 1..Nb loop   --Boucle chargée d'afficher une ligne
         Put('#') ;
      end loop ; 
      New_line ;            --Après avoir affiché une ligne, pensez à retourner à la ligne
   end loop ; 

end Carre ;

Et voilà le travail ! C'est simple, clair et efficace. Pas besoin d'en écrire des pleines pages ou d'avoir fait Math Sup. Retenez bien cette pratique, car elle nous sera très utile à l'avenir, notamment lorsque nous verrons les tableaux.

Il est possible d'insérer des instructions EXIT dans des boucles WHILE ou FOR, même si elles sont imbriquées. Toutefois, si l'instruction exit est placée dans la boucle n°2, elle ne donnera l'ordre de sortir que pour la boucle n°2, pas pour la n°1. Pour sortir des boucles n°1 et n°2 en même temps (voir même d'une boucle n°3), l'astuce consiste à nommer la première boucle comme nous l'avons vu au début de ce chapitre, par exemple B_Numero1, et à écrire l'instruction EXIT B_Numero1.

Pourquoi utilises-tu des lettres i, j… et pas des noms de variable plus explicites ?

Cette habitude n'est pas une obligation, libre à vous de donner à vos variables de boucles les noms que vous souhaitez. Voici toutefois trois raisons, assez personnelles, pour lesquelles je conserverai cette habitude tout au long de ce tutoriel :

  1. Raison 1 : la variable i ou j a un temps de vie très court (restreint à la seule boucle FOR) elle ne sera pas réutilisée plus tard. Je devrais donc réussir à me souvenir de sa signification jusqu'au END LOOP.
  2. Raison 2 : la variable i ou j servira très souvent comme indice d'un tableau. Exemple : tableau(i) est la i-ème valeur du tableau. Nous verrons cela dans un prochain chapitre, mais vous pouvez comprendre qu'il est plus simple d'écrire tableau(i) que tableau(Nom_de_variable_long_et_complique).
  3. Raison 3 : j'ai suivi une formation Mathématiques-Informatique. Et en «Maths» on a pour habitude d'utiliser la variable i lorsque l'on traite d'objets ayant un comportement similaire aux boucles. Et vous avez sûrement remarqué que nous faisons régulièrement appel aux Mathématiques pour programmer. Par exemple, lorsqu'un mathématicien veut additionner les carrés de tous les nombres compris entre 1 et N, il écrit généralement : ${\sum_{i=1}^{N} {i^2}}$.

Exercices

Voilà voilà. Si maintenant vous voulez vous exercer avec des boucles imbriquées, voici quelques idées d'exercices. Je ne propose pas de solution cette fois. À vous de vous creuser les méninges.

Exercice 1

Affichez un carré creux :

1
2
3
4
5
6
######
#    #
#    #
#    #
#    #
######

Exercice 2

Affichez un triangle rectangle :

1
2
3
4
5
6
#
##
###
####
#####
######

Exercice 3

Affichez un triangle isocèle, équilatéral, un rectangle… ou toute autre figure.

Exercice 4

Rédigez un programme testant si un nombre N est multiple de 2, 3, 4… N-1. Pensez à l'opération mod !

Exercice 5

Rédigez un programme qui demande à l'utilisateur de saisir une somme S, un pourcentage P et un nombre d'années T. Ce programme affichera les intérêts accumulés sur cette somme S avec un taux d'intérêt P ainsi que la somme finale, tous les ans jusqu'à la «t-ième» année.

Calculer un intérêt à P% sur une somme S revient à effectuer l'opération SP/100. La somme finale est obtenue en ajoutant les intérêts à la somme initiale ou en effectuant l'opération S(100+P)/100.

Exercice 6

Créez un programme qui calcule les termes de la suite de Fibonacci. En voici les premiers termes : 0 ; 1 ; 1 ; 2 ; 3 ; 5 ; 8 ; 13 ; 21 ; 34 ; 55… Chaque terme se calcule en additionnant les deux précédents, les deux premiers termes étant 0 et 1.

Maintenant que nous avons acquis de bonnes notions sur les boucles et les conditions, nous allons pouvoir nous intéresser aux sous-programmes. Nous reviendrons amplement sur les boucles lors du chapitre sur les tableaux. Celui-ci fera appel à tout ce que vous avez déjà vu sur le typage des données (integer, float…), les conditions mais aussi et surtout sur les boucles qui révèleront alors toute leur utilité (notamment la boucle FOR). Ce sera également l'occasion de découvrir une autre version de la boucle FOR que je ne peux encore vous révéler.


En résumé :

  • Lorsqu'il est nécessaire de répéter plusieurs fois des instructions vous devrez faire appel aux boucles.
  • Si vous utilisez une boucle simple (LOOP), n'oubliez pas d'insérer une condition de sortie avec l'instruction EXIT. Vérifiez également que cette condition peut-être remplie afin d'éviter les boucles infinies.
  • En règle générale, on utilise plus souvent des boucles WHILE ou FOR afin que la condition de sortie soit facile à retrouver parmi vos lignes de code. La boucle LOOP sera souvent réservée à des conditions de sortie complexes et/ou multiples.
  • L'instruction EXIT peut être utilisée dans des boucles WHILE ou FOR, mais limitez cette pratique autant que possible.
  • De même qu'il est possible d'imbriquer plusieurs conditions IF, vous pouvez imbriquer plusieurs boucles, de même nature ou pas. Cette pratique est très courante et je vous invite à la travailler si vous n'êtes pas à l'aise avec elle.