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:
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)
int x;
int f (void )
{
int x = 0 ;
x+ + ;
}
int main (void )
{
x = 2 ;
f ();
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 :
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é.
|
# include <stdio.h>
# include <assert.h>
static void afficher (int const t[], size_t n)
{
size_t i;
for (i = 0 ; i <= n; i+ + )
{
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;
for (i = 0 ; i < n; i+ + )
{
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.
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
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 } ,
} ;
|
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.
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
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à.
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.