Cours 2 · Structures de contrôle et fonctionsLeçon 1 sur 2
Contrôle du flux
5 h de lecture8 sections Version PDF
if et switch, while, do…while et for, break et continue ; traduire en C le pseudocode d'Algorithmique 1.
En février 2014, Apple corrige en urgence une faille présente dans iOS et macOS. Le code fautif vérifiait un certificat TLS, et il contenait ceci :
if ((err = SSLHashSHA1.update(&hashCtx, &signedParams)) != 0) goto fail; goto fail; /* ← cette ligne est en trop */if ((err = SSLHashSHA1.final(&hashCtx, &hashOut)) != 0) goto fail;La seconde ligne goto fail n'appartient à aucun if : l'indentation le suggère, mais il n'y a
pas d'accolades, donc un if sans accolades ne gouverne que l'instruction suivante. Le saut
est donc inconditionnel, toute la suite de la vérification est sautée, et la fonction rend
err = 0, c'est-à-dire « certificat valide ». N'importe quel certificat.
Ce chapitre traite des structures de contrôle du C. Elles ne présentent aucune difficulté conceptuelle pour qui a suivi Algorithmique 1 — ce sont les mêmes — mais leur syntaxe recèle une poignée de pièges dont celui-ci est le plus célèbre.
Une condition est un entier
C'est le point de départ, et il explique presque tous les pièges qui suivent. Le C n'a pas de type booléen dans sa version historique : une condition est une expression entière, où zéro est faux et toute autre valeur est vraie.
if (3) /* vrai */if (0) /* faux */if (-1) /* vrai : seul zéro est faux */Trois conséquences pratiques.
if (x = 5) compile. L'affectation est une expression, dont la valeur est 5, donc la
condition est vraie — et x a été modifié au passage. C'est l'erreur de frappe la plus coûteuse
du C, et -Wall la signale. Certains l'évitent en écrivant if (5 == x), la constante à
gauche : une faute de frappe donnerait alors une erreur de compilation.
if (strcmp(a, b)) est inversé par rapport à l'intuition. strcmp rend 0 quand les chaînes
sont égales, donc cette condition est vraie quand elles diffèrent. On écrit
if (strcmp(a, b) == 0) pour tester l'égalité.
Enfin, stdbool.h fournit depuis C99 bool, true et false. Ce n'est qu'un habillage —
true vaut 1 — mais il rend les intentions lisibles, et on l'emploiera.
Conditionnelles
if (note >= 16) { printf("Très bien\n");} else if (note >= 14) { printf("Bien\n");} else if (note >= 10) { printf("Passable\n");} else { printf("Insuffisant\n");}L'ordre des tests décide de tout — c'est déjà l'exercice de la cascade d'Algorithmique 1 : le test le plus exigeant en premier, sans quoi les suivants deviennent inatteignables.
Et la règle que l'introduction impose : toujours mettre les accolades, même pour une seule
instruction. Le coût est nul, le bénéfice est d'éliminer une classe entière de bogues — celui
d'Apple, et celui, plus banal, d'ajouter une ligne à un if sans s'apercevoir qu'elle en sort.
Le switch compare une expression entière à des constantes :
switch (choix) { case 1: printf("Ajouter\n"); break; /* sans lui, on continue sur le cas 2 */ case 2: printf("Supprimer\n"); break; default: printf("Choix inconnu\n");}Le piège est le glissement (fallthrough) : sans break, l'exécution continue dans le cas
suivant. Ce n'est pas un défaut de conception mais une fonctionnalité — elle permet de
regrouper plusieurs cas partageant un traitement —, seulement l'oubli est fréquent, et gcc
propose -Wimplicit-fallthrough pour le signaler.
Deux limites du switch : l'expression doit être entière (pas de chaînes, pas de
flottants), et les étiquettes doivent être des constantes. Un test sur un intervalle
demande un if.
Boucles
Les trois boucles du C se distinguent par le moment du test.
while (condition) { ... } /* test AVANT : zéro tour possible */ do { ... } while (condition); /* test APRÈS : au moins un tour */ for (init; condition; incrément) { ... }Le for du C est plus général qu'il n'y paraît : ses trois parties sont des expressions
quelconques, et chacune peut être vide. for (;;) est une boucle infinie parfaitement
idiomatique.
Le do...while a un usage précis : la saisie contrôlée, où l'on doit demander au moins une fois
avant de pouvoir tester. C'est aussi le seul dont le point-virgule final est obligatoire, et
l'oublier produit un message d'erreur particulièrement obscur.
Deux fautes classiques méritent d'être nommées.
Le point-virgule fantôme : for (int i = 0; i < n; i++); suivi d'un bloc. La boucle a pour
corps l'instruction vide, elle tourne n fois sans rien faire, puis le bloc s'exécute une
seule fois. Aucun avertissement par défaut.
Le compteur non modifié : oublier l'incrément dans un while donne une boucle infinie —
c'est la faute déjà signalée dans le chapitre 6 d'architecture, où l'incrément est explicite en
assembleur.
Enfin, break sort de la boucle la plus interne et continue passe au tour suivant. Sortir
de deux boucles imbriquées demande donc un drapeau, une fonction, ou — cas où il est
défendable — un goto.
Que fait ce code ? int x = 3 ; if (x = 0) printf('nul') ; else printf('%d', x) ;
Traduire le pseudocode d'Algorithmique 1
C'est l'exercice central du chapitre, et il est presque mécanique.
| Pseudocode | C |
|---|---|
Lire(x) | scanf("%d", &x); |
Écrire(x) | printf("%d\n", x); |
x ← 5 | x = 5; |
Si c Alors ... FinSi | if (c) { ... } |
Pour i de 1 à n | for (int i = 1; i <= n; i++) |
Tant que c Faire | while (c) { ... } |
T[i] (indices de 1 à n) | T[i-1] (indices de 0 à n−1) |
La dernière ligne est la seule vraie difficulté, et elle vaut un avertissement. Le pseudocode du cours numérote souvent les tableaux à partir de 1 ; le C numérote à partir de 0. Une traduction distraite produit soit un décalage d'un rang, soit un débordement d'une case — les deux fautes les plus fréquentes du bloc III, et le C ne signalera ni l'une ni l'autre.
La bonne pratique n'est pas de traduire T[i] en T[i-1] mais de réécrire les bornes :
for (int i = 0; i < n; i++), avec un < strict. Cet idiome donne exactement n tours et des
indices valides de 0 à n−1 ; il vaut mieux l'adopter une fois pour toutes que de compter à
chaque boucle.
Un switch sur un menu affiche « Ajouter » puis « Supprimer » quand l'utilisateur choisit 1. Quelle est la cause ?
À vous
L'exercice traduit du pseudocode en C — simulé en JavaScript, dont la syntaxe de contrôle est assez proche pour que la traduction reste fidèle — puis reproduit les trois pièges du chapitre.
D'abord la classe de bogue d'Apple : un bloc conditionnel sans accolades auquel on ajoute une
ligne, et l'effet sur le résultat. Ensuite le glissement de switch, avec le cas où il est
volontaire et celui où il est fautif. Enfin le point-virgule fantôme après un for.
La dernière partie est la plus utile : la traduction d'un algorithme d'Algorithmique 1 — somme, maximum, recherche — avec des indices à partir de 0, et un test qui vérifie qu'aucune case n'a été lue hors bornes.
Reproduisez le bogue « goto fail », le glissement de switch, et corrigez une traduction d'indices.
// ── 1. La classe de bogue « goto fail » ─────────────────────────────────── // Un if sans accolades ne gouverne QUE l'instruction suivante. function verifier(etapes) { let erreur = 0; for (const e of etapes) { if ((erreur = e.resultat) !== 0) return "ÉCHEC à l'étape " + e.nom; // ← quelqu'un a dupliqué la ligne ci-dessus lors d'une fusion. // Ajoutez « return "ÉCHEC (ligne en trop)"; » ici et observez. } return "certificat valide"; } // ── 2. Glissement de switch ─────────────────────────────────────────────── function menu(choix) { const sortie = []; switch (choix) { case 1: sortie.push("Ajouter"); // ← break manquant case 2: sortie.push("Supprimer"); break; case 3: case 4: sortie.push("Lister"); // glissement VOLONTAIRE : 3 et 4 font pareil break; default: sortie.push("Choix inconnu"); } return sortie.join(" puis "); } // ── 3. Traduire le pseudocode d'Algorithmique 1 ─────────────────────────── // Pseudocode, indices de 1 à n : // max ← T[1] // Pour i de 2 à n Faire // Si T[i] > max Alors max ← T[i] // // Traduction en C, indices de 0 à n−1. Le tableau est instrumenté : tout // accès hors bornes est enregistré au lieu de passer inaperçu. function tableauSurveille(valeurs) { const hors = []; return { n: valeurs.length, lire(i) { if (i < 0 || i >= valeurs.length) { hors.push(i); return undefined; } return valeurs[i]; }, horsBornes: () => hors, }; } function maximum(T) { let max = T.lire(1); // ← traduction distraite de T[1] for (let i = 2; i <= T.n; i++) { if (T.lire(i) > max) max = T.lire(i); } return max; } // ── À VOUS ──────────────────────────────────────────────────────────────── // 1. Ajoutez la ligne en trop dans verifier() et constatez l'effet. // 2. Complétez menu() pour que le choix 1 n'affiche que « Ajouter ». // 3. Corrigez maximum() : bornes de 0 à n−1, avec un < strict. Vérifiez // qu'aucun accès hors bornes n'est enregistré. const T = tableauSurveille([3, 17, 8, 42, 5]); console.log("maximum :", maximum(T), "| hors bornes :", JSON.stringify(T.horsBornes()));
En travaux pratiques
Les pièges du contrôle
Rencontrer, dans du code qui compile sans le moindre avertissement, les quatre erreurs de structure qui coûtent le plus de temps en C.
- Les TP 1 et 2
- 1. Le point-virgule
Écrivez une boucle qui compte de 1 à 10 en plaçant un point-virgule juste après la parenthèse fermante du for. Compilez avec -Wall et exécutez. Que se passe-t-il ?
- 2. L'affectation dans le test
Écrivez un si qui compare une variable à 5 en utilisant un seul signe égal. Exécutez. Puis trouvez l'écriture qui aurait fait échouer la compilation.
- 3. Le switch sans break
Écrivez un aiguillage à quatre cas sans aucun break. Exécutez avec chaque valeur et notez les sorties. Trouvez ensuite un cas où l'absence de break est VOULUE.
- 4. Le bloc manquant
Écrivez un si sans accolades suivi de deux instructions indentées identiquement. Exécutez, et expliquez pourquoi l'indentation vous a menti.
- 5. Boucle et condition composée
Écrivez une recherche dans un tableau qui s'arrête dès qu'elle trouve. Écrivez-la de trois façons : avec break, avec un drapeau, avec la condition dans le while. Comparez la lisibilité.
- 6. Au fil rouge
Ajoutez à journal le comptage des lignes contenant un mot donné, avec un rapport final. Testez sur un fichier vide, un fichier d'une ligne, et un gros fichier.
- Vous savez expliquer sans exécuter ce qu'affiche une boucle terminée par un point-virgule
- Vous écrivez vos comparaisons de façon qu'un signe égal oublié ne compile pas
- Votre journal donne le bon compte sur un fichier vide
Ce que la suite en fait
Le chapitre 4 découpe le programme en fonctions, ce qui introduit la vraie difficulté du bloc
II : le passage par valeur. Une fonction reçoit des copies, donc elle ne peut pas modifier
les variables de son appelant — et le premier réflexe, écrire une fonction echanger(a, b), ne
fonctionne pas. La solution demande le bloc IV.
Les boucles reviendront aussi au chapitre 5 : parcourir un tableau est la construction la plus fréquente du langage, et c'est là que le décalage d'indice se paie.
À retenir
Vous avez parcouru les 8 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.