Notes sur le langage C


précédentsommairesuivant

XXXVIII. Du bon usage de qsort()

XXXVIII-A. Description de la fonction qsort()

XXXVIII-A-1. Introduction

La fonction qsort() implémente un algorithme de tri non spécifié qui permet de trier tout ou partie de n'importe quel tableau de données, du moment qu'il existe un critère de tri dans les données. Elle s'appuie sur une fonction utilisateur qui se charge d'exprimer le critère de tri.

C'est l'implémentation qui décide quel algorithme est utilisé. En général, l'algorithme est choisi pour sa performance. Il n'est pas rare que ce soit un Quick Sort.

XXXVIII-A-2. Interface

Le prototype de la fonction qsort() est

 
Sélectionnez
void qsort (void *tableau
            , size_t nb_elem
            , size_t taille_elem
            , int (*compare) (void const *a, void const *b));

Les paramètres sont :

  • void *tableau : adresse du premier élément du tableau à trier. La partie à trier doit être modifiable
  • size_t nb_elem : nombre d'éléments du tableau à trier (à ne pas confondre avec sa taille) : Par exemple, tout le tableau : sizeof tab / sizeof *tab
  • size_t taille_elem : taille d'un élément du tableau : sizeof *tab
  • int (*compare) (void const *a, void const *b) : adresse de la fonction de comparaison (pointeur de fonction). Cette fonction est fournie par l'utilisateur
XXXVIII-A-2-a. Interface de la fonction de comparaison

Cette fonction dispose de 2 paramètres :

  • void const *a : adresse d'un des éléments du tableau en cours d'évaluation par l'algorithme. Il est qualifié 'const', car la fonction de comparaison ne doit en aucun cas en modifier le contenu sous peine de comportement indéfini.
  • void const *b : adresse d'un autre élément du tableau en cours d'évaluation par l'algorithme. Il est qualifié 'const', car la fonction de comparaison ne doit en aucun cas en modifier le contenu sous peine de comportement indéfini.

de plus, elle doit retourner un int qui prend la valeur suivante (tri croissant) :

  • 0 si le critère de a est égal au critère de b
  • < 0 si le critère de a est inférieur au critère de b
  • > 0 si le critère de a est supérieur au critère de b

XXXVIII-A-3. Comportement

La fonction qsort() effectue le tri du tableau selon le critère indiqué. La valeur rétournée par la fonction de comparaison permet à l'algorithme de prendre les décisions qui s'imposent.

XXXVIII-A-3-a. Comportement de la fonction de comparaison

La fonction de comparaison reçoit l'adresse des 2 éléments en cours d'évaluation. Par l'intérmédaire de pointeurs locaux typés et initialisés avec ces paramètres, elle doit evaluer le critère de tri et retourner un int de la valeur résultant de l'évaluation. Pour un entier, une simple différence suffit. Pour une chaine, str[n]cmp() a été conçue pour ça. Pour un réel, c'est plus délicat, car la notion d'égalité est soumise à l'appréciation d'un EPSILON qui dépend de la précision recherchée.

XXXVIII-B. Usage de la fonction qsort()

Afin de dédramatiser l'usage de qsort(), qui fait parfois un peu peur, je fournis ici quelques exemples d'utilisation.

Attention, je suppose que les bases du C sont connues (tableaux, pointeurs, fonctions, I/O)

Ces exemples ne sont pas des suites de validation d'algorithmes de tri ! Ce ne sont que de simples exemples.

XXXVIII-B-1. Tri d'un tableau d'entiers

Soit un tableau de 5 entiers à trier : 4 , 6, -3, 4, 7,

 
Sélectionnez
#include <stdio.h>
#include <stdlib.h>
 
/* affichage du tableau */
static void aff (int *a, size_t n)
{
   size_t i;
   for (i = 0; i < n; i++)
   {
      printf ("=", a[i]);
   }
   printf ("\n");
}
 
/* fonction utilisateur de comparaison fournie a qsort() */
static int compare (void const *a, void const *b)
{
   /* definir des pointeurs type's et initialise's
      avec les parametres */
   int const *pa = a;
   int const *pb = b;
 
   /* evaluer et retourner l'etat de l'evaluation (tri croissant) */
   return *pa - *pb;
}
 
