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.