7 Structures de contrôle

Les structures de contrôle appartiennent aux langages de programmation dits structurés. Elles permettent de modifier l'ordre des opérations lors de l'exécution du code. Il y a trois catégories de structures de contrôle en C :

  1. Les embranchements (branching)

  2. Les boucles (loops)

  3. Les sauts (jumps)

Ces structures de contrôles sont toujours composées de :

  • Séquences

  • Sélections

  • Répétitions

  • Appels de fonctions

Sans structure de contrôle, un programme se comportera toujours de la même manière et ne pourra pas être sensible à des évènement extérieurs puisque le flux d'exécution ne pourra pas être modifié conditionnellement.

7.1 Séquences

En C, chaque instruction est séparée de la suivante par un point virgule ; (U+003B):

k = 8; k *= 2;

Une séquence est une suite d'instructions regroupées en un bloc matérialisé par des accolades {}:

{
    double pi = 3.14;
    area = pi * radius * radius;
}

Note

N'allez pas confondre le point virgule ; (U+003B) avec le ; (U+037E), le point d'interrogation grec (ερωτηματικό). Certains farceurs aiment à le remplacer dans le code de camarades ce qui génère naturellement des erreurs de compilation.

7.2 Les embranchements

Les embranchements sont des instructions de prise de décision. Une prise de décision peut être binaire, lorsqu'il y a un choix vrai et un choix faux, ou multiple lorsque la condition est scalaire. En C il y en a trois type d'embranchements :

  1. if, if else

  2. switch

  3. L'instruction ternaire

Diagrammes BPMN

Fig. 7.1 Exemples d'embranchements dans les diagrammes de flux BPMN (Business Process Modelling Notation) et NSD (Nassi-Shneiderman)

Les embranchements s'appuient naturellement sur les séquences puisque chaque branche est composée d'une séquence regroupant le code la composant :

if (value % 2)
{
    printf("odd\n");
}
else
{
    printf("even\n");
}

7.2.1 if..else

Le mot clé if est toujours suivi d'une condition entre parenthèses qui est évaluée. Si la condition est vraie, le premier bloc est exécuté, sinon, le second bloc situé après le else est exécuté.

Les enchaînements possibles sont :

  • if

  • if + else

  • if + else if

  • if + else if + else if + ...

  • if + else if + else

Une condition n'est pas nécessairement unique, mais peut-être la concaténation logique de plusieurs conditions séparées :

if((0 < x && x < 10) || (100 < x && x < 110) || (200 < x && x < 210))
{
    printf("La valeur %d est valide", x);
    is_valid = true;
}
else
{
    printf("La valeur %d n'est pas valide", x);
    is_valid = false;
}

Remarquons qu'au passage cet exemple peut être simplifié:

is_valid = (0 < x && x < 10) || (100 < x && x < 110) || (200 < x && x < 210);

if (is_valid)
{
    printf("La valeur %d est valide", x);
}
else
{
    printf("La valeur %d n'est pas valide", x);
}

Notons quelques erreurs courantes :

  • Il est courant de placer un point virgule derrière un if. Le point virgule correspondant à une instruction vide, c'est cette instruction qui sera exécutée si la condition du test est vraie.

    if (z == 0);
    printf("z est nul"); // ALWAYS executed
    
  • Le test de la valeur d'une variable s'écrit avec l'opérateur d'égalité == et non l'opérateur d'affectation =. Ici, l'évaluation de la condition vaut la valeur affectée à la variable.

    if (z = 0)               // set z to zero !!
        printf("z est nul"); // NEVER executed
    
  • L'oubli des accolades pour déclarer un bloc d'instructions

    if (z == 0)
        printf("z est nul");
        is_valid = false;
    else
        printf("OK");
    

L'instruction if permet également l'embranchement multiple, lorsque les conditions ne peuvent pas être regroupées :

if (value % 2)
{
    printf("La valeur est impaire.");
}
else if (value > 500)
{
    printf("La valeur est paire et supérieure à 500.");
}
else if (!(value % 5))
{
    printf("La valeur est paire, inférieur à 500 et divisible par 5.");
}
else
{
    printf("La valeur ne satisfait aucune condition établie.");
}

Exercice 7.1

Comment se comporte l'exemple suivant :

if (!(i < 8) && !(i > 8))
    printf("i is %d\n", i);

Exercice 7.2

Compte tenu de la déclaration int i = 8;, indiquer pour chaque expression si elles impriment ou non i vaut 8:

  1. if (!(i < 8) && !(i > 8)) then
        printf("i vaut 8\n");
    
  2. if (!(i < 8) && !(i > 8))
        printf("i vaut 8");
        printf("\n");
    
  3. if !(i < 8) && !(i > 8)
        printf("i vaut 8\n");
    
  4. if (!(i < 8) && !(i > 8))
        printf("i vaut 8\n");
    
  5. if (i = 8) printf("i vaut 8\n");
    
  6. if (i & (1 << 3)) printf("i vaut 8\n");
    
  7. if (i ^ 8) printf("i vaut 8\n");
    
  8. if (i - 8) printf("i vaut 8\n");
    
  9. if (i == 1 << 3) printf ("i vaut 8\n");
    
  10. if (!((i < 8) || (i > 8)))
        printf("i vaut 8\n");
    