int main (void)
{
/* tableau a trier */
   int tab[] = { 4, 6, -3, 4, 7 };
 
/* affichage du tableau dans l'etat */
   aff (tab, sizeof tab / sizeof *tab);
 
   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, compare);
 
/* affichage du tableau apres le tri */
   aff (tab, sizeof tab / sizeof *tab);
 
   return 0;
}

Ce qui donne :

 
Sélectionnez
  4  6 -3  4  7
 -3  4  4  6  7
 
Press ENTER to continue.

XXXVIII-B-2. Tri d'un tableau de chaines

Il s'agit maintenant de trier un tableau de chaines constantes. Ici, il est particulièrement important de bien comprendre le sens de 'const'. Le tableau est modifiable, mais il est composé de pointeurs sur des chaines non modifiables. Ce que va faire qsort(), c'est de réorganiser le tableau de pointeurs de façon à ce lorsqu'on lira le tableau en séquence, les chaines pointées apparaissent comme 'triées'. Ce concept est assez subtil, car le tri est indirect. En effet, si on regardait la valeur des pointeurs dans le tableau, ils ne seraient probablement pas triés. De même, les chaines en mémoire non modifiable sont, par définition, réstées à leur place.

Dans la fonction de comparaison, comme d'habitude, on récupère les adresse des éléments en cours d'évaluation. Ici, les éléments sont de type char const *. Leur adresse est donc de type char const * *. Mais comme on a pas le droit de modifier les données pointées, on doit ajouter un 'const' pour être conforme aux paramètres : char const * const *

 
Sélectionnez
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
/* affichage du tableau */
static void aff (char const **a, size_t n)
{
   size_t i;
   for (i = 0; i < n; i++)
   {
      printf ("%s\n", a[i]);
   }
   printf ("\n");
}
 
/* fonction utilisateur de comparaison fournie a qsort() */
static int compare (void const *a, void const *b)
{
   /* definir des pointeurs type's et initialise's
      avec les parametres */
   char const *const *pa = a;
   char const *const *pb = b;
 
   /* evaluer et retourner l'etat de l'evaluation (tri croissant) */
   return strcmp (*pa, *pb);
}
 
int main (void)
{
/* tableau a trier (tableau de pointeurs sur char const) */
   char const *tab[] = { "world", "hello", "wild" };
 
/* affichage du tableau dans l'etat */
   aff (tab, sizeof tab / sizeof *tab);
 
   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, compare);
 
/* affichage du tableau apres le tri */
   aff (tab, sizeof tab / sizeof *tab);
 
   return 0;
}

malgré tout, le tri a eu lieu :

 
Sélectionnez
world
hello
wild
 
hello
wild
world
 
 
Press ENTER to continue.

XXXVIII-B-3. Tri d'un tableau de structures

Soit une structure comprenant {nom, prenom, age}. On crée un tableau de 5 éléments de ce type que l'on remplit "à la main", puis, on fait un tri par prenom, par age décroissant, puis par age croissant.

 
Sélectionnez
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
 
struct fiche
{
   char nom[11];
   char prenom[11];
   int age;
};
 
/* affichage du tableau */
static void aff (struct fiche const *a, size_t n)
{
   size_t i;
   for (i = 0; i < n; i++)
   {
      /* pointeur intermediaire pour alleger l'ecriture */
      struct fiche const *p = a + i;
      printf ("%-10s %-10s %d ans\n", p->nom, p->prenom, p->age);
   }
   printf ("\n");
}
 
/* fonction utilisateur de comparaison fournie a qsort() */
static int compare_prenom (void const *a, void const *b)
{
   /* definir des pointeurs type's et initialise's
      avec les parametres */
   struct fiche const *pa = a;
   struct fiche const *pb = b;
 
   /* evaluer et retourner l'etat de l'evaluation (tri croissant) */
   return strcmp (pa->prenom, pb->prenom);
}
 
