Section courante

A propos

Section administrative du site

Contrôle du flux

Le contrôle de flux d'un langage spécifie l'ordre dans lequel les calculs sont effectués. Nous avons déjà rencontré les constructions de contrôle de flux les plus courantes dans des exemples précédents ; nous allons ici compléter l'ensemble et être plus précis sur celles discutées auparavant.

Voici les structures principales de contrôle du flux en C :

Instructions et blocs

Une expression telle que x = 0 ou i++ ou printf(...) devient une instruction lorsqu'elle est suivie d'un point-virgule, comme dans :

  1. x = 0;
  2. i++;
  3. printf(...);

En C, le point-virgule est un terminateur d'instruction, plutôt qu'un séparateur comme dans des langages comme Pascal.

Les accolades { et } sont utilisées pour regrouper des déclarations et des instructions dans une instruction composée, ou un bloc, de sorte qu'elles soient syntaxiquement équivalentes à une seule instruction. Les accolades entourant les instructions d'une fonction en sont un exemple évident ; les accolades autour de plusieurs instructions après un if, else, while ou for en sont un autre. (Les variables peuvent être déclarées à l'intérieur de n'importe quel bloc. Il n'y a pas de point-virgule après l'accolade droite terminant un bloc.

If-Else

L'instruction if-else est utilisée pour exprimer des décisions. Formellement, la syntaxe est :

if (expression)
   statement1
else
   statement2

où la partie else est facultative. L'expression expression est évaluée ; si elle est vraie (c'est-à-dire si expression a une valeur non nulle), l'instruction statement1 est exécutée. Si elle est fausse (expression est nulle) et s'il y a une partie else, l'instruction statement2 est exécutée à la place.

Puisqu'un if teste la valeur numérique d'une expression, certains raccourcis de codage sont possibles. Le plus évident est d'écrire :

if (expression)

au lieu de :

if (expression != 0)

Parfois, cela est naturel et clair ; d'autres fois, cela peut être cryptique.

Étant donné que la partie else d'un if-else est facultative, il existe une ambiguïté lorsqu'un else if est omis d'une séquence if imbriquée. Cela est résolu en associant le else au else-if précédent le plus proche. Par exemple, dans :

  1. if (n > 0)
  2.  if (a > b)
  3.   z = a;
  4.  else
  5.   z = b;

le else va au if interne, comme nous l'avons montré par l'indentation. Si ce n'est pas ce que vous voulez, des accolades doivent être utilisées pour forcer l'association appropriée :

  1. if (n > 0) {
  2.  if (a > b)
  3.   z = a;
  4. }
  5. else
  6.     z = b;

L'ambiguïté est particulièrement pernicieuse dans des situations comme celle-ci :

  1. if (n > 0)
  2.  for (i = 0; i < n; i++)
  3.   if (s[i] > 0) {
  4.    printf("...");
  5.    return i;
  6.   }
  7. else /* MAUVAIS */
  8.     printf("erreur -- n est négatif\n");    

L'indentation montre sans équivoque ce que vous voulez, mais le compilateur ne reçoit pas le message et associe le else au if interne. Ce type de bogue peut être difficile à trouver ; c'est une bonne idée d'utiliser des accolades lorsqu'il y a des if imbriqués.

Au fait, remarquez qu'il y a un point-virgule après z = a dans :

  1. if (a > b)
  2.  z = a;
  3. else
  4.  z = b;    

C'est parce que grammaticalement, une instruction suit le if, et une instruction d'expression comme «z = a ;» est toujours terminée par un point-virgule.

Else-If

La construction :

if (expression)
   statement
else if (expression)
   statement
else if (expression)
   statement
else if (expression)
   statement
else
   statement

se produit si souvent qu'il vaut la peine d'en discuter brièvement à part. Cette séquence d'instructions if est la manière la plus générale d'écrire une décision à plusieurs voies. Les expressions sont évaluées dans l'ordre ; si une expression est vraie, le statement lui étant associé est exécuté, ce qui met fin à toute la chaîne. Comme toujours, le code de chaque statement est soit une instruction unique, soit un groupe d'instructions entre accolades.

La dernière partie else gère le cas «aucun des cas ci-dessus» ou par défaut où aucune des autres conditions n'est satisfaite. Parfois, il n'y a aucune action explicite pour la valeur par défaut ; dans ce cas, la dernière partie :

else
   statement

peut être omis, ou il peut être utilisé pour la vérification des erreurs afin de détecter une condition «impossible».

