Aller au contenu
Topos Uranos

Résumé

La leçon Boucles a appris à écrire une répétition ; celle-ci apprend à bien l’écrire et à savoir pourquoi elle est juste. Elle commence par les schémas avec lesquels se construit la plupart des boucles (le compteur, l’accumulateur, le maximum et le minimum, la sentinelle et le drapeau), chacun défini par ce que sa variable signifie après chaque itération. Cette observation conduit au concept central : l’invariant, une affirmation sur les variables qui est vraie avant la première itération et après chacune, et qui, jointe à la condition de sortie, implique le résultat. C’est une récurrence sur le nombre d’itérations, et grâce à elle on conçoit une boucle à rebours : d’abord ce qui doit être vrai à la fin, puis l’invariant, enfin le corps. La terminaison se justifie par le variant, déjà présenté dans la leçon Du problème à l’algorithme, et la leçon montre trois boucles qui ne terminent pas : une avec une condition sur des réels, une avec un compteur non signé et une dont le compteur signé déborde, ce qui est un comportement indéfini. Elle se termine par un premier contact avec le débogueur de Visual Studio Code, qui arrête le programme sur une ligne et montre ses variables itération après itération.

Objectifs d’apprentissage

  1. Reconnaître dans une boucle les schémas du compteur, de l’accumulateur, du maximum et de la sentinelle, et s’en servir pour concevoir de nouvelles boucles.
  2. Énoncer l’invariant d’une boucle simple et vérifier qu’il se conserve au cours d’une itération quelconque.
  3. Justifier la terminaison d’une boucle par une quantité entière qui décroît et qui est bornée.
  4. Suivre l’exécution d’une boucle avec le débogueur de Visual Studio Code, à l’aide d’un point d’arrêt et de la vue des variables.

Schémas

Ce que signifie une variable

La plupart des boucles ne s’inventent pas : elles s’assemblent à partir de quelques pièces qui reviennent dans tous les programmes. Chaque pièce est une variable avec une initialisation avant la boucle, une mise à jour à l’intérieur de celle-ci et, surtout, une signification qui est vraie après chaque itération. Connaître les pièces par leur signification, et non par leur forme, est ce qui permet de les combiner sans erreur.

  • Un compteur vaut le nombre de données traitées (ou de celles qui remplissent une condition). On l’initialise à 0, parce qu’avant la première itération aucune n’a été traitée, et on le met à jour avec ++count, seulement lorsqu’il y a quelque chose à compter.
  • Un accumulateur vaut la combinaison des données traitées : leur somme, leur produit. On l’initialise à l’élément neutre de l’opération (0 pour la somme, 1 pour le produit), parce que la somme de zéro donnée vaut 0 et le produit de zéro donnée vaut 1.
  • Un maximum vaut la plus grande des données traitées, et un minimum, la plus petite. Le maximum de zéro donnée n’existe pas : c’est pourquoi on l’initialise avec la première donnée, et non avec un nombre choisi au hasard.
  • Une sentinelle est une valeur de l’entrée qui n’est pas une donnée, mais le signal que les données sont terminées. Le schéma exige de lire une fois avant la boucle et une autre fois à la fin du corps, comme l’a fait la leçon Boucles pour sommer jusqu’au zéro, de sorte que la condition examine chaque valeur avant qu’elle soit traitée, et que la sentinelle ne soit jamais comptée comme donnée.
  • Un drapeau est un bool qui indique si quelque chose s’est produit au moins une fois parmi les données traitées. On l’initialise à false et il passe à true la première fois que cela se produit ; aucune itération ultérieure ne le ramène à false.

Un exemple avec toutes les pièces

Un relevé pluviométrique note les millimètres tombés chaque jour. Le programme lit d’abord un seuil, puis les mesures de plusieurs jours, terminées par un nombre strictement négatif, qui n’est pas une mesure possible et sert de sentinelle ; il écrit combien il y a de mesures, leur moyenne, le maximum, combien de jours ont dépassé le seuil et s’il y a eu un jour sans pluie.

Programme en C++
#include <iostream>#include <print>​int main(){    double threshold{};    std::cin >> threshold;    int count{0};          // compteur : mesures traitées    int aboveCount{0};     // compteur : mesures au-dessus du seuil    double total{0.0};     // accumulateur : somme des mesures    double largest{0.0};   // maximum : valable seulement si count > 0    bool dryDay{false};    // drapeau : il y a eu un jour sans pluie    double value{};    std::cin >> value;    while (std::cin && value >= 0.0) {        if (count == 0 || value > largest) {            largest = value;        }        ++count;        total += value;        if (value > threshold) {            ++aboveCount;        }        if (value == 0.0) {            dryDay = true;        }        std::cin >> value;    }    if (count == 0) {        std::println("Aucune mesure.");    } else {        std::println("Mesures : {} ; moyenne : {:.2f} mm", count, total / count);        std::println("Maximum : {:.1f} mm ; jours au-dessus de {:.1f} mm : {}", largest, threshold, aboveCount);        std::println("Jours sans pluie : {}", dryDay ? "oui" : "non");    }}
Entrée du programme
104.5 0 12.5 21.5 7.5 -1
Sortie
Mesures : 5 ; moyenne : 9.20 mmMaximum : 21.5 mm ; jours au-dessus de 10.0 mm : 2Jours sans pluie : oui

Quatre décisions méritent un commentaire. La condition count == 0 || value > largest initialise le maximum avec la première donnée sans lecture séparée : à la première itération le compteur vaut 0 et l’affectation a toujours lieu ; aux suivantes, seulement si la donnée dépasse le maximum. La comparaison value == 0.0 est exacte, parce que la valeur a été lue et non calculée : le chiffre 0 se convertit en le zéro exact, et la mise en garde de la leçon Les nombres à virgule flottante contre == porte sur des résultats d’opérations. La condition de la boucle inclut std::cin, de sorte qu’une entrée qui s’achève sans sentinelle termine aussi la boucle. Enfin, la moyenne n’est calculée que s’il y a des mesures : avec count égal à 0, total / count serait 0/00/0, ce qui en virgule flottante n’est pas une erreur, mais la valeur « pas un nombre », que le programme écrirait sans avertissement.

ExempleLes variables itération après itération

Démonstration

  1. Avant la boucle : le seuil est 10, les compteurs et l’accumulateur valent 0, le drapeau est faux, et la première mesure, 4.5, a déjà été lue. Le maximum vaut 0, mais cette valeur ne signifie rien tant que count vaut 0.

  2. Première itération : count vaut 0, de sorte que 4.5 devient le maximum ; le compteur passe à 1 et le total à 4.5. On lit 0.

  3. Deuxième itération : 0 ne dépasse pas le maximum, mais il lève le drapeau. On lit 12.5.

  4. Troisième itération : 12.5 est le nouveau maximum et dépasse le seuil : aboveCount passe à 1. On lit 21.5.

  5. Quatrième itération : 21.5 est le nouveau maximum et dépasse aussi le seuil. On lit 7.5.

  6. Cinquième itération : 7.5 ne change ni le maximum ni aboveCount ; le total atteint 46. On lit −1-1.

  7. La condition examine −1-1, qui est strictement négatif : la boucle se termine sans le traiter. La moyenne est 46/5=9.246 / 5 = 9.2.

On observe que, dans chaque ligne de la figure, chaque variable respecte sa signification par rapport aux mesures traitées jusque-là : total est leur somme, largest leur maximum, dryDay indique s’il y a eu un zéro parmi elles. Cette propriété, vraie après chaque itération, porte un nom.