/* fonction utilisateur de comparaison fournie a qsort() */
static int compare_age (void const *a, void const *b)
{
   struct fiche const *pa = a;
   struct fiche const *pb = b;
 
   return pa->age - pb->age;
}
 
/* fonction utilisateur de comparaison fournie a qsort() */
static int compare_age_dec (void const *a, void const *b)
{
   struct fiche const *pa = a;
   struct fiche const *pb = b;
 
   return pb->age - pa->age;
}
 
int main (void)
{
/* tableau a trier (tableau de pointeurs sur char const) */
   struct fiche tab[] = {
      {"Simpson", "Homer", 36},
      {"Bouvier", "Marge", 34},
      {"Simpson", "Bart", 10},
      {"Simpson", "Lisa", 8},
      {"Simpson", "Maggie", 2},
   };
 
/* affichage du tableau dans l'etat */
   aff (tab, sizeof tab / sizeof *tab);
 
   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, compare_prenom);
 
/* affichage du tableau apres le tri */
   aff (tab, sizeof tab / sizeof *tab);
 
   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, compare_age);
 
/* affichage du tableau apres le tri */
   aff (tab, sizeof tab / sizeof *tab);
 
   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, compare_age_dec);
 
/* affichage du tableau apres le tri */
   aff (tab, sizeof tab / sizeof *tab);
 
   return 0;
}
 
Sélectionnez
Simpson    Homer      36 ans
Bouvier    Marge      34 ans
Simpson    Bart       10 ans
Simpson    Lisa       8 ans
Simpson    Maggie     2 ans
 
Simpson    Bart       10 ans
Simpson    Homer      36 ans
Simpson    Lisa       8 ans
Simpson    Maggie     2 ans
Bouvier    Marge      34 ans
 
Simpson    Maggie     2 ans
Simpson    Lisa       8 ans
Simpson    Bart       10 ans
Bouvier    Marge      34 ans
Simpson    Homer      36 ans
 
Simpson    Homer      36 ans
Bouvier    Marge      34 ans
Simpson    Bart       10 ans
Simpson    Lisa       8 ans
Simpson    Maggie     2 ans
 
 
Press ENTER to continue.

XXXVIII-B-4. Tri de nombres réels

Pour trier les nombres réels (float, double), il y a quelques précautions à prendre, car la notion d'égalité n'existe pas. Il faut travailler par plages :

 
Sélectionnez
#include <stdio.h>
#include <time.h>
 
double random_range (double min, double max)
{
   return min + ((max - min) * (rand () / (double) RAND_MAX));
}
 
static int cmp (void const *a, void const *b)
{
   int ret = 0;
   double const *pa = a;
   double const *pb = b;
   double diff = *pa - *pb;
   if (diff > 0)
   {
      ret = 1;
   }
   else if (diff < 0)
   {
      ret = -1;
   }
   else
   {
      ret = 0;
   }
 
   return ret;
}
 
int main (void)
{
   double tab[100];
   int i;
   srand ((int) time (NULL));
   for (i = 0; i < sizeof tab / sizeof *tab; i++)
   {
      tab[i] = random_range (0, 1);
   }
 
   qsort (tab, sizeof tab / sizeof *tab, sizeof *tab, cmp);
 
   for (i = 0; i < sizeof tab / sizeof *tab; i++)
   {
      printf ("%8.4f", tab[i]);
   }
 
   return 0;
}

XXXIX. Les identificateurs réservés

Le document de définition du langage C a réservé un certain nombre d'identificateurs réservés. Ils peuvent être utilisés par les implémenteurs (ceux qui écrivent les compilateurs) ou pour des extensions du langage.

Ces identificateurs peuvent apparaître dans les interfaces publiques ou pour réaliser certaines parties des fichiers d'entête (macros, paramètres...). Ils sont choisis de façon à ne pas interférer avec les identificateurs des utilisateurs, sous réserve, bien sûr, que ceux-ci ne les utilisent pas, d'où l'intérêt de cet article.

