Structures de contrôle

Un programme évolué peut nécessiter de sauter à une autre ligne que celle qui se trouve immédiatement après.

Saut inconditionnel

Un programme C s’écoule naturellement d’instruction en instruction, de haut en bas, de gauche à droite. On peut altérer ce cours de plusieurs manières en C.

Appel de fonction

L’appel de fonction permet de quitter la fonction où on se trouve, d’exécuter une autre fonction puis de revenir à l’endroit où on se trouvait. La machine virtuelle du C garde en mémoire le lieu d’appel de la fonction pour y revenir une fois que la fonction appellée s’est exécutée complètement.

Cette pile est limitée en taille et varie selon l’architecture de l’ordinateur.

Elle place un marqueur dans l’emplacement actuel du code exécuté, effectue un saut, crée éventuellement des variables, et au retour de la fonction le fil d’exécution reprend où il était.

Une fonction peut en apeller une autre et s’apeller elle même, ou en appeller d’autres qui en appellent d’autres qui appellent la fonction d’origine. La mémoire d’appel est limitée donc il faut éviter d’abuser d’appels récursifs. Par exemple un parseur où chaque node appellerait une fonction pour parser le node suivant d’un texte causerait une panne à cause d’un débordement de pile (stack overflow) à partir d’une certaine longueur de texte à parser.

Goto

Le goto permet de se déplacer à l’intérieur d’une fonction à n’importe quel endroit de celle-ci. L’instruction gogo prend en paramètre un label nommé. On donne un label à une instruction en la faisant précéder d’un nom et d’un deux points.

void fun() {

    goto tata;
    toto:
    puts("toto");
    tata:
    puts("tata");  // seule cette instruction est exécutée
}

Ici le programme sautera l’affichage de “toto”.

Seule une instruction peut être précédée d’un label, c’est à dire qu’une expression ne le peut pas, ni une accolade fermante (sauf depuis le C23 ):

void fun() {
    int b = 1;
    
    goto tata;
    int a = toto:b + tata:1; // illegal
    titi:   // illegal, autorisé en C23
}

void fun2() {

    toto:; // avant le c23 on ajoute un instruction vide
}

On peut avec un goto sauter vers l’intérieur d’un bloc et sortir d’autant de blocs que l’on souhaite. Dans ce cas les variables déclarées dans ce bloc ont de l’espace aloué pour elles, mais les instructions d’initialisation sont sautées.

Il sera toutefois possible d’y alouer des valeurs, qui seront à l’endroit correct de la pile.

void fun() {

    int a = 4;
    goto toto; // ok
    {
        int a = 2; // on aloue de l'espace pour 
        // un nouveau a, mais on n'exécute pas 
        // l'initialisation
        toto:
        printf("%d\n", a); // valeur indéfinie
        a = 3;
        printf("%d\n", a); // 3
    }

    printf("%d\n", a); // 4
}

La portee d’un goto est limitée à sa fonction. On ne peut pas sauter d’une fonction vers une autre. Un label peut être déclaré après son goto correspondant (pas besoin de le déclarer “avant” son invocation comme avec une variable).

Deux fonctions differentes peuvent avoir un label avec le même nom. Un nom de label doit être unique au sein d’une fonction c’est à dire qu’un bloc ne peut pas redéfinir un label déclaré dans un bloc supérieur.

void fun() {
    toto:
    tata:
}

void fun2() {

    toto: // ok
    goto tata; // illegal

    {
        toto: // illegal
    }
}

Le nom d’un label suit les mêmes règles que celles des variables, une succession de lettres minuscules, majuscules, de chiffres et de souligné « _ ». Le nom ne peut pas commencer par un chiffre et il est sensible à la casse.

Setjmp/lngjmp

Le header <setjmp.h> définit le type opaque jmp_buf, la fonction int setjmp(jmp_buf) et la fonction void longjmp(jmp_buf, int).

longjmp() après un appel à setjmp() permet de sauter depuis une fonction vers une autre fonction au point où setjmp() a été appelé.

Par retrocompatibilité, le type jmp_buf est considéré comme de type tableau car il est n’est pas passé avec l’opérateur & dans les fonctions setjmp() et longjmp().

Un appel à setjmp() sauvegarde l’état local de la fonction en cours dans un buffer de type jmp_buf et retourne 0.

Un appel à longjmp() restaure l’état de la pile tel qu’il a été sauvegardé, et restaure l’état du programme au moment où l’appel à la fonction setjmp() retourne. La valeur retournée par setjmp() cette fois ci sera celle qu’on passe par le deuxième paramètre de longjmp(). Si la veleur passée est 0, elle sera transformée en 1.

On utilise généralement cette construction pour la gestion d’erreurs ou implémenter des coroutines.

Le comportement de cette fonction est disparate selon les implémentations, et elle interdit de nombreuses optimisations sur la pile, il est généralement peu courant d’en voir ailleurs que dans du code spécifique à un environnement donné, ni dans du code où la vitesse d’exécution est critique.

Voila un programme d’exemple.

#include <stdio.h>
#include <setjmp.h>

static jmp_buf buf;

void danger() {

    puts("danger");
    longjmp(buf, 42);
}

void normal() {
    int err;
    if((err = setjmp(buf)) == 0) {
        puts("normal");
        danger();
        puts("normal2");
    } else { // gestion d'erreur
        printf("erreur %d \n", err);
    }

}