Pour illustrer une décision à trois voies, voici une fonction de recherche binaire décidant si une valeur particulière x apparaît dans le tableau trié v. Les éléments de v doivent être dans l'ordre croissant. La fonction renvoie la position (un nombre compris entre 0 et n-1) si x apparaît dans v, et -1 sinon.

La recherche binaire compare d'abord la valeur d'entrée x à l'élément central du tableau v. Si x est inférieur à la valeur médiane, la recherche se concentre sur la moitié inférieure du tableau, sinon sur la moitié supérieure. Dans les deux cas, l'étape suivante consiste à comparer x à l'élément central de la moitié sélectionnée. Ce processus de division de la plage en deux se poursuit jusqu'à ce que la valeur soit trouvée ou que la plage soit vide :

  1. /* binsearch: Recherche x dans v[0] <= v[1] <= ... <= v[n-1] */
  2.  
  3. int binsearch(int x, int v[], int n) {
  4.  int low, high, mid;
  5.  
  6.  low = 0;
  7.  high = n - 1;
  8.  while (low <= high) {
  9.   mid = (low+high)/2;
  10.   if (x < v[mid])
  11.       high = mid + 1;
  12.   else if (x > v[mid])
  13.       low = mid + 1;
  14.   else /* correspondance trouvé */
  15.       return mid;
  16.  }
  17.  return -1; /* pas de correspondance */
  18. }

La décision fondamentale est de savoir si x est inférieur, supérieur ou égal à l'élément central v[mid] à chaque étape ; c'est une décision naturelle pour else-if.

switch

L'instruction switch est une décision à plusieurs voies testant si une expression correspond à l'une des valeurs entières constantes et se divise en conséquence :

switch (expression) {
   case const-expr: statements
   case const-expr: statements
   default: statements
}

Chaque cas est étiqueté par une ou plusieurs constantes à valeur entière ou expressions constantes. Si un cas correspond à la valeur de l'expression, l'exécution démarre à partir de ce cas. Toutes les expressions de cas doivent être différentes. Le cas étiqueté default est exécuté si aucun des autres cas n'est satisfait. Un default est facultatif ; s'il n'est pas présent et si aucun des cas ne correspond, aucune action n'a lieu. Les cas et la clause default peuvent se produire dans n'importe quel ordre.

Dans la page Les premiers pas, nous avons écrit un programme pour compter les occurrences de chaque chiffre, espace blanc et tous les autres caractères, en utilisant une séquence de if... else if ... else. Voici le même programme avec un switch :

  1. #include <stdio.h>
  2.  
  3. /* compter les chiffres, les espaces blancs, autres */
  4.  
  5. main()  {
  6.  int c, i, nwhite, nother, ndigit[10];
  7.  nwhite = nother = 0;
  8.  for (i = 0; i < 10; i++) ndigit[i] = 0;
  9.  while ((c = getchar()) != EOF) {
  10.   switch (c) {
  11.    case '0': case '1': case '2': case '3': case '4':
  12.    case '5': case '6': case '7': case '8': case '9':
  13.     ndigit[c-'0']++;
  14.     break;
  15.    case ' ':
  16.    case '\n':
  17.    case '\t':
  18.     nwhite++;
  19.     break;
  20.    default:
  21.     nother++;
  22.     break;
  23.   }
  24.  }
  25.  printf("chiffres =");
  26.  for (i = 0; i < 10; i++) printf(" %d", ndigit[i]);
  27.  printf(", espace blanc = %d, autre = %d\n",nwhite, nother);
  28.  return 0;
  29. }

L'instruction break provoque une sortie immédiate du switch. Étant donné que les case servent simplement d'étiquettes, une fois le code d'une case terminé, l'exécution passe à la suivante, à moins que vous n'interveniez explicitement pour vous en échapper. break et return sont les moyens les plus courants de quitter un switch. Une instruction break peut également être utilisée pour forcer une sortie immédiate des boucles while, for et do.

Passer d'une case à l'autre est une bénédiction mitigée. Du côté positif, cela permet d'attacher plusieurs case à une seule action, comme avec les chiffres dans cet exemple. Mais cela implique également que normalement chaque case doit se terminer par une pause pour éviter de passer à la suivante. Passer d'une case à l'autre n'est pas robuste, car il est sujet à la désintégration lorsque le programme est modifié. À l'exception de plusieurs étiquettes pour un seul calcul, les passages à travers doivent être utilisés avec parcimonie et commentés.

Par souci de bonne habitude, mettez un saut de ligne après le dernier cas (par default ici) même si cela n'est logiquement pas nécessaire. Un jour, lorsqu'un autre cas sera ajouté à la fin, ce petit peu de programmation défensive vous sauvera la mise.