Les identificateurs réservés sont

 
Sélectionnez
 +---------------------+----------------+---------------------+----------------+
 | Les identificateurs |                | Exemple             | Exemple        |
 | commençant par      | suivi de       | valide              | Reservé        |
 +---------------------+----------------+---------------------+----------------+
 | "_"                 | "_A-Z"         | _123                | _ABC           |
 +---------------------+----------------+---------------------+----------------+
 | "is"                | "a-z"          | is_abc              | isabc          |
 +---------------------+----------------+---------------------+----------------+
 | "mem"               | "a-z"          |                     |                |
 +---------------------+----------------+---------------------+----------------+
 | "str"               | "a-z"          |                     |                |
 +---------------------+----------------+---------------------+----------------+
 | "to"                | "a-z           |                     |                |
 +---------------------+----------------+---------------------+----------------+
 | "wcs"               | "a-z"          |                     |                |
 +---------------------+----------------+---------------------+----------------+
 | "E"                 | "A-Z" ou "0-9" | Eabc                | E123 EABC      |
 +---------------------+----------------+---------------------+----------------+
 | "LC_"               | "A-Z"          | LC_abc LC_123 LCABC | LC_ABC         |
 +---------------------+----------------+---------------------+----------------+
 | "SIG"               | "_A-Z"         | SIGabc              | SIGABC SIG_ABC |
 +---------------------+----------------+---------------------+----------------+

XL. Bien gérer la portée des objets et des fonctions

Le langage C offre par nature un contrôle assez fin de la portée des objets et des fonctions. Cette caractéristique est souvent mal connue, pourtant elle apporte un bénéfice certain, notamment sur le plan de l'organisation du code (conception détaillée).

XL-A. Fonctions

Par défaut, la portée d'une fonction est globale.

 
Sélectionnez
int function (int a, char *b)
{
}

Elle est visible d'un autre module une simple déclaration:

 
Sélectionnez
int function ();

ou mieux, un prototype:

 
Sélectionnez
int function (int a, char *b);

Il est possible cependant de réduire la portée de la fonction à l'unité de compilation dans laquelle elle a été définie, en ajoutant le qualificateur static.

 
Sélectionnez
static int function (int a, char *b)
{
}

Cette pratique, lorsqu'elle est possible, apporte différents avantages :

  • Une économie d'identificateurs. La portée de celui-ci étant limitée à une unité de compilation, il est possible de le réutiliser pour une autre fonction qualifiée 'static' dans une autre unité de compilation.
  • Une meilleure optimisation. Certains compilateurs sont capables d'"inliner" une telle fonction dans certaines conditions, ce qui diminue le temps d'exécution au prix d'une augmentation de la taille (le code de la fonction est recopié autant de fois que nécessaire).
  • Une meilleure organisation du code. Etant donné que ces fonctions sont forcément appelées par une fonction de l'unité de compilation dans laquelle elles ont été définies, le code se trouve naturellement organisé en blocs fonctionnels cohérents. De plus, comme à priori, ces fonctions n'ont pas besoin de prototypes séparés, cela favorise une organisation du fichier source selon le principe 'Définir avant d'utiliser' (Top-down)

On évitera cependant de multiplier les codes identiques, et les principes de factorisation du code restent en vigueur.

 
Sélectionnez
+--------------------+    +--------------------+
| Bloc fonctionnel A |    | Bloc Fonctionnel B |
+--------------------+    +--------------------+
               |               |
               v               v
             +--------------------+
             |       Outils       |
             +--------------------+

XL-B. Objets

La portée d'un objet est régie selon plusieurs critères.

XL-B-1. Définition hors d'un bloc

La portée par défaut est globale. Elle peut être réduite à l'unité de compilation en ajoutant le qualificateur static.

XL-B-2. Définition dans un bloc

Si deux objets ont le même nom, l'objet de portée inférieure masque les objets de portée supérieure. Pour cette raison, qui entraine un comportement confus, on évite de donner le même nom à des objets dont les portées sont imbriquées.

La portée est celle du bloc et des blocs inclus.

XL-B-3. Masquage (Shadowing)

 
Sélectionnez
/* objet de portee globale */
int x;
 
int f (void)
{
   /* la globale x est masque'e par une locale du meme nom */
   int x = 0;
 
   /* la globale x n'est pas modifie'e. */
   x++;
}
 