int main() {
    normal();
    // affiche
    // "normal"
    // "danger"
    // "erreur 42"
}

Structures conditionelles

Dans les années 50, on considérait le branchement conditionnel suivant la manière qu’il était traité au niveau de la machine, c’est à dire via la soustraction 🌍⤴ et la recursion lorsque plus de deux états logiques étaient en cause.

Le langage C implémente le formalisme de Mc Carthy crée dans les années 1960n avec mots-clés if, else, while, switch, etc.

L’allure d’un programme C est pour cette raison assez differente des autres formalismes mathématiques, c’est à dire qu’un programme C a une allure

D’autres langages que le C implémentent davantage de mots-clés et principes de ce paradigme. Le python par exemple permet de coupler le while et le else. Le php et le java permettent de break de plusieurs boucles imbriquées. Nous allons voir à quoi correspondent ces mots-clés mais gardez à l’esprit que l’implémentation de ce paradigme en C n’a que peu évolué depuis ses débuts et il est à certains égards moins complet que dans d’autres langages.

Ce paradigme évolue parallèlement au C par les apports des autres langages, certaines constructions structurées permises dans d’autres langages ne le sont pas en C, et vous vous trouverez dans certaines circonstances obligé d’utiliser un goto : sortir d’une boucle dans un switch, sortir de deux boucles imbriquées, inverser la logique sans augmenter l’indentation du code, etc.

If: branchement conditionnel

L’instruction « if » permet de donner au programme deux embranchements possibles à suivre.

Cette instruction est suivie d’une clause de test et d’une instruction qui s’exécute si la clause de test est valide au point d’exécution du programme.

#include <stdio.h>


int main() {

    puts("Voulez vous aller a gauche (g) ou a droite (d) ? ");
    char r = getchar();

    if(r == 'g')
        puts("Vous allez à gauche.");

    if(r == 'd') {
        puts("Vous allez à droite.");
        r = 'g';
        puts("Puis marchez un peu.");
    }

    puts("fin");
}

Ici le programme demande a l’utilisateur d’entrer un caractère, et n’exécute certaines portions de code que si la clause de test est valide au point d’exécution du programme.

Il n’importe pas que la condition soit valide ou non à un point donné avant ou après cette clause, seule compte ici la validité au point t où la clause s’exécute.

L’instruction qui suit peut être une instruction simple ou une instruction composée.

Blocs d’instructions

Une instruction composée est composée d’une paire d’accolades qui regroupe zero ou plus instructions, toutes terminées par un point virgule.

    {
        puts("un");
        2+2;
        puts("deux");
    }

Il est assez rare de trouver des accolades dans la nature ailleurs que dans la notation mathématique d’un ensemble 🌍⤴ qu’on peut noter

$$\{ 1, 2, 4 \};$$

ou

$$\{ 1; 2; 4 \}.$$

A noter que le langage Pascal utilise le point-virgule comme séparateur d’instructions alors que le C utilise le point-virgule comme terminateur d’instruction.

Par ailleurs bien que la notation s’inspire des ensembles, le comportement d’un bloc d’instructions est plus proche d’un uplet 🌍⤴ non ordonné.

Ici trois instructions sont groupées et au vu d’une instruction if seront toutes exécutées en un seul tenant. Si pendant l’exécution du bloc, la clause de test venait à être invalidée, le bloc continuera son exécution.

    if(r == 'd') {
        puts("Vous allez à droite.");
        r = 'g';
        puts("Puis marchez un peu.");
    }

Ici r = 'g'; invalide la clause de test, mais cela n’interrompt pas l’exécution du bloc. De même le programme ne va pas s’exécuter à rebours car la première clause de test est devenue valide.

Pour exécuter du code a rebours, il faut le specifier avec une instruction de saut.

#include <stdio.h>


int main() {

    puts("Voulez vous aller a gauche (g) ou a droite (d) ? ");
    char r = getchar();

    top:
    if(r == 'g')
        puts("Vous allez à gauche.");

    if(r == 'd') {
        puts("Vous allez à droite.");
        r = 'g';
        puts("Puis marchez un peu.");
        goto top;
    }

    puts("fin");
}

Si l’utilisateur entre 'd', le programme affichera

"Vous allez à droite."
"Puis marchez un peu."
"Vous allez à gauche."
"fin"

Bien qu’une instruction simple ou composée soit identique aux yeux d’une instruction if, il faut faire attention aux risques posés pqs des instructions simples

    if(r == 'g')
        puts("Vous allez à gauche.");
        puts("fin");

correspond en fait à écrire

    if(r == 'g') {
        puts("Vous allez à gauche.");
    }
    puts("fin");

c’est à dire que dans les deux cas l’instruction puts("fin"); s’exécute indépendament de la clause de test, mais avec les accolades c’est plus visible. Certaines conventions de style ou des normes de robustesse demandent que toute instruction attachée à une clause de controle soit sous forme composée. npm u

Opérateurs logiques

On utilise les opérateurs « et logique » &&, « ou logique » || et négation logique ! pour articuler plusieurs tests selon une logique booléenne.

Il n’existe pas de ou exclusif logique, il faut le noter de toute lettre (a && !b) || (!b && a).

Ces opérateurs renvoyent 1 ou 0 selon que les prédicats soient faux ou vrais.

Dans le cas du « et logique » &&, les deux prédicats doivent être vrais.

Dans le cas du « ou logique » ||, un des deux prédicats doivent être vrais.

References