Boucles : While et For

Nous avons déjà rencontré les boucles while et for. Dans :

while (expression)
   statement

l'expression est évaluée. Si elle est différente de zéro, l'instruction est exécutée et l'expression est réévaluée. Ce cycle continue jusqu'à ce que l'expression devienne nulle, moment auquel l'exécution reprend après l'instruction.

L'instruction for :

for (expr1; expr2; expr3)
   statement

est équivalent à :

expr1;
while (expr2) {
   statement
   expr3;
}

sauf pour le comportement de continue. Grammaticalement, les trois composantes d'une boucle for sont des expressions. Le plus souvent, expr1 et expr3 sont des affectations ou des appels de fonction et expr2 est une expression relationnelle. Chacune des trois parties peut être omise, bien que les points-virgules doivent rester. Si expr1 ou expr3 est omis, il est simplement supprimé de l'expansion. Si le test, expr2, n'est pas présent, il est considéré comme vrai en permanence, donc :

  1. for (;;) {
  2. ...
  3. }

est une boucle «infinie», pouvant vraisemblablement être interrompue par d'autres moyens, comme une pause ou un retour.

L'utilisation de while ou de for est en grande partie une question de préférence personnelle. Par exemple, dans :

  1. while ((c = getchar()) == ' ' || c == '\n' || c = '\t')
  2.    ; /* ignorer les caractères d'espacement */

il n'y a pas d'initialisation ou de réinitialisation, donc le while est le plus naturel.

Le for est préférable lorsqu'il y a une initialisation et une incrémentation simples car il garde les instructions de contrôle de boucle proches les unes des autres et visibles en haut de la boucle. Cela est particulièrement évident dans :

  1. for (i = 0; i < n; i++)
  2. ...

étant l'idiome C pour traiter les n premiers éléments d'un tableau, l'analogue de la boucle DO de Fortran ou du for de Pascal. L'analogie n'est cependant pas parfaite, car la variable d'index i conserve sa valeur lorsque la boucle se termine pour une raison quelconque. Comme les composantes du for sont des expressions arbitraires, les boucles for ne se limitent pas aux progressions arithmétiques. Néanmoins, il est mal vu de forcer des calculs sans rapport dans l'initialisation et l'incrémentation d'un for, étant mieux réservés aux opérations de contrôle de boucle.

A titre d'exemple plus large, voici une autre version d'atoi pour convertir une chaîne de caractères en son équivalent numérique. Celle-ci est légèrement plus générale que celle de la page Types, opérateurs et expressions ; elle gère les espaces blancs de début facultatifs et un signe + ou - facultatif.

La structure du programme reflète la forme de l'entrée :

Ignorer les espaces blancs, le cas échéant
Obtenir le signe, le cas échéant
Obtenir la partie entière et la convertir

Chaque étape fait sa part et laisse les choses dans un état propre pour la suivante. L'ensemble du processus se termine au premier caractère ne pouvant pas faire partie d'un nombre :

  1. #include <ctype.h>
  2.  
  3. /* atoi: convertir s en entier ; version 2 */
  4.  
  5. int atoi(char s[]) {
  6.  int i, n, sign;
  7.  for (i = 0; isspace(s[i]); i++) /* ignorer les espaces blancs */
  8.    ;
  9.  sign = (s[i] == '-') ? -1 : 1;
  10.  if (s[i] == '+' || s[i] == '-') /* signe de saut */
  11.    i++;
  12.  for (n = 0; isdigit(s[i]); i++)
  13.   n = 10 * n + (s[i] - '0');
  14.  return sign * n;
  15. }

La bibliothèque standard fournit une fonction strtol plus élaborée pour la conversion de chaînes de caractères en entiers longs.

Les avantages de garder le contrôle de boucle centralisé sont encore plus évidents lorsqu'il y a plusieurs boucles imbriquées. La fonction suivante est un tri Shell pour trier un tableau d'entiers. L'idée de base de cet algorithme de tri, inventé en 1959 par D. L. Shell, est que dans les premières étapes, les éléments éloignés sont comparés, plutôt que les éléments adjacents comme dans les tris par échange plus simples. Cela tend à éliminer rapidement de grandes quantités de désordre, de sorte que les étapes ultérieures ont moins de travail à faire. L'intervalle entre les éléments comparés est progressivement réduit à un, à partir duquel le tri devient effectivement une méthode d'échange adjacent.

  1. /* shellsort : trier v[0]...v[n-1] par ordre croissant */
  2. void shellsort(int v[], int n) {
  3.  int gap, i, j, temp;
  4.  for (gap = n/2; gap > 0; gap /= 2)
  5.  for (i = gap; i < n; i++)
  6.   for (j=i-gap; j>=0 && v[j]>v[j+gap]; j-=gap) {
  7.    temp = v[j];
  8.    v[j] = v[j+gap];
  9.    v[j+gap] = temp;
  10.   }
  11. }