int main (void)
{
   /* la globale x est modifie'e */
   x = 2;
 
   f();
 
   /* la globale x vaut toujours 2 */
 
   return 0;
}

XLI. Du bon usage de assert()

assert() est une macro qui permet de 'poser un piège'. On s'en sert en phase de mise au point pour vérifier si la conception et la réalisation sont correctes.

Le paramètre est une expression. Si elle retourne 0 (expression fausse), le programme s'arrête et un message indiquant le lieu et la cause est affiché.

Il est d'usage qu'en mode production (release), la macro globale NDEBUG soit définie, ce qui fait que les macros assert(), bien que toujours présentes dans le source, ne génèrent plus aucun code de vérification. En conséquence, cette macro ne doit évidemment pas être utilisée pour détecter des erreurs d'utilisation ou de système.

XLI-A. Exemple d'utilisation

 
Sélectionnez
#include <stdio.h>
 
static void afficher (int const t[], size_t n)
{
   size_t i;
   for (i = 0; i <= n; i++)
   {
      printf ("M", t[i]);
   }
   printf ("\n");
}
 
 
int main (void)
{
   int tab[] = {1, 2, 3, 4};
 
   afficher (tab, sizeof tab / sizeof *tab);
   return 0;
}

Ce code parait correct, mais à l'exécution, on constate :

 
Sélectionnez
1   2   3   4   2
 
Press ENTER to continue.

Pour vérifier le comportement, je pose un piège qui vérifie la validité de l'index.

  • il doit être >=0 (toujours vrai, vu le type size_t)
  • il doit être < n

Je vais donc ajouter un piège :

 
Sélectionnez
assert (i < n);

Avant l'accès en lecture au tableau.

Pour être valide, la conception du piège doit se faire sans lire le code à tester, mais en se basant uniquement sur l'interface et le comportement présumé.

 
Sélectionnez
#include <stdio.h>
#include <assert.h>
 
static void afficher (int const t[], size_t n)
{
   size_t i;
   for (i = 0; i <= n; i++)
   {
      /* ajout du piege */
      assert (i < n);
      printf ("M", t[i]);
   }
   printf ("\n");
}
 
int main (void)
{
   int tab[] = {1, 2, 3, 4};
 
   afficher (tab, sizeof tab / sizeof *tab);
   return 0;
}

Ce qui provoque bien sûr :

 
Sélectionnez
1   2   3   4Assertion failed: i < n, file main.c, line 10
 
This application has requested the Runtime to terminate it in an unusual way.
Please contact the application's support team for more information.
 
Press ENTER to continue.

Ce qui signifie que i a dépassé la valeur maximale qu'autorise le langage C. La cause est évidemment le <= au lieu de < dans l'expression du for(), ce qui entraine une action corrective immédiate :

 
Sélectionnez
#include <stdio.h>
#include <assert.h>
 
static void afficher (int const t[], size_t n)
{
   size_t i;
   /* correction */
   for (i = 0; i < n; i++)
   {
      /* ajout du piege */
      assert (i < n);
      printf ("M", t[i]);
   }
   printf ("\n");
}
 
int main (void)
{
   int tab[] = {1, 2, 3, 4};
 
   afficher (tab, sizeof tab / sizeof *tab);
   return 0;
}

L'exécution et, à présent, conforme aux attentes :

 
Sélectionnez
1   2   3   4
 
Press ENTER to continue.

XLII. Comportement indéfini

Le langage C est défini par un document unique et reconnu sur le plan international (ISO) par tous les intervenants, que ce soit les développeurs de compilateurs (les 'implémenteurs') les développeurs d'applications (les 'utilisateurs') ou les différents formateurs.

Ce document de référence définit un certain nombre d'éléments (obligations, interdictions).

Les autres éléments sont soit laissés à l'appréciation des implémenteurs (implementation defined ou défini par l'implémentation) qui doivent accompagner leur production (compilateur etc.) d'un document précisant les comportement de tel ou tels éléments, soit non définis du tout. Dans ce dernier cas, le comportement est dit indéfini ou indéterminé. (Undefined Behaviour ou UB)