Note

Notons que formellement, la grammaire C ne connait pas else if il s'agit d'une construction implicite dans laquelle un if ou if..else est la condition du else parent. Hiérarchiquement, on devrait écrire :

if (x)
    a = 1;
else
    if (y)
        a = 2;
    else
        if (z)
            a = 3;
        else
            a = 4;

Pour preuve, il suffit de jeter un oeil à la grammaire C :

selection_statement
    : IF '(' expression ')' statement
    | IF '(' expression ')' statement ELSE statement
    | SWITCH '(' expression ')' statement
    ;

7.2.2 switch

L'instruction switch n'est pas fondamentale et certain langage de programmation comme Python ne la connaisse pas. Elle permet essentiellement de simplifier l'écriture pour minimiser les répétitions. On l'utilise lorsque les conditions multiples portent toujours sur la même variable. Par exemple, le code suivant peut être réécrit plus simplement en utilisant un switch :

if (defcon == 1)
    printf("Guerre nucléaire imminente");
else if (defcon == 2)
    printf("Prochaine étape, guerre nucléaire");
else if (defcon == 3)
    printf("Accroissement de la préparation des forces");
else if (defcon == 4)
    printf("Mesures de sécurité renforcées et renseignements accrus");
else if (defcon == 5
    printf("Rien à signaler, temps de paix");
else
    printf("ERREUR: Niveau d'alerte DEFCON invalide");

Voici l'expression utilisant switch. Notez que chaque condition est plus clair :

switch (defcon)
{
    case 1 :
        printf("Guerre nucléaire imminente");
        break;
    case 2 :
        printf("Prochaine étape, guerre nucléaire");
        break;
    case 3 :
        printf("Accroissement de la préparation des forces");
        break;
    case 4 :
        printf("Mesures de sécurité renforcées et renseignements accrus");
        break;
    case 5 :
        printf("Rien à signaler, temps de paix");
        break;
    default :
        printf("ERREUR: Niveau d'alerte DEFCON invalide");
}

La valeur par défaut default est optionnelle mais recommandée pour traiter les cas d'erreurs possibles.

La structure d'un switch est composée d'une condition switch (condition) suivie d'une séquence {}. Les instructions de cas case 42: sont appelés labels. Notez la présence de l'instruction break qui est nécessaire pour terminer l'exécution de chaque condition. Par ailleurs, les labels peuvent être chaînés sans instructions intermédiaires ni break:

switch (coffee)
{
    case IRISH_COFFEE :
        add_whisky();

    case CAPPUCCINO :
    case MACCHIATO :
        add_milk();

    case ESPRESSO :
    case AMERICANO :
        add_coffee();
        break;

    default :
        printf("ERREUR 418: Type de café inconnu");
}

Notons quelques observations :

  • La structure switch bien qu'elle puisse toujours être remplacée par une structure if..else if est généralement plus élégante et plus lisible. Elle évite par ailleurs de répéter la condition plusieurs fois (c.f. Section 24.2.1).

  • Le compilateur est mieux à même d'optimiser un choix multiple lorsque les valeurs scalaires de la condition triées se suivent directement e.g. {12, 13, 14, 15}.

  • L'ordre des cas d'un switch n'a pas d'importance, le compilateur peut même choisir de réordonner les cas pour optimiser l'exécution.

7.3 Les boucles

../_images/road-runner.svg

Fig. 7.2 Bien choisir sa structure de contrôle

Une boucle est une structure itérative permettant de répéter l'exécution d'une séquence. En C il existe trois types de boucles :

  • for

  • while

  • do .. while

../_images/for.svg

Fig. 7.3 Aperçu des trois structure de boucles

7.3.1 while

La structure while répète une séquence tant que la condition est vraie.

Dans l'exemple suivant tant que le poids d'un objet déposé sur une balance est inférieur à une valeur constante, une masse est ajoutée et le système patiente avant stabilisation.

while (get_weight() < 420 /* newtons */)
{
    add_one_kg();
    wait(5 /* seconds */);
}

Séquentiellement une boucle while teste la condition, puis exécute la séquence associée.

Exercice 7.3

Comment se comportent ces programmes :

  1. size_t i=0;while(i<11){i+=2;printf("%i\n",i);}

  2. i=11;while(i--){printf("%i\n",i--);}

  3. i=12;while(i--){printf("%i\n",--i);}

  4. i = 1;while ( i <= 5 ){ printf ( "%i\n", 2 * i++ );}

  5. i = 1; while ( i != 9 ) { printf ( "%i\n", i = i + 2 ); }

  6. i = 1; while ( i < 9 ) { printf ( "%i\n", i += 2 ); break; }

  7. i = 0; while ( i < 10 ) { continue; printf ( "%i\n", i += 2 ); }

7.3.2 do..while

De temps en temps il est nécessaire de tester la condition à la sortie de la séquence et non à l'entrée. La boucle do...while permet justement ceci :

size_t i = 10;

do {
    printf("Veuillez attendre encore %d seconde(s)\r\n", i);
    i -= 1;
} while (i);

Contrairement à la boucle while, la séquence est ici exécutée au moins une fois.

7.3.3 for

La boucle for est un while amélioré qui permet en une ligne de résumer les conditions de la boucle :

for (/* expression 1 */; /* expression 2 */; /* expression 3 */)
{
    /* séquence */
}
Expression 1

Exécutée une seule fois à l'entrée dans la boucle, c'est l'expression d'initialisation permettant par exemple de déclarer une variable et de l'initialiser à une valeur particulière.

Expression 2

Condition de validité (ou de maintien de la boucle). Tant que la condition est vraie, la boucle est exécutée.

Expression 3

Action de fin de tour. À la fin de l'exécution de la séquence, cette action est exécutée avant le tour suivant. Cette action permet par exemple d'incrémenter une variable.

Voici comment répéter 10x un bloc de code :

for (size_t i = 0; i < 10; i++)
{
    something();
}

Notons que les portions de for sont optionnels et que la structure suivante est strictement identique à la boucle while:

for (; get_weight() < 420 ;)
{
    /* ... */
}

Exercice 7.4

Comment est-ce que ces expressions se comportent-elles ?

int i, k;
  1. for (i = 'a'; i < 'd'; printf ("%i\n", ++i));

  2. for (i = 'a'; i < 'd'; printf ("%c\n", ++i));

  3. for (i = 'a'; i++ < 'd'; printf ("%c\n", i ));

  4. for (i = 'a'; i <= 'a' + 25; printf ("%c\n", i++ ));

  5. for (i = 1 / 3; i ; printf("%i\n", i++ ));

  6. for (i = 0; i != 1 ; printf("%i\n", i += 1 / 3 ));

  7. for (i = 12, k = 1; k++ < 5 ; printf("%i\n", i-- ));

  8. for (i = 12, k = 1; k++ < 5 ; k++, printf("%i\n", i-- ));

Exercice 7.5

Identifier les deux erreurs dans ce code suivant :

for (size_t = 100; i >= 0; --i)
    printf("%d\n", i);

Exercice 7.6

Écrivez un programme affichant les entiers de 1 à 100 en employant :

  1. Une boucle for

  2. Une boucle while

  3. Une boucle do..while

Quelle est la structure de contrôle la plus adaptée à cette situation ?

Exercice 7.7

Expliquez quelle est la fonctionnalité globale du programme ci-dessous :

int main(void) {
    for(size_t i = 0, j = 0; i * i < 1000; i++, j++, j %= 26, printf("\n"))
        printf("%c", 'a' + (char)j);
}

Proposer une meilleure implémentation de ce programme.

7.3.4 Boucles infinies

Une boucle infinie n'est jamais terminée. On rencontre souvent ce type de boucle dans ce que l'on appelle à tort La boucle principale aussi nommée run loop. Lorsqu'un programme est exécuté bare-metal, c'est à dire directement à même le microcontrôleur et sans système d'exploitation, il est fréquent d'y trouver une fonction main telle que :

void main_loop()
{
    // Boucle principale
}

int main(void)
{
    for (;;)
    {
        main_loop();
    }
}

Il y a différentes variantes de boucles infinies :

for (;;) { }

while (true) { }

do { } while (true);

Notions que l'expression while (1) que l'on rencontre fréquemment dans des exemples est faux syntaxiquement. Une condition de validité devrait être un booléen, soit vrai, soit faux. Or, la valeur scalaire 1 devrait préalablement être transformée en une valeur booléenne. Il est donc plus juste d'écrire while (1 == 1) ou simplement while (true).

On préférera néanmoins l'écriture for (;;) qui ne fait pas intervenir de conditions extérieures, car, avant C99 définir la valeur true était à la charge du développeur et on pourrait s'imaginer cette plaisanterie de mauvais goût :

_Bool true = 0;

while (true) { /* ... */ }

Lorsque l'on a besoin d'une boucle infinie, il est généralement préférable de permettre au programme de se terminer correctement lorsqu'il est interrompu par le signal SIGINT (c.f. Section 8.1.4). On rajoute alors une condition de sortie à la boucle principale :

#include <stdlib.h>
#include <signal.h>
#include <stdbool.h>

static volatile bool is_running = true;

void sigint_handler(int dummy)
{
    is_running = false;
}

int main(void)
{
    signal(SIGINT, sigint_handler);

    while (is_running)
    {
       /* ... */
    }

    return EXIT_SUCCESS;
}

7.4 Les sauts

Il existe 4 instructions en C permettant de contrôler le déroulement de l'exécution d'un programme. Elles déclenchent un saut inconditionnel vers un autre endroit du programme.

  • break interrompt la structure de contrôle en cours. Elle est valide pour :
    • while

    • do...``while``

    • switch

  • continue: saute un tour d'exécution dans une boucle

  • goto: interrompt l'exécution et saute à un label situé ailleurs dans la fonction

  • return

7.4.1 goto

Il s'agit de l'instruction la plus controversée en C. Cherchez sur internet et les détracteurs sont nombreux, et ils ont partiellement raison, car dans la très vaste majorité des cas où vous pensez avoir besoin de goto, une autre solution plus élégante existe.

Néanmoins, il est important de comprendre que goto était dans certain langage de programmation comme BASIC, la seule structure de contrôle disponible permettant de faire des sauts. Elle est par ailleurs le reflet du langage machine, car la plupart des processeurs ne connaissent que cette instruction souvent appelée JUMP. Il est par conséquent possible d'imiter le comportement de n'importe quelle structure de contrôle si l'on dispose de if et de goto.

goto effectue un saut inconditionnel à un label défini en C par un identificateur suivi d'un :.

L'un des seuls cas de figure autorisés est celui d'un traitement d'erreur centralisé lorsque de multiples points de retours existent dans une fonction ceci évitant de répéter du code :

#include <time.h>

int parse_message(int message)
{
    struct tm *t = localtime(time(NULL));
    if (t->tm_hour < 7) {
        goto error;
    }

    if (message > 1000) {
        goto error;
    }

    /* ... */

    return 0;

    error:
        printf("ERROR: Une erreur a été commise\n");
        return -1;
}

7.4.2 continue

Le mot clé continue ne peut exister qu'à l'intérieur d'une boucle. Il permet d'interrompre le cycle en cours et directement passer au cycle suivant.

uint8_t airplane_seat = 100;

while (--airplane_seat)
{
    if (airplane_seat == 13) {
        continue;
    }

    printf("Dans cet avion il y a un siège numéro %d\n", airplane_seat);
}

Cette structure est équivalente à l'utilisation d'un goto avec un label placé à la fin de la séquence de boucle, mais promettez-moi que vous n'utiliserez jamais cet exemple :

while (true)
{
    if (condition) {
        goto next;
    }

    /* ... */

    next:
}

7.4.3 break

Le mot-clé break peut être utilisé dans une boucle ou dans un switch. Il permet d'interrompre l'exécution de la boucle ou de la structure switch la plus proche. Nous avions déjà évoqué l'utilisation dans un switch (c.f. Section 7.2.2).

7.4.4 return

Le mot clé return suivi d'une valeur de retour ne peut apparaître que dans une fonction dont le type de retour n'est pas void. Ce mot-clé permet de stopper l'exécution d'une fonction et de retourner à son point d'appel.

void unlock(int password)
{
    static tries = 0;

    if (password == 4710 /* MacGuyver: A Retrospective 1986 */) {
        open_door();
        tries = 0;
        return;
    }

    if (tries++ == 3)
    {
        alert_security_guards();
    }
}

Exercice 7.8

Considérons les déclarations suivantes :

long i = 0;
double x = 100.0;

Indiquer la nature de l'erreur dans les expressions suivantes :

  1. do
        x = x / 2.0;
        i++;
    while (x > 1.0);
    
  2. if (x = 0)
        printf("0 est interdit !\n");
    
  3. switch(x) {
        case 100 :
            printf("Bravo.\n");
            break;
        default :
            printf("Pas encore.\n");
    
    }
    
  4. for (i = 0 ; i < 10 ; i++);
        printf("%d\n", i);
    
  5. while i < 100 {
        printf("%d", ++i);
    }
    

Exercice 7.9

Parmi les cas suivants, quelle structure de contrôle utiliser ?

  1. Test qu'une variable est dans un intervalle donné.

  2. Actions suivant un choix multiple de l'utilisateur

  3. Rechercher un caractère particulier dans une chaîne de caractère

  4. Itérer toutes les valeurs paires sur un intervalle donné

  5. Demander la ligne suivante du télégramme à l'utilisateur jusqu'à STOP

Exercice 7.10

Un texte est passé à un programme par stdin. Comptez le nombre de caractères transmis.

$ echo "Hello world" | count-this
11

Exercice 7.11

Quel est le problème avec cette ligne de code ?

if (x&mask==bits)