Il y a trois boucles imbriquées. La plus externe contrôle l'écart entre les éléments comparés, le réduisant de n/2 par un facteur de deux à chaque passage jusqu'à ce qu'il devienne nul. La boucle du milieu parcourt les éléments. La boucle la plus interne compare chaque paire d'éléments séparés par un écart et inverse ceux étant dans le désordre. Puisque l'écart est finalement réduit à un, tous les éléments sont finalement ordonnés correctement. Notez comment la généralité de for fait que la boucle externe s'adapte à la même forme que les autres, même s'il ne s'agit pas d'une progression arithmétique.

Un dernier opérateur C est la virgule «,», trouvant le plus souvent son utilisation dans l'instruction for. Une paire d'expressions séparées par une virgule est évaluée de gauche à droite, et le type et la valeur du résultat sont le type et la valeur de l'opérande de droite. Ainsi, dans une instruction for, il est possible de placer plusieurs expressions dans les différentes parties, par exemple pour traiter deux indices en parallèle. Ceci est illustré dans la fonction reverse(s), inversant la chaîne de caractères s en place :

  1. #include <string.h>
  2.  
  3. /* reverse : inverser la chaîne de caractères s en place */
  4. void reverse(char s[]) {
  5.  int c, i, j;
  6.  for (i = 0, j = strlen(s)-1; i < j; i++, j--) {
  7.   c = s[i];
  8.   s[i] = s[j];
  9.   s[j] = c;
  10.  }
  11. }

Les virgules séparant les paramètres de fonction, les variables dans les déclarations,..., ne sont pas des opérateurs virgule et ne garantissent pas une évaluation de gauche à droite.

Les opérateurs virgule doivent être utilisés avec parcimonie. Les utilisations les plus appropriées sont pour les constructions fortement liées les unes aux autres, comme dans la boucle for en sens inverse, et dans les macros où un calcul en plusieurs étapes doit être une expression unique. Une expression virgule peut également être appropriée pour l'échange d'éléments en sens inverse, où l'échange peut être considéré comme une opération unique :

  1. for (i = 0, j = strlen(s)-1; i < j; i++, j--)  c = s[i], s[i] = s[j], s[j] = c;    

Boucles : do-while

Comme discuter dans la page Les premiers pas, les boucles while et for testent la condition de terminaison en haut. En revanche, la troisième boucle en C, la do-while, teste en bas après avoir effectué chaque passage dans le corps de la boucle ; le corps est toujours exécuté au moins une fois.

La syntaxe de la boucle do est :

do
   statement
while (expression);

L'instruction est exécutée, puis expression est évaluée. Si elle est vraie, instruction est évaluée à nouveau, et ainsi de suite. Lorsque l'expression devient fausse, la boucle se termine. À l'exception du sens du test, do-while est équivalent à l'instruction REPEAT-UNTIL de Pascal.

L'expérience montre que do-while est beaucoup moins utilisé que while et for. Néanmoins, de temps en temps, il est utile, comme dans la fonction suivante itoa, convertissant un nombre en chaîne de caractères (l'inverse de atoi). Le travail est légèrement plus compliqué qu'on pourrait le penser au premier abord, car les méthodes simples de génération des chiffres les génèrent dans le mauvais ordre. Nous avons choisi de générer la chaîne de caractères à l'envers, puis de l'inverser :

  1. /* itoa: convertir n en caractères dans s */
  2.  
  3. void itoa(int n, char s[]) {
  4.  int i, sign;
  5.  if ((sign = n) < 0) /* signe d'enregistrement */
  6.   n = -n; /* rendre n positif */
  7.  i = 0;
  8.  do { /* générer des chiffres dans l'ordre inverse */
  9.   s[i++] = n % 10 + '0'; /* obtenir le chiffre suivant */
  10.  } while ((n /= 10) > 0); /* supprime-le */
  11.  if (sign < 0) s[i++] = '-';
  12.  s[i] = '\0';
  13.  reverse(s);
  14. }

Le do-while est nécessaire, ou du moins pratique, car au moins un caractère doit être installé dans le tableau s, même si n est nul. Nous avons également utilisé des accolades autour de l'instruction unique constituant le corps du do-while, même si elles sont inutiles, afin que le lecteur pressé ne confonde pas la partie while avec le début d'une boucle while.

break et continue

