Des boucles qui se répètent, répètent, répètent…

Nous sommes maintenant capables d’exécuter des codes différents en fonction de conditions. Mais notre programme reste très linéaire : nous exécutons les instructions l’une après l’autre, du début à la fin, de haut en bas. Dans la droite ligne du chapitre précédent, nous allons donc voir un mécanisme offert par C++ pour répéter autant de fois que l’on souhaite une série d’instructions.

Ce chapitre introduira les boucles et permettra d’améliorer encore notre gestion des erreurs, vue dans le chapitre précédent.

while — Tant que…

Commençons ce chapitre avec une boucle while. En toute originalité, ce mot-clef est aussi un mot anglais qui signifie « tant que ». Elle exécute une série d’instructions tant que la condition est vraie. Par contre, dès que la condition est évaluée à false, la boucle s’arrête. On dit aussi qu'on boucle ou qu'on itère.

Image originale tirée du tutoriel sur le langage C.
Image originale tirée du tutoriel sur le langage C.

Prenons un exemple tout bête : notre programme va compter de 0 à 9. Voici le code correspondant.

#include <iostream>

int main()
{
    std::cout << "Salut, je vais compter de 0 à 9." << std::endl;
    int compteur { 0 };

    while (compteur < 10)
    {
        std::cout << compteur << std::endl;
        ++compteur;
    }

    std::cout << "Je m'arrête, après je sais pas faire." << std::endl;
    return 0;
}

Le code est exécuté ainsi : on évalue la condition. Tant que compteur est strictement inférieur à 10, alors on affiche le nombre puis on l’incrémente. Quand finalement compteur vaut 10, la condition devient fausse, on passe à la suite du code.

Boucle infinie

Il est important de vérifier qu’à un moment ou un autre la condition devienne fausse et qu’on sort bien de la boucle. Si l’on n’y prend pas garde, on tombe dans le piège de la boucle infinie !

#include <iostream>

int main()
{
    std::cout << "Salut, je vais compter de 0 à 9." << std::endl;
    int compteur { 0 };

    while (compteur < 10)
    {
        std::cout << compteur << std::endl;
        // Oups, on a oublié d'incrémenter le compteur. Nous sommes bloqués dans la boucle.
    }

    std::cout << "Je ne serai jamais affiché." << std::endl;
    return 0;
}

Dans ces cas, il ne reste plus qu’à terminer le programme sauvagement en le tuant (oui les informaticiens sont brutaux). Je répète donc, faites attention aux boucles infinies.

Comme les boucles sont basées sur les conditions, aucun problème pour en utiliser plusieurs en même temps.

#include <iostream>

int main()
{
    char entree { '?' };
    int compteur { 0 };

    // On boucle tant qu'on a pas rentré la lettre 'e' ou que le compteur n'a pas atteint 5.
    while (compteur < 5 && entree != 'e')
    {
        std::cout << "Le compteur vaut " << compteur << std::endl;
        std::cout << "Rentre un caractère : ";
        std::cin >> entree;

        ++compteur;
    }
    
    std::cout << "Fin du programme." << std::endl;
    return 0;
}

Exercices

Une laverie

Nous allons faire un programme d’aide dans une laverie automatique. Dans celle-ci, deux types de machines sont employées : des machines économes capables de laver 5kg de linge au maximum et d’autres, plus gourmandes, capables de laver 10kg.

  • Si le linge fait moins de 5 kg, il faut le mettre dans une machine de 5kg.
  • S’il fait entre 5kg et 10kg, il faut le mettre dans une machine de 10kg.
  • Enfin, s’il fait plus, il faut répartir le linge entre machines de 10kg et, dès qu’il reste 5kg ou moins, mettre le linge restant dans une machine 5kg.
Correction laverie
#include <iostream>

int main()
{
    std::cout << "Bienvenue à la laverie de Clem. Combien de kilos de linge as-tu ? ";
    int kilos { 0 };
    std::cin >> kilos;

    if (kilos <= 5)
    {
        std::cout << "Tu as peu de linge, mets-le dans une machine de 5kg." << std::endl;
    }
    else if (kilos <= 10)
    {
        std::cout << "Tu as beaucoup de linge, mets-le dans une machine de 10kg." << std::endl;
    }
    else
    {
        // On a plus que 10 kilos, il va falloir diviser le linge en plusieurs machines.
        std::cout << "Dites donc, il faut laver son linge plus souvent !" << std::endl;

        int nb_machines_10_kilos { 0 };

        while (kilos > 5)
        {
            // Pour chaque machine de 10kg utilisée, on enlève 10kg au total de linge restant.
            kilos -= 10;
            ++nb_machines_10_kilos;
        }

        std::cout << "Tu as besoin de " << nb_machines_10_kilos << " machine(s) de 10kg." << std::endl;
        if (kilos >= 0)
        {
            // S'il reste entre 0 et 5 kilos, il ne faut pas oublier la dernière machine de 5kg.
            std::cout << "Le reste rentrera dans une machine de 5kg." << std::endl;
        }
    }

    return 0;
}