Quelques exemples :

 
Sélectionnez
#include <stdio.h>
 
int main (void)
{
   int i = 0;
 
   printf ("i = %d\n", i);
   return 0;
}

Ce code est conforme à la spécification du langage, aucune zone n'a été laissée dans l'ombre. Le comportement est déterminé. Il est garanti d'écrire

 
Sélectionnez
i = 0

Par contre, voici 2 cas de comportement indéterminé :

  • Absence de prototype pour printf()
 
Sélectionnez
int main (void)
{
   int i = 0;
 
   printf ("i = %d\n", i);
   return 0;
}
  • Lecture d'une valeur non initialisée
 
Sélectionnez
#include <stdio.h>
 
int main (void)
{
   int i;
 
   printf ("i = %d\n", i);
   return 0;
}

Les conséquences d'un UB ne sont pas prévisibles. En effet, ça va du crash au comportement d'apparence conforme. Il est donc impossible de compter sur la simple vérification du comportement pour garantir qu'un code est correct. Il faut avant tout qu'il soit exempt de tout UB.

Le compilateur et ses warnings (ou un outil d'analyse spécialisé comme Lint) peut nous aider à débusquer certains UB. Ici, il est probable qu'une 'utilisation de variable non initialisée' ou qu'un 'appel de fonction sans prototypes' soient detectés (mais ça dépend du compilateur et de ses reglages). Mais il est des cas où le compilateur ne voit rien. Le seul recours est alors l'oeil exercé du programmeur expérimenté.

La chasse aux UB est donc ouverte en permanence. C'est la principale source de bugs dans un programme C. Il convient donc, d'une part, de bien connaitre le langage et ses limites de définition et, d'autre part, d'être extrêmement vigilant lors de l'écriture et de la relecture du code. Lorsqu'elle est possible, la relecture croisée est une bonne méthode de détection des UB.

Exercice : trouver le UB :

 
Sélectionnez
#include <stdio.h>
 
int main (void)
{
   int num = 12;
   char num_text[] = "";
 
   sprintf (num_text, "%d", num);
   printf ("Voici num_text : %s\n", num_text);
   return 0;
}

XLIII. Les item-lists

XLIII-A. Introduction

Qui n'a jamais été confronté à ce problème :

Comment faire le lien entre des constantes symboliques et leur représentation textuelle ?

Le but des item-lists est de résoudre de façon la plus automatique possible ce genre de problème.

XLIII-B. Mise en oeuvre

Le principe est de séparer les informations de bases constantes (par exemple, la correspondance entre caractère et chaine morse) dans un fichier indépendant (ni.c, ni.h, on y reviendra), mais que l'on peut inclure (#include) de façon à réaliser une génération automatique de code en fonction de la demande.

Je prends un exemple plus parlant.

Je veux créer une série de constantes qui représentent des fruits

 
Sélectionnez
enum fruits
{ BANANE, ORANGE, POMME, FRAISE, KIWI };

Ca peut servir à définir une masse de fruit :

 
Sélectionnez
struct dosage_fruit
{
   enum fruits fruit;
   int masse;
};

puis des recettes de fruits mixés :

 
Sélectionnez
struct dosage_fruit mix_energie[] =
{
   {BANANE, 100},
   {ORANGE, 150},
   {FRAISE, 80},
};
 
struct dosage_fruit mix_forme[] =
{
   {BANANE, 50},
   {ORANGE, 50},
   {POMME, 100},
   {KIWI, 50},
   {FRAISE, 50},
};
 
// etc.

Si on veut afficher la recette, il faut un moyen simple pour convertir la constante fruit en une chaine imprimable.

On va donc utiliser un tableau de chaines construit sur le même modèle que le enum (même ordre, c'est primordial, car l'enum sert d'indice au tableau):

 
Sélectionnez
static char const *chaines_fruits[] =
{
   "banane",
   "orange",
   "pomme",
   "fraise",
   "kiwi",
};

ce qui permet maintenant d'afficher la composition :

 
Sélectionnez
void afficher_composition (char const *nom, struct dosage_fruit const *a,
						   size_t n)
{
   size_t i;
   printf ("%s\n", nom);
   for (i = 0; i &lt; n; i++)
   {
      struct dosage_fruit const *p = a + i;
      printf ("%d g de %s\n", p->masse, chaines_fruits[p->fruit]);
   }
   printf ("\n");
}

que l'on appelle comme ceci :

 
Sélectionnez
#define N(a) (sizeof(a) / sizeof *(a))
 
{
   afficher_composition ("Mix energie", mix_energie, N(mix_energie));
   afficher_composition ("Mix forme", mix_forme, N(mix_forme));
 
   etc.

Ce qui donne

 
Sélectionnez
Mix energie
100 g de banane
150 g de orange
80 g de fraise
 
Mix forme
50 g de banane
50 g de orange
100 g de pomme
50 g de kiwi
50 g de fraise
 
 
Press ENTER to continue.

(je laisse au programmeur malin le soin d'écrire "d'orange" au lieu de "de orange"...)

Maintenant, patatras, nouvelle recette à la carte :

 
Sélectionnez
struct dosage_fruit mix_tropical[] =
{
   {BANANE, 50},
   {ORANGE, 80},
   {MANGUE, 100},
   {ANANAS, 50},
};

Quelles sont les conséquences sur le programme :

2 modifications :

  • l'enum :
     
    Sélectionnez
    enum fruits
    { BANANE, ORANGE, POMME, FRAISE, KIWI, MANGUE, ANANAS };
    
  • la liste des chaines 'associées' (manuellement, pour le moment)
     
    Sélectionnez
    static char const *chaines_fruits[] =
    {
       "banane",
       "orange",
       "pomme",
       "fraise",
       "kiwi",
       "mangue",
       "ananas",
    };
    

Si j'inverse ou que j'en oubli une, c'est la catastrophe. Idem, et c'est beaucoup plus sournois, si j'oublie une ','.

Donc, après quelques sueurs froides, ça marche, et on obtient bien :

 
Sélectionnez
Mix energie
100 g de banane
150 g de orange
80 g de fraise
 
Mix forme
50 g de banane
50 g de orange
100 g de pomme
50 g de kiwi
50 g de fraise
 
Mix tropical
50 g de banane
80 g de orange
100 g de mangue
50 g de ananas
 
 
Press ENTER to continue.

Mais c'est déjà beaucoup de stress dans un petit programme comme ça. Dans l'industrie, la moyenne, c'est 1 000 000 de lignes... Pas question de se stresser comme ça si, pour ajouter une valeur dans la liste, il faut modifier dans 10 fichiers différents... (Lesquels ? On n'est pas des robots...)

Par contre, on peut utiliser des techniques de programmations qui font que la maintenance est centralisée en un seul fichier. On le modifie, on recompile tout le projet, et les modifications sont automatiquement reportées dans tout le code.

Pour ça, on va faire travailler la machine à partir d'un fichier unique, pour qu'elle produise ce qu'on veut. C'est toute la puissance qu'offre le préprocesseur bien maitrisé.

Je vais montrer les différentes étapes pour expliquer le principe, mais dans la pratique, on ne fait que la dernière évidemment.

Dans notre exemple, Les deux éléments à "synchroniser" sont :

 
Sélectionnez
enum fruits
{ BANANE, ORANGE, POMME, FRAISE, KIWI, MANGUE, ANANAS };

et la liste des chaines 'associées' (manuellement, pour le moment)

 
Sélectionnez
static char const *chaines_fruits[] =
{
   "banane",
   "orange",
   "pomme",
   "fraise",
   "kiwi",
   "mangue",
   "ananas",
};

Si on observe bien ces deux éléments, on constate qu'ils se ressemblent. Pour être plus parlant, on va les réorganiser en colonnes :

 
Sélectionnez
enum fruits
{
   BANANE,
   ORANGE,
   POMME,
   FRAISE,
   KIWI,
   MANGUE,
   ANANAS
};

et la liste des chaines 'associées' (manuellement, pour le moment)

 
Sélectionnez
static char const *chaines_fruits[] =
{
   "banane",
   "orange",
   "pomme",
   "fraise",
   "kiwi",
   "mangue",
   "ananas",
};

On voit que le schéma est similaire :

  • une entête
  • une liste (chaque élément est terminé par une ,)
  • une fin particulière.

On voit que la liste des enum présente une petite irrégularité : le dernier élément n'est pas terminé par une ','. C'est normal (et obligatoire) en C90 (en C99, la virgule est acceptée).

Petite astuce : on ajoute un élément à la liste, qui sert à terminer la liste sans virgule. Il ne fait pas partie de la liste. C'est soit un 'dummy' (inutile), soit, comme ici où les valeurs sont automatiques, une constante qui exprime le nombre d'éléments de la liste, ce qui, à priori, n'est pas complètement inutile....

Modification :

 
Sélectionnez
enum fruits
{
   BANANE,
   ORANGE,
   POMME,
   FRAISE,
   KIWI,
   MANGUE,
   ANANAS,
   NB_FRUITS
};

Afin de faciliter la maintenance, il faudrait disposer d'une liste 'double' comme ceci :

 
Sélectionnez
BANANE "banane"
ORANGE "orange"
POMME  "pomme"
FRAISE "fraise"
KIWI   "kiwi"
MANGUE "mangue"
ANANAS "ananas"

Comment faire comprendre ça à un programme C ?

C'est la qu'intervient la puissance du préprocesseur.

Il suffit d'écrire une liste de macros avec deux paramètres. Chaque macro représentant un groupe cohérent d'informations ou 'item' :

 
Sélectionnez
ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")

Il suffit ensuite d'écrire la définition de ITEM qui correspond à l'usage qu'on en fait :

 
Sélectionnez
enum fruits
{
#define ITEM(id, chaine)\
   id,
 
ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")
 
#undef ITEM
 
   NB_FRUITS
};

ce qui va produire automatiquement la bonne liste.

On fait pareil avec l'autre liste :

 
Sélectionnez
static char const *chaines_fruits[] =
{
#define ITEM(id, chaine)\
   chaine,
 
ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")
 
#undef ITEM
};

Le #undef permet le 'recyclage' du nom de la macro qui est invariablement ITEM.

L'étape ultime consiste à inclure la liste à partir d'un fichier exterieur auquel je donne l'extension .itm (par exemple : fruits.itm semble approprié).

Je conseille de placer un commentaire dans ce fichier qui rappelle son nom et le début de la définition de la macro avec la signification des champs.

 
Sélectionnez
/* fruits.itm
 
#define ITEM(id, chaine)\
 
*/
ITEM (BANANE, "banane")
ITEM (ORANGE, "orange")
ITEM (POMME , "pomme" )
ITEM (FRAISE, "fraise")
ITEM (KIWI  , "kiwi"  )
ITEM (MANGUE, "mangue")
ITEM (ANANAS, "ananas")

La maintenance de ce fichier est extrêmement simple. Elle est "visuelle".

Les deux définitions deviennent alors :

 
Sélectionnez
enum fruits
{
#define ITEM(id, chaine)\
   id,
#include "fruits.itm"
#undef ITEM
 
   NB_FRUITS
};

et

 
Sélectionnez
static char const *chaines_fruits[] =
{
#define ITEM(id, chaine)\
   chaine,
#include "fruits.itm"
#undef ITEM
};

Ce qui allège considérablement le code source et rend la maintenance automatique. (Le C, c'est Bien)

Il peut y avoir des centaines de lignes dans un .itm. Idem pour le nombre de champs. Ici, il y en a 2 champs, mais on aurait pu en avoir qu'un seul. En effet, une macro sait transformer un paramètre symbolique (ORANGE) en une chaine ("ORANGE") avec #. Mais elle ne sait pas modifier la casse des caractères, d'où mon choix de mettre 2 champs.

Exemples de fichiers itm que j'utilise

(Simple) : gestion des erreurs

(Complexe) : tables de caractères

Je laisse au lecteur le soin d'écrire l'ensemble de l'exemple et de faire les tests nécessaires. Tous les éléments sont là.


précédentsommairesuivant

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.