Programmation en C · C2 Structures de contrôle et fonctions · Chapitre 1 · 5 h
Contrôle du flux
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.
Quiz · 1 question
Que fait ce code ? int x = 3 ; if (x = 0) printf('nul') ; else printf('%d', x) ;
- Il affiche « nul », car x est comparé à 0 et vaut bien 0 après l'affectation — affiche nul
- Il affiche 0 : l'affectation x = 0 rend la valeur 0, donc la condition est FAUSSE, on part dans le else — et x a été écrasé au passage — affiche 0
- Il ne compile pas : on ne peut pas affecter dans une condition — ne compile pas
Réponse : Trois choses se produisent en même temps. L'affectation x = 0 est une EXPRESSION, dont la valeur est celle affectée, soit 0. Une condition en C est un entier, et zéro est faux : on part donc dans le else. Et x, qui valait 3, a été écrasé — le programme affiche 0. C'est la confusion = / == , et elle est d'autant plus vicieuse que le code compile sans erreur : il est parfaitement légal. Deux parades : activer -Wall, qui signale « suggest parentheses around assignment used as truth value », et prendre l'habitude d'écrire la constante à gauche — if (0 == x) — de sorte qu'une faute de frappe donne une erreur de compilation au lieu d'un bogue silencieux.
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.
Quiz · 1 question
Un switch sur un menu affiche « Ajouter » puis « Supprimer » quand l'utilisateur choisit 1. Quelle est la cause ?
- Le case 1 est mal écrit et intercepte aussi la valeur 2 — case mal écrit
- Il manque un break à la fin du cas 1 : sans lui, l'exécution GLISSE dans le cas suivant, ce qui est le comportement normal du switch en C — glissement
- Le default est placé avant les cas, donc tous les cas sont exécutés — default mal placé
Réponse : Le switch du C ne choisit pas un bloc : il calcule un POINT D'ENTRÉE. Une fois entré au bon case, l'exécution continue en séquence jusqu'à un break ou jusqu'à la fin du switch, en traversant les étiquettes suivantes comme si elles n'existaient pas. Ce n'est pas un défaut mais une fonctionnalité — elle permet d'écrire case 1: case 2: case 3: pour un traitement commun, sans rien répéter. Seulement l'oubli est fréquent et silencieux : gcc propose -Wimplicit-fallthrough pour le signaler, et la convention est de commenter explicitement les glissements volontaires. À noter aussi que la place du default est libre : il n'est atteint que si aucune étiquette ne correspond, où qu'il soit écrit.
À 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.
Exercice de code
Reproduisez le bogue « goto fail », le glissement de switch, et corrigez une traduction d'indices.
Point de départ
// ── 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()));
Solution
function verifierAvecBogue(etapes) {
let erreur = 0;
for (const e of etapes) {
if ((erreur = e.resultat) !== 0)
return "ÉCHEC à l'étape " + e.nom;
return "ÉCHEC (ligne en trop)"; // hors du if : TOUJOURS exécutée
}
return "certificat valide";
}
function verifierCorrige(etapes) {
let erreur = 0;
for (const e of etapes) {
// Les accolades coûtent deux caractères et suppriment la classe entière
// de bogues : la ligne ajoutée est visiblement DANS le bloc, ou hors.
if ((erreur = e.resultat) !== 0) {
return "ÉCHEC à l'étape " + e.nom;
}
}
return "certificat valide";
}
function menu(choix) {
const sortie = [];
switch (choix) {
case 1:
sortie.push("Ajouter");
break; // le break qui manquait
case 2:
sortie.push("Supprimer");
break;
case 3:
case 4:
sortie.push("Lister"); // glissement volontaire, à commenter
break;
default:
sortie.push("Choix inconnu");
}
return sortie.join(" puis ");
}
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 maximumFaux(T) {
let max = T.lire(1);
for (let i = 2; i <= T.n; i++) if (T.lire(i) > max) max = T.lire(i);
return max;
}
function maximumJuste(T) {
// On ne traduit pas T[i] en T[i-1] : on RÉÉCRIT LES BORNES. L'idiome
// « i = 0 ; i < n ; i++ » donne exactement n tours et des indices valides.
let max = T.lire(0);
for (let i = 1; i < T.n; i++) {
const v = T.lire(i);
if (v > max) max = v;
}
return max;
}
console.log("— 1. la ligne en trop —");
const etapes = [{ nom: "hachage", resultat: 0 }, { nom: "signature", resultat: 0 }];
console.log(" avec la ligne en trop :", verifierAvecBogue(etapes));
console.log(" avec les accolades :", verifierCorrige(etapes));
console.log(" (le premier n'a vérifié AUCUNE étape : il sort au premier tour)");
console.log("");
console.log("— 2. switch —");
for (const c of [1, 2, 3, 4, 9]) console.log(" choix " + c + " -> " + menu(c));
console.log("");
console.log("— 3. traduction du pseudocode —");
const A = tableauSurveille([3, 17, 8, 42, 5]);
console.log(" version distraite : max =", maximumFaux(A),
"| accès hors bornes :", JSON.stringify(A.horsBornes()));
const B = tableauSurveille([3, 17, 8, 42, 5]);
console.log(" version corrigée : max =", maximumJuste(B),
"| accès hors bornes :", JSON.stringify(B.horsBornes()));
// La version distraite lit T[5] — une case au-delà — et rate T[0]. Ici le
// maximum tombe juste par chance ; avec [42, 3, 17, 8, 5] elle rendrait 17.
const C = tableauSurveille([42, 3, 17, 8, 5]);
console.log(" sur [42,3,17,8,5] : distraite =", maximumFaux(C),
" (le 42 en tête n'a jamais été lu)");
// En C, cet accès hors bornes ne provoque AUCUNE erreur : il lit l'octet
// suivant en mémoire. C'est le sujet du chapitre 5.
En travaux pratiques
Travaux pratiques 3 · 2 h
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.
Avant de commencer
- Les TP 1 et 2
Énoncé
- 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 ?
- 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. Indice : Mettre la constante à gauche.
- 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.
- 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.
- 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é.
- 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.
C'est réussi quand
- 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
Correction
for (int i = 1; i <= 10; i++); /* ← ce point-virgule */
printf("%d ", i);
/* le corps de la boucle est l'instruction VIDE.
Le printf est exécuté UNE fois, après la boucle —
et ne compile même pas ici, car i n'existe plus. */Une instruction vide est licite en C, et le compilateur n'a rien à redire. gcc signale ce cas avec -Wempty-body, qui n'est pas dans -Wall. C'est la meilleure illustration du principe du langage : le C fait ce que vous écrivez, pas ce que vous vouliez écrire.
if (x = 5) … /* AFFECTE 5 à x, puis teste 5, donc toujours VRAI */ if (x == 5) … /* compare */ /* l'écriture qui protège : la constante à gauche */ if (5 == x) … /* correct */ if (5 = x) … /* ERREUR de compilation : on ne peut affecter à 5 */
En C, une affectation est une EXPRESSION qui a une valeur — c'est ce qui rend l'erreur possible et ce qui permet d'écrire while ((c = getchar()) != EOF). L'écriture inversée est un garde-fou mécanique, et les doubles parenthèses de la seconde forme signalent au compilateur, et au lecteur, que l'affectation est voulue.
switch (n) {
case 1: printf("un ");
case 2: printf("deux ");
case 3: printf("trois ");
default: printf("fin\n");
}
n=1 → « un deux trois fin » n=3 → « trois fin »
/* le cas où c'est VOULU */
switch (c) {
case 'a': case 'e': case 'i': case 'o': case 'u':
voyelles++;
break;
}La traversée est un choix du langage, pas un accident : elle permet de regrouper des cas. Mais comme elle est implicite, l'oubli d'un break ne produit aucun avertissement par défaut. Écrire un commentaire explicite là où la traversée est voulue est la convention universelle — et gcc reconnaît -Wimplicit-fallthrough pour signaler les autres.
if (erreur) fprintf(stderr, "échec\n"); return 1; /* ← TOUJOURS exécuté */ /* le compilateur lit : */ if (erreur) fprintf(…); return 1;
Le C ignore complètement l'indentation ; seules les accolades comptent. Cette faute a produit en 2014 la faille « goto fail » d'Apple, qui désactivait la vérification des certificats TLS sur des millions d'appareils. La règle qui l'élimine tient en une phrase : des accolades TOUJOURS, même pour une seule instruction.
/* 1. break — la plus directe */ for (i = 0; i < n; i++) if (t[i] == cible) break; if (i < n) … /* 2. drapeau — verbeuse, mais parfois nécessaire */ int trouve = 0; for (i = 0; i < n && !trouve; i++) if (t[i] == cible) trouve = 1; /* 3. condition dans le while — la plus dense */ i = 0; while (i < n && t[i] != cible) i++;
Les trois sont correctes. La troisième repose sur l'ÉVALUATION PARESSEUSE : si i < n est faux, t[i] n'est jamais lu — sans cette garantie, elle sortirait du tableau. C'est une propriété du langage, pas un hasard, et elle sera le fondement de la moitié des vérifications de pointeur du TP 7.
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
Flashcards · 4 cartes
- Pourquoi if (x = 5) compile-t-il, et quelles parades ?
- Parce qu'une condition en C est un ENTIER — zéro est faux, tout le reste est vrai — et qu'une affectation est une EXPRESSION dont la valeur est celle affectée. if (x = 5) affecte 5 puis teste 5 : toujours vrai, et x est écrasé. Parades : -Wall le signale, et écrire la constante à gauche — if (5 == x) — transforme la faute de frappe en erreur de compilation. Autre conséquence du même principe : if (strcmp(a,b)) est vrai quand les chaînes DIFFÈRENT.
- Pourquoi faut-il toujours mettre les accolades après un if ?
- Parce qu'un if sans accolades ne gouverne QUE l'instruction suivante, quelle que soit l'indentation. C'est la faille « goto fail » d'Apple en 2014 : une ligne goto fail dupliquée, hors du if faute d'accolades, sautait toute la vérification du certificat TLS et rendait « certificat valide » pour n'importe quel certificat. Coût des accolades : nul. Bénéfice : une classe entière de bogues éliminée.
- Qu'est-ce que le glissement dans un switch, et est-ce un défaut ?
- Le switch calcule un POINT D'ENTRÉE, pas un bloc : une fois entré au bon case, l'exécution continue en séquence jusqu'à un break ou la fin, traversant les étiquettes suivantes. Ce n'est pas un défaut mais une fonctionnalité — elle permet case 1: case 2: case 3: pour un traitement commun. Mais l'oubli est fréquent et silencieux : -Wimplicit-fallthrough le signale, et on commente les glissements volontaires.
- Quel est le piège de la traduction du pseudocode vers le C, et quel idiome adopter ?
- Le pseudocode numérote souvent les tableaux À PARTIR DE 1, le C à partir de 0. Traduire distraitement produit un décalage d'un rang ou un débordement d'une case, et le C ne signale ni l'un ni l'autre. La bonne pratique n'est pas d'écrire T[i-1] mais de RÉÉCRIRE LES BORNES : for (int i = 0; i < n; i++), avec un < strict — exactement n tours, indices valides de 0 à n−1.