PGCD

Allez, on va faire un peu de mathématiques ! Le PGCD de deux nombres est un entier qui est le plus grand diviseur que ces deux nombres ont en commun. Ainsi, le PGCD de 427 et 84 est 7, car 427=7×61427 = 7 \times 61 et 84=7×1284 = 7 \times 12. Pour le calculer, voici l’algorithme.

  • On stocke le modulo du premier nombre, appelé a, par l’autre nombre, appelé b, dans une variable r.
  • Tant que r est différent de zéro, on effectue les opérations suivantes.
    • On affecte la valeur de b à a.
    • On affecte la valeur de r à b.
    • On affecte à r le résultat du modulo de a par b.
  • Quand r est nul, alors b représente le PGCD des deux nombres donnés en entrée.
Correction PGCD
#include <iostream>

int main()
{
    std::cout << "Rentre un nombre a : ";
    int a { 0 };
    std::cin >> a;

    std::cout << "Rentre un nombre b : ";
    int b { 0 };
    std::cin >> b;

    int r { a % b };
    while (r != 0)
    {
        a = b;
        b = r;
        // r vaut le reste de la division entière de a par b.
        r = a % b;
    }

    std::cout << "PGCD vaut = " << b << std::endl;
    return 0;
}

Somme de nombres de 1 à n

Disons que je vous donne un entier NN au hasard. Pouvez-vous me créer un programme capable de me donner la somme de tous les entiers de 1 à nn, c’est-à-dire la somme de 1+2+3+...+(n2)+(n1)+n1 + 2 + 3 + ... + (n - 2) + (n - 1 ) + n ?

Correction somme de 1 à n
#include <iostream>

int main()
{
    std::cout << "Donne-moi un entier : ";
    int n { 0 };
    std::cin >> n;

    int total { 0 };
    int compteur { 1 };

    while (compteur <= n)
    {
        total += compteur;
        ++compteur;
    }
    
    std::cout << "La somme totale vaut " << total << "." << std::endl;
    return 0;
}

Ceux qui aiment les maths ont remarqué qu’il s’agit de la somme des premiers entiers de 1 à n et qu’il existe une formule pour calculer directement sans boucler : N×(N+1)2\frac {N \times (N+1)} {2}. Bravo à vous. :)

do while — Répéter … tant que

La deuxième boucle offerte par C++ est très proche de la précédente. En fait, c’est la même, à deux différences près. Une boucle do while s’exécute toujours une fois au minimum, même si la condition est fausse. La condition est en effet vérifiée après l’exécution du code.

Do while
Do while

La deuxième, c’est qu’il faut un point-virgule après le while. Voyez par vous-mêmes le code suivant.

#include <iostream>

int main()
{
    do
    {
        std::cout << "On passe quand même ici." << std::endl;
    } while (false);

    return 0;
}

Hormis ces deux subtiles différences, do while se comporte exactement comme while et permet elle aussi de boucler. Nous allons voir dans la section suivante comment les deux nous aiderons à résoudre le problème de sécurisation des entrées, soulevé au chapitre précédent.

[T.P] Gérer les erreurs d'entrée — Partie II

Dans la première partie, nous avions un mécanisme qui ne nous protégeait qu’une seule fois des erreurs. Maintenant, nous sommes en mesure de nous protéger tant que l’entrée n’est pas correcte. Améliorez donc le code que vous avez écrit pour qu’il soit capable de protéger le code suivant.

#include <iostream>

int main()
{
    std::cout << "Quel jour es-tu né ? ";
    int jour { 0 };
    std::cin >> jour;

    std::cout << "Quel mois es-tu né ? ";
    int mois { 0 };
    std::cin >> mois;

    std::cout << "Tu es né le " << jour << "/" << mois << "." << std::endl;
    return 0;
}
Correction T.P partie II
#include <iostream>

int main()
{
    std::cout << "Quel jour es-tu né ? ";
    int jour { 0 };

    // Comme std::cin >> jour renvoie false s'il y a eu une erreur, afin de rentrer dans la boucle, on prend la négation de l'expression avec !
    while (!(std::cin >> jour))
    {
        std::cout << "Entrée invalide. Recommence." << std::endl;
        std::cin.clear();
        std::cin.ignore(255, '\n');
    }

    std::cout << "Quel mois es-tu né ? ";
    int mois { 0 };

    // Même version avec un do while.
    do
    {
        if (std::cin.fail())
        {
            std::cout << "Entrée invalide. Recommence." << std::endl;
            std::cin.clear();
            std::cin.ignore(255, '\n');
        }

    } while (!(std::cin >> mois));

    std::cout << "Tu es né le " << jour << "/" << mois << "." << std::endl;
    return 0;
}