Il est parfois pratique de pouvoir sortir d'une boucle autrement qu'en testant en haut ou en bas. L'instruction break permet de sortir plus tôt de for, while et do, tout comme de switch. Un break provoque la sortie immédiate de la boucle ou du switch le plus interne.

La fonction suivante, trim, supprime les espaces, les tabulations et les sauts de ligne de fin d'une chaîne de caractères, en utilisant un break pour sortir d'une boucle lorsque le point le plus à droite non vide, non tabulé et non saut de ligne est trouvé :

  1. /* trim: supprimer les espaces de fin, les tabulations et les nouvelles lignes */
  2. int trim(char s[]) {
  3.  int n;
  4.  for (n = strlen(s)-1; n >= 0; n--)
  5.  if (s[n] != ' ' && s[n] != '\t' && s[n] != '\n')
  6.   break;
  7.  s[n+1] = '\0';
  8.  return n;
  9. }

strlen renvoie la longueur de la chaîne de caractères. La boucle for démarre à la fin et analyse en arrière à la recherche du premier caractère n'étant pas un espace, une tabulation ou une nouvelle ligne. La boucle est interrompue lorsqu'un caractère est trouvé ou lorsque n devient négatif (c'est-à-dire lorsque la chaîne de caractères entière a été analysée). Vous devez vérifier que ce comportement est correct même lorsque la chaîne de caractères est vide ou ne contient que des caractères d'espacement.

L'instruction continue est liée à break, mais moins souvent utilisée ; elle provoque le début de l'itération suivante de la boucle for, while ou do englobante. Dans les cas while et do, cela signifie que la partie test est exécutée immédiatement ; dans le cas du cas for, le contrôle passe à l'étape d'incrémentation. L'instruction continue s'applique uniquement aux boucles, pas à switch. Un continue à l'intérieur d'un switch à l'intérieur d'une boucle provoque l'itération suivante de la boucle.

À titre d'exemple, ce fragment traite uniquement les éléments non négatifs du tableau a ; les valeurs négatives sont ignorées :

  1. for (i = 0; i < n; i++)
  2.  if (a[i] < 0) /* ignorer les éléments négatifs */
  3.   continue;
  4. ... /* faire des éléments positifs */

L'instruction continue est souvent utilisée lorsque la partie de la boucle suivant est compliquée, de sorte que l'inversion d'un test et l'indentation d'un autre niveau imbriqueraient le programme trop profondément.

goto et les étiquettes

C fournit l'instruction goto, infiniment abusable, et des étiquettes vers lesquelles se brancher. Formellement, l'instruction goto n'est jamais nécessaire, et en pratique, il est presque toujours facile d'écrire du code sans elle.

Néanmoins, il existe quelques situations où les goto peuvent trouver leur place. La plus courante consiste à abandonner le traitement dans une structure profondément imbriquée, comme sortir de deux ou plusieurs boucles à la fois. L'instruction break ne peut pas être utilisée directement car elle ne sort que de la boucle la plus interne. Ainsi :

  1.     for ( ... )
  2.      for ( ... ) {
  3.       ...
  4.       if (disaster) goto error;
  5.      }
  6.  ...
  7. error:
  8.     /* nettoyer le désordre */    

Cette organisation est pratique si le code de gestion des erreurs n'est pas trivial et si des erreurs peuvent survenir à plusieurs endroits.

Une étiquette a la même forme qu'un nom de variable et est suivie de deux points. Elle peut être attachée à n'importe quelle instruction de la même fonction que le goto. La portée d'une étiquette est la fonction entière. Comme autre exemple, considérons le problème de déterminer si deux tableaux a et b ont un élément en commun. Une possibilité est :

  1. for (i = 0; i < n; i++)
  2.  for (j = 0; j < m; j++)
  3.   if (a[i] == b[j])
  4.    goto found;
  5. /* n'a trouvé aucun élément commun */
  6. ...
  7. found:
  8. /* j'en ai un: a[i] == b[j] */    

Le code impliquant un goto peut toujours être écrit sans un goto, mais peut-être au prix de quelques tests répétés ou d'une variable supplémentaire. Par exemple, la recherche dans un tableau devient :

  1. found = 0;
  2. for (i = 0; i < n && !found; i++)
  3.  for (j = 0; j < m && !found; j++)
  4.   if (a[i] == b[j])
  5.    found = 1;
  6. if (found)
  7.   /* j'en ai un: a[i-1] == b[j-1] */
  8.    ...
  9. else
  10.   /* n'a trouvé aucun élément commun */    


Dernière mise à jour : Mardi, le 19 novembre 2024