IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Notes sur le langage C

Date de publication : 29 avril 2009


XXXVIII. Du bon usage de qsort()
XXXIX. Les identificateurs réservés
XL. Bien gérer la portée des objets et des fonctions
XL-A. Fonctions
XL-B. Objets
XL-B-1. Définition hors d'un bloc
XL-B-2. Définition dans un bloc
XL-B-3. Masquage (Shadowing)
XLI. Du bon usage de assert()
XLI-A. Exemple d'utilisation
XLII. Comportement indéfini
XLIII. Les item-lists
XLIII-A. Introduction
XLIII-B. Mise en oeuvre


XXXVIII. Du bon usage de qsort()


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

+---------------------+----------------+---------------------+----------------+
 | 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.

int function (int a, char *b)
{
}
Elle est visible d'un autre module une simple déclaration:

int function ();
ou mieux, un prototype:

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.

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.

+--------------------+    +--------------------+
| 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)


/* 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


#include <stdio.h>

static void afficher (int const t[], size_t n)
{
   size_t i;
   for (i = 0; i <= n; i++)
   {
      printf ("%4d", 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 :

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 :

assert (i < n);
Avant l'accès en lecture au tableau.

info 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é.

#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 ("%4d", 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 :

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 :

#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 ("%4d", 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 :

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 :

#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

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

  • Absence de prototype pour printf()

int main (void)
{
   int i = 0;

   printf ("i = %d\n", i);
   return 0;
}
  • Lecture d'une valeur non initialisée

#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 :

#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

enum fruits
{ BANANE, ORANGE, POMME, FRAISE, KIWI };
Ca peut servir à définir une masse de fruit :

struct dosage_fruit
{
   enum fruits fruit;
   int masse;
};
puis des recettes de fruits mixés :

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):

static char const *chaines_fruits[] =
{
   "banane",
   "orange",
   "pomme",
   "fraise",
   "kiwi",
};
ce qui permet maintenant d'afficher la composition :

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 :

#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

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 :

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 :
    
    enum fruits
    { BANANE, ORANGE, POMME, FRAISE, KIWI, MANGUE, ANANAS };
    
  • la liste des chaines 'associées' (manuellement, pour le moment)
    
    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 :

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 :

enum fruits
{ BANANE, ORANGE, POMME, FRAISE, KIWI, MANGUE, ANANAS };
et la liste des chaines 'associées' (manuellement, pour le moment)

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 :

enum fruits
{
   BANANE,
   ORANGE,
   POMME,
   FRAISE,
   KIWI,
   MANGUE,
   ANANAS
};
et la liste des chaines 'associées' (manuellement, pour le moment)

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 :

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 :

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' :

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 :


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 :

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.

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.

/* 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 :

enum fruits
{
#define ITEM(id, chaine)\
   id,
#include "fruits.itm"
#undef ITEM

   NB_FRUITS
};
et

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à.

 

Valid XHTML 1.1!Valid CSS!

Copyright © 2009 Emmanuel Delahaye. Aucune reproduction, même partielle, ne peut être faite de ce site ni 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.