Les deux versions sont légèrement différentes dans ce qu’elles font. La première demande l’entrée, puis tant qu’elle est invalide, elle nettoie puis demande à nouveau. La deuxième vérifie s’il y a eu une quelconque erreur, la nettoie puis demande l’entrée et recommence si l’entrée est invalide.

Peu importe celle que vous avez choisi d’utiliser, l’essentiel est de nettoyer nos entrées et d’avoir des programmes robustes. Et c’est justement ce que nous faisons. :)

Petit à petit, grâce à nos connaissances qui s’étoffent, nous améliorons la protection des entrées. Mais c’est dommage d’avoir le même code dupliqué à plusieurs endroits. C’est sale, ça nous fait perdre du temps si l’on doit modifier un morceau de code dans chaque endroit ; et je ne parle pas des erreurs liés à des morceaux de codes différents des autres. Nous apprendrons dans le chapitre suivant comment s’en sortir.

for — Une boucle condensée

Les boucles while et do while sont très utiles et ajoutent une corde à notre arc. Mais ne trouvez-vous pas ça dommage d’avoir nos variables liées à l’itération (comme compteur) séparées de la boucle elle-même ?

Actuellement, notre code est simple, mais imaginons un instant un code bien plus complexe. Il serait mieux de séparer les opérations liées à la boucle de nos autres variables importantes. Cela rendra le code plus clair et plus compréhensible. C’est exactement ce que permet for (« pour » en anglais).

for (/* initialisation */; /* condition */; /* itération */)
{
    // Code de la boucle.
}

// Est équivalent à la boucle ci-dessous.

// Notez la création d'une portée artificielle.
{
    // Initialisation.
    while (/* condition */)
    {
        // Code de la boucle.
        /* itération */
    }
}

Hormis cette syntaxe différente, for s’utilise comme ses consœurs. Nous sommes libres de l’initialisation, de la condition et de l’itération.

#include <iostream>

int main()
{
    // Compte de 10 à 1. On décrémente ici.
    for (int compteur {10}; compteur > 0; --compteur)
    {
        std::cout << compteur << std::endl;
    }
    
    std::cout << "Fin du programme." << std::endl;
    return 0;
}

Nous pouvons même effectuer plusieurs opérations en même temps, tant que les variables déclarées sont du même type. Cette possibilité est cependant à prendre avec des gants parce qu'elle peut rendre le code très peu lisible.

#include <iostream>

int main()
{
    // Compte de 10 à 1 et de 1 à 10 en même temps.
    for (int compteur {10}, compteur_inverse {1}; compteur > 0 && compteur_inverse <= 10; --compteur, ++compteur_inverse)
    {
        std::cout << compteur << " et " << compteur_inverse << std::endl;
    }
    
    std::cout << "Fin du programme." << std::endl;
    return 0;
}

À quel moment dois-je utiliser for ou while ?

Le choix est vôtre, C++ vous laisse entièrement libre. Après, on utilise plus souvent for dans le cas où l’on veut itérer (compter de 1 à 100 par exemple), alors que while boucle pour réaliser un traitement autant de fois que nécessaire (par exemple, tant que std::cin.fail renvoie true).

Variante

Dans le chapitre suivant, nous verrons qu’il existe une autre boucle for.

Boucles imbriquées

Comme pour les conditions, il est tout à fait possible d'imbriquer plusieurs boucles l’une dans l’autre. De même, on peut tout à fait mélanger les types de boucles, comme le prouve l’exemple suivant, qui affiche les tables de multiplications de 1 à 10 en utilisant while et for.

#include <iostream>

int main()
{
    int facteur_gauche { 1 };
    while (facteur_gauche <= 10)
    {
        for (int facteur_droite { 1 }; facteur_droite <= 10; ++facteur_droite)
        {
            std::cout << facteur_gauche << "x" << facteur_droite << " = " << facteur_gauche * facteur_droite << std::endl;
        }
        // On saute une ligne pour séparer chaque table.
        std::cout << std::endl;
        ++facteur_gauche;
    }

    return 0;
}

Convention de nommage

Dernier point avant de continuer : vous verrez très souvent, tant dans ce tutoriel que dans les divers codes C++ que vous aurez l’occasion d’examiner, que les programmeurs utilisent i comme identifiant de variable pour parcourir une boucle (ainsi que j et k dans le cas de boucles imbriquées). C’est une abréviation pour iterator, parce que cette variable itère, répète le corps de la boucle. Ce nom est tellement court et explicite qu’il est pour ainsi dire devenu une convention de nommage en C++.

Contrôler plus finement l'exécution de la boucle

Les boucles sont très utiles, mais le code à l’intérieur des accolades est soit entièrement sauté, soit entièrement exécuté. Sachez que C++ permet de contrôler un peu plus précisément l’exécution avec deux mots-clefs : break et continue.

Précision

Simple précision : break et continue s’utilisent avec les boucles uniquement. Ils ne s’utilisent pas avec les conditions : pas de break au sein d’un if, par exemple.

break — Je m’arrête là

Le premier, c’est break, qui en anglais signifie littéralement « casser », « interrompre ». Celui-ci nous autorise à mettre fin à l’exécution de la boucle, peu importe où nous en étions. Dans le code ci-dessous, vous ne verrez jamais afficher les nombres au-dessus de 3 parce que la boucle est terminée par break dès que notre variable vaut 3.

#include <iostream>

int main()
{
    for (int i { 0 }; i < 10; ++i)
    {
        // Si i vaut 3, j'arrête la boucle.
        if (i == 3)
        {
            break;
        }

        std::cout << i << std::endl;
    }
    
    return 0;
}

Dans le cas de boucles imbriquées, break ne stoppe l’exécution que de la boucle dans laquelle il se trouve, mais pas les boucles englobantes.

for (int i { 0 }; i < 5; ++i)
{
    for (int j { 0 }; j < 3; ++j)
    {
        if (j == 1)
        {
            // Quand j vaut 1, on retourne dans le for(i).
            break;
        }

        std::cout << "Voici la valeur de j : " << j << std::endl;
    }

    std::cout << "Voici la valeur de i : " << i << std::endl;
}

Pour illustrer son fonctionnement avec un cas plus utile, prenons un exemple mathématique. Connaissez-vous le concept du test de primalité ? Il s’agit d’un test pour déterminer si un entier est premier, c’est-à-dire divisible uniquement par 1 et par lui-même ; 13 est un nombre premier mais pas 10, car divisible par 1, 2, 5, et 10. Essayez donc de faire cet exercice.

Indice

Nous allons devoir tester tous les nombres entre 2 et x pour savoir s’ils sont diviseurs de x. Mais si nous trouvons un nombre y différent de x et qui divise x, alors nous savons que le nombre n’est pas premier, inutile de continuer. ;)

Correction
#include <iostream>

int main()
{
    std::cout << "Donne-moi un nombre, je te dirai s'il est premier : ";
    int nombre { 0 };
    std::cin >> nombre;

    bool est_premier { true };
    for (int i { 2 }; i < nombre; ++i)
    {
        if (nombre % i == 0)
        {
            // Si i divise nombre, alors nous savons que nombre n'est pas premier.
            est_premier = false;
            break;
        }
    }
    
    if (est_premier)
    {
        std::cout << nombre << " est un nombre premier !" << std::endl;
    }
    else
    {
        std::cout << nombre << " n'est pas premier." << std::endl;
    }

    return 0;
}

continue — Saute ton tour !

L’autre mot-clef, continue, permet de sauter l’itération courante. Toutes les instructions qui se trouvent après ce mot-clef sont donc ignorées et la boucle continue au tour suivant. Imaginons que nous voulions un programme permettant d’afficher les nombres impairs compris entre 0 et un nombre choisi par l’utilisateur. Essayez donc de le faire, avec les connaissances que vous avez.

Indice

Pour rappel, un nombre est pair s’il est divisible par 2 et impair autrement.

Correction
#include <iostream>

int main()
{
    std::cout << "Donne-moi un nombre, je t'afficherai tous les nombres impairs jusqu'à ce nombre : ";
    int maximum { 0 };
    std::cin >> maximum;

    for (int i { 0 }; i <= maximum; ++i)
    {
        if (i % 2 == 0)
        {
            // On saute ce nombre puisqu'il est pair.
            continue;
        }

        std::cout << i << std::endl;
    }

    return 0;
}
Potentielle boucle infinie

Attention lorsque vous utilisez continue au sein d’une boucle while ou do while, parce que TOUT le code situé après est sauté, même les instructions d’incrémentation. Le code suivant est donc un exemple de boucle infinie.

int i { 0 };
while (i < 10)
{
    if (i == 5)
    {
        // Ohoh...
        continue;
    }

    ++i;
}

En résumé

  • C++ nous offre trois façons de boucler : while, do while et for.
  • while s’utilise plutôt pour boucler tant qu’une condition n’est pas remplie alors que for est utilisé pour itérer sur un nombre connu d’éléments.
  • Il faut faire attention à ne pas tomber dans une boucle infinie.
  • Nous disposons de deux mots-clefs, break et continue, pour affiner notre contrôle sur l’exécution de la boucle.