[FAQ] fr.comp.lang.c - partie 4/4

Le
Guillaume Rumeau
Archive-Name: fr/comp/lang/faq-c-4

Archive-Name: fr/comp/lang/faq-c-4


FAQ de fr.comp.lang.c

18 avril 2003
Partie 4/4 (sections 13 à 17)



13. Le pré-processeurs


13.1 Quel est le rôle du préprocesseur ?


Le préprocesseur interprète les directives qui commencent par
#. Principalement, ces directives permettent d'inclure
d'autres fichiers (via #include) et de définir des
macros (via #define) qui sont remplacées lors de la
compilation.


Chaque directive de compilation commence par un # situé
en début de ligne (mais éventuellement précédé par des espaces,
des tabulations ou des commentaires) et se termine en fin de
ligne.


Le préprocesseur est également responsable de la reconnaissance
des trigraphes, des backslashs terminaux, et de
l'ablation des commentaires.


13.2 Qu'est-ce qu'un trigraphe ?


Dans les temps anciens, les ordinateurs n'utilisaient pas ASCII ;
chaque machine avait son propre jeu de caractères. L'ISO a défini
un jeu de caractères supposés présents sur toutes les machines,
c'est l'Invariant Code Set ISO 646-1983. Ce jeu de
caractères ne comporte pas certains caractères intéressants, tels
que les accolades et le backslash. Aussi, le standard
C89 a introduit les trigraphes, séquences de trois caractères
commençant par ??.
Il existe neuf séquences remplacées par le préprocesseur.
Ce remplacement a lieu avant toute autre opération, et agit
également dans les commentaires, les chaînes constantes, etc.


Les trigraphes sont, de fait, rarement utilisés. On les voit
apparaître occasionnellement et par erreur, quand on écrit ça :


printf("Kikoo ??!");


Le trigraphe est remplacé par un pipe, donc ce code
affiche ceci :


Kikoo |


De toutes façons, le redoublement du point d'interrogation est de
mauvais goût.


Les compilateurs modernes soient ne reconnaissent plus les
trigraphes, soit émettent des avertissements quand ils les
rencontrent.


13.3 À quoi sert un backslash en fin de ligne ?


Après le remplacement des trigraphes, le préprocesseur recherche
tous les caractères backslash situés en fin de ligne
; chaque occurrence de ce caractère est supprimée, de même que le
retour à la ligne qui le suit. Ceci permet d'unifier plusieurs
lignes en une seule.


Ce comportement est pratique pour écrire des macros ou des
chaînes de caractères sur plusieurs lignes :


printf("Hello
World !");


Pour les chaînes de caractères, on peut aussi écrire plusieurs
chaînes côte à côte, et le compilateur les unifiera (mais pas le
préprocesseur : pour lui, ce seront deux chaînes à la suite l'une
de l'autre). C'est une technique qui doit être généralement préférée.
On écrira donc pour l'exemple précédent :


printf("Hello"
"World !");


13.4 Quelles sont les formes possibles de commentaires ?


Un commentaire en C commence par /* et se termine par
*/, éventuellement plusieurs lignes plus loin. Les
commentaires ne s'imbriquent pas.


On peut aussi utiliser la compilation conditionnelle, comme ceci :


#if 0
/* ceci est ignore */
#endif /* 0 */


Dans ce cas, il faut que ce qui est ignoré soit une suite de
tokens valide (le préprocesseur va quand même les
regarder, afin de trouver le #endif). Ceci veut dire
qu'il ne faut pas de chaîne non terminées. Ce genre de
commentaire n'est pas adapté à du texte, à cause des apostrophes.


La nouvelle norme du C (C99) permet d'utiliser les commentaires
du C++ : ils commencent par // et se terminent en fin de
ligne. Ce type de commentaire n'est pas encore supporté partout,
donc mieux vaut ne pas s'en servir si on veut faire du code vraiment
portable, même si avant C99, des compilateurs acceptaient ce type
de commentaires.


13.5 Comment utiliser #include ?


#include comporte trois formes principales :


#include <fichier>
#include "fichier"
#include tokens


La première forme recherche le fichier indiqué dans les
répertoires système ; on peut les ajuster soit via des menus (dans
le cas des compilateurs avec une interface graphique), soit en
ligne de commande. Sur un système Unix, le répertoire
système classique est /usr/include/. Une fois le fichier
trouvé, tout se passe comme si son contenu était tel quel dans le
code source, là où se trouve le #include.


La deuxième forme recherche le fichier dans le répertoire
courant. Si le fichier ne s'y trouve pas, il est ensuite cherché
dans les répertoires systèmes, comme dans la première forme.


La troisième forme, où ce qui suit le #include ne
correspond pas à une des deux formes précédentes, commence par
effectuer tous les remplacements de macros dans la suite de
tokens, et le résultat doit être d'une des deux formes
précédentes.


Si le fichier n'est pas trouvé, c'est une erreur, et la
compilation s'arrête. On notera que si on se sert d'habitude de
#include pour inclure des fichiers d'en-tête (tels que
stdio.h), ce n'est pas une obligation.


13.6 Comment éviter l'inclusion multiple d'un fichier ?


Il arrive assez souvent qu'un fichier inclus en incluse un autre,
qui lui-même en inclut un autre, etc. On peut arriver à des
boucles, qui peuvent conduire à des redoublements de déclarations
(donc des erreurs, pour typedef par exemple), voire des
boucles infinies (le compilateur finissant par planter).


Pour cela, le moyen le plus simple est de « protéger » chaque
fichier par une construction de ce genre :


#ifndef FOO_H_
#define FOO_H_
/* ici, contenu du fichier */
#endif /* FOO_H_ */


Ainsi, même si le fichier est inclus plusieurs fois, son contenu
ne sera actif qu'une fois. Certains préprocesseurs iront même
jusqu'à reconnaître ces structures, et ne pas lire le fichier
si la macro de protection (ici FOO_H_) est encore
définie.


Il y a eut divers autres moyens proposés, tels que #import
ou #pragma once, mais ils ne sont pas standards, et encore
moins répandus.


13.7 Comment définir une macro ?


On utilise #define. La forme la plus simple est la
suivante :


#define FOO 3 + 5


Après cette déclaration, toute occurrence de l'identificateur
FOO est remplacée par son contenu (ici 3 + 5).
Ce remplacement est syntaxique et n'a pas lieu dans les chaînes
de caractères, ou dans les commentaires (qui n'existent déjà plus,
de toutes façons, à cette étape).


On peut définir un contenu vide. La macro sera remplacée, en cas
d'utilisation, par rien. Le contenu de la macro est une suite
de tokens du C, qui n'a pas à vouloir dire quelque
chose. On peut définir ceci, c'est valide :


#define FOO (({/coucou+}[}+ "zap" 123


La tradition est d'utiliser les identificateurs en majuscules pour
les macros ; rien n'oblige cependant à appliquer cet usage. Les
règles pour les identificateurs de macros sont les mêmes que pour
celles du langage.


13.8 Comment définir une macro avec des arguments ?


On fait ainsi :


#define FOO(x, y) ((x) + (x) * (y))


Ceci définit une macro qui attend deux arguments ; notez qu'il
n'y a pas d'espace entre le nom de la macro (FOO) et la
parenthèse ouvrante. Toute invocation de la macro par la suite
est remplacée par son contenu, les arguments l'étant aussi.
Ainsi, ceci :


FOO(bla, "coucou")


devient ceci :


((bla) + (bla) * ("coucou"))


(ce qui ne veut pas dire grand'chose en C, mais le préprocesseur
n'en a cure). Le premier argument est remplacé deux fois, donc,
s'il a des effets de bord (appel d'une fonction, par exemple),
ces effets seront présents deux fois.


Si la macro est invoquée sans arguments, elle n'est pas
remplacée. Cela permet de définir une macro sensée remplacer une
fonction, mais en conservant la possibilité d'obtenir un pointeur
sur la fonction.
Ainsi :


int min(int x, int y) { return x < y ? x : y; }
#define min(x, y) ((x) < (y) ? (x) : (y))
min(3, 4); /* invocation de la macro */
(min)(3, 4); /* invocation de la fonction, via le pointeur */


C'est une erreur d'invoquer une macro à argument avec un nombre
incorrect d'arguments. En C99, on peut utiliser des arguments
vides ; en C89, c'est flou et mieux vaut éviter.


13.9 Comment faire une macro avec un nombre variable d'arguments ?


Ce n'est possible qu'en C99. On utilise la construction suivante :


#define error(l, ) {
fprintf(stderr, "line %d: ", l);
fprintf(stderr, __VA_ARGS__);
}


Ceci définit une macro, qui attend au moins un argument ; tous
les arguments supplémentaires sont concaténés, avec leurs virgules
de séparation, et on peut les obtenir en utilisant
__VA_ARGS__. Ainsi, ceci :


error(5, "boo: '%s'", bla)


sera remplacé par ceci :


{ fprintf(stderr, "line %d: ", 5);
fprintf(stderr, "boo: '%s'", bla); }


Ce mécanisme est supporté par les dernières versions de la plupart
des compilateurs C actuellement développés ; mieux vaut l'éviter
si le code doit aussi fonctionner avec des compilateurs un peu
plus anciens.


Il existe aussi des extensions sur certains compilateurs. Par exemple,
sous GCC, le code suivant est équivalent à l'exemple précédent :


#define error(l, format) {
fprintf(stderr, "line %d: ", l);
fprintf(stderr, format);
}


Une autre méthode consiste à utiliser le parenthèsage des arguments :

#define PRINTF(s) printf s

PRINTF (("Vitesse du vent %d m/s", v));


13.10 Que font les opérateurs # et ## ?

L'opérateur # permet de transformer un argument d'une
macro en une chaîne de caractères. On fait ainsi :


#define BLA(x) printf("l'expression '%s' retourne %d", #x, x);
BLA(5 * x + y);


ce qui donne le résultat suivant :


printf("l'expression '%s' retourne %d", "5 * x + y", 5 * x + y);


Les éventuelles chaînes de caractères et backslashs dans
l'argument sont protégés par des backslashes, afin de
constituer une chaîne valide.


L'opérateur ## effectue la concaténation de deux
tokens. Si le résultat n'est pas un
token valide, alors c'est une erreur ; mais certains
préprocesseurs sont peu stricts et se contentent de re-séparer
les tokens.
On l'utilise ainsi :


#define FOO(x, y) x ## y
FOO(bar, qux)();


qui donne ceci :


barqux();


13.11 Une macro peut-elle invoquer d'autres macros ?


Oui. Mais il est prévu un mécanisme qui empêche les boucles
infinies.


Tout d'abord, les invocations de macros ne sont constatées que
lors de l'utilisation de la macro, pas lors de sa définition.
Si on fait ceci :


#define FOO BAR
#define BAR 100


alors on obtient bien 100, pas BAR.


Si la macro possède des arguments, chaque fois que cet argument
est utilisé (sans être précédé d'un # ou précédé ou suivi
d'un ##), il est d'abord examiné par le préprocesseur,
qui, s'il y reconnaît une macro, la remplace. Une fois les
arguments traités, le préprocesseur les implante à leur place
dans la suite de tokens générés par la macro, et gère
les opérateurs # et ##.


À la suite de cette opération, le résultat est de nouveau examiné
pour rechercher d'autres remplacements de macros ; mais si une
macro est trouvée, alors qu'on est dans le remplacement de ladite,
cette macro n'est pas remplacée. Ceci évite les boucles infinies.


Je sais, c'est compliqué. Quelques exemples :


#define FOO coucou BAR
#define BAR zoinx FOO
FOO


FOO est remplacée par coucou BAR, et le
BAR résultant est remplacé par zoinx FOO. Ce
FOO n'est pas remplacé, parce qu'on est dans le
remplacement de FOO. Donc, on obtient coucou zoinx
FOO.


Un autre exemple, plus tordu :


#define FOO(x) x(5)
FOO(FOO);


La macro FOO est invoquée ; elle attend un argument, qui
est FOO. Cet argument est d'abord examiné ; il y a
FOO dedans, mais non suivi d'une parenthèse ouvrante
(l'argument est examiné tout seul, indépendamment de ce qui le
suit lors de son usage), donc le remplacement n'a pas
lieu. Ensuite, l'argument est mis en place, et on obtient
FOO(5). Ce résultat est réexaminé ; cette fois,
FOO est bien invoquée avec un argument, mais on est dans
le deuxième remplacement, à l'intérieur de la macro FOO,
donc on ne remplace pas.
Le résultat est donc : FOO(5);


Si vous voulez utiliser ce mécanisme, allez lire une douzaine
de fois la documentation du GNU cpp, et surtout le
paragraphe 12 de l'annexe A du Kernighan &
Ritchie (2ème édition).


13.12 Comment redéfinir une macro ?


On peut redéfinir à l'identique une macro ; ceci est prévu pour
les fichiers d'en-tête inclus plusieurs fois. Mais il est en
général plus sain de protéger ses fichiers contre l'inclusion
multiple (cf. 13.6).


Redéfinir une macro avec un contenu ou des arguments différents,
est une erreur. Certains préprocesseurs laxistes se contentent
d'un avertissement. La bonne façon est d'abord d'indéfinir la
macro via un #undef. Indéfinir une macro qui n'existe
déjà pas, n'est pas une erreur.


13.13 Que peut-on faire avec #if ?

#if permet la compilation conditionnelle. L'expression
qui suit le #if est évaluée à la compilation, et, suivant
son résultat, le code qui suit le #if jusqu'au prochain
#endif, #elif ou #else est
évalué, ou pas.
Quand le code n'est pas évalué, les directives de préprocesseur
ne le sont pas non plus ; mais les #if et similaires sont
néanmoins comptés, afin de trouver la fin de la zone non compilée.


Lorsque le préprocesseur rencontre un #if, il :

- récupère l'expression qui suit le #if
- remplace les defined MACRO et
defined(MACRO) par la constante 1 si
la macro nommée est définie, 0 sinon
- remplace les macros qui restent dans l'expression
- remplace par la constante 0 tous les
identificateurs qui restent
- évalue l'expression


L'expression ne doit comporter que des constantes entières (donc,
éventuellement, des constantes caractères), qui sont promues au
type (unsigned) long (en C89) ou (u)intmax_t (en
C99). Les flottants, les pointeurs, l'accès à un tableau, et
surtout l'opérateur sizeof ne sont pas utilisables par le
préprocesseur.


Il n'est pas possible de faire agir un #if suivant
sizeof(long), pour reprendre un desiderata
fréquent. Par ailleurs, les constantes de type caractère n'ont pas
forcément la même valeur pour le préprocesseur et pour le
compilateur.


13.14 Qu'est-ce qu'un #pragma ?


C'est une indication pour le compilateur. Le préprocesseur
envoie cette directive sans la modifier. Le standard C89 ne
prévoit aucune directive standard, mais le préprocesseur
comme le compilateur sont sensés ignorer les directives
inconnues.


Le C99 définit trois #pragma qui permettent d'ajuster
le comportement du compilateur, quant au traitement des nombres
flottants et complexes.


13.15 Qu'est-ce qu'un #assert ?


C'est une extension gérée par GNU et (au moins) le
compilateur Sun (Workshop Compiler, pour
Solaris). C'est une sorte d'alternative à
#ifdef, avec une syntaxe plus agréable.
C'est à éviter, car non standard.


13.16 Comment définir proprement une macro qui comporte plusieurs statements ?

On peut ouvrir un bloc, car tout statement est
remplaçable, en C, par un bloc, mais cela pose des problèmes avec
le ; terminal du statement. La manière
recommandée est la suivante :


#define foo(x) do { f(x); printf("coucou"); } while (0)


On peut ainsi l'utiliser comme ceci :


if (bar) foo(1); else foo(2);


Si on avait défini foo sans le do et le
while (0), le code ci-dessus aurait provoqué une erreur
de compilation, car le else serait séparé du if
par deux statements : le bloc et le statement vide, terminé par le
point-virgule.


13.17 Comment éviter les effets de bord ?

Les macros peuvent être dangereuses si l'on ne fait
pas attention aux effets de bord. Par exemple si l'on
a le code suivant :


#define MAX 3 + 5

int i = MAX;


La variable i vaudra bien 8, mais si on utilise la
macro MAX ainsi :


int i = MAX * 2;


La variable i ne vaudra pas 16, mais 13. Pour éviter
ce genre de comportement, il faut écrire la macro
ainsi :


#define MAX (3 + 5)


Dans certains cas, une macro représente une expression C
complète. Il est alors plus cohérent de placer des parenthèses
vides pour simuler une fonction. Et dans ce cas il ne faut pas la
terminer par un point virgule, et


#define PRTDEBUG() (void)printf("Coucou")


ainsi, on pourra utiliser la macro par :


if (i == 10) {
PRTDEBUG();
}
else {
i++;
}


Quand une macro a des arguments il faut faire attention
à la façon de les utiliser. Ainsi la macro :


#define CALCUL(x, y) (x + y * 2)


a des effets de bord suivant la façon de l'utiliser :


int i = CALCUL(3, 5);


donnera bien un résultat de 13, alors que le même résultat
serait attendu avec :


int i = CALCUL(3, 2 + 3);


qui donne 11. Pour éviter cela, il suffit de placer des
parenthèses sur les arguments de la macro:


#define CALCUL(x, y) ((x) + (y) * 2)


Un effet de bord qui ne peut être contourné survient quand
la macro utilise plusieurs fois une variable :


#define MAX(x, y) ((x) > (y) ? (x) : (y))


Si on utilise la macro comme cela :


i = MAX(j, k);


On obtiendra un résultat correct, alors qu'avec :


i = MAX(j++, k++);


une des variables j ou k sera incrémentée 2
fois. Pour éviter ce genre de comportement, il faut remplacer la
macro par une fonction, de préférence inline (C99) :


inline int max(int x, int y)
{
return x > y ? x : y;
}


En règle générale, quand on utilise une fonction avec un nom
en majuscule (comme MAX), on s'attend à ce que ce soit en fait
une macro, avec les effets de bord qui en découlent. Alors que
si le nom est en minuscule, il s'agit sûrement d'une véritable
fonction. Cette règle n'est hélas pas générale et donc il
convient de vérifier le véritable type d'une fonction si l'on
ne veut pas être surpris lors de son utilisation.


13.18 Le préprocesseur est-il vraiment utile ?

La réponse est oui. Il existe un mouvement qui voudrait faire
disparaître le préprocesseur, en remplaçant les #define
par des variables constantes, et avec un quelconque artifice
syntaxique pour importer les déclarations d'un fichier d'entête.


Il s'avère qu'on a vraiment besoin d'un mécanisme polymorphe
(comme une fonction qui accepterait plusieurs types différents),
et que seules les macros apportent ce mécanisme en C. Les
anti-préprocesseurs acharnés parlent d'adopter le mécanisme
des templates du C++, mais ça ne risque pas d'arriver de sitôt.


Dans la vie de tous les jours, l'utilisation du préprocesseur,
avec quelques #include et des #define sans
surprise, ne pose pas de problème particulier, ni de maintenance,
ni de portabilité.


13.19 Approfondir le sujet.


Outre le Kernighan & Ritchie, qui comporte
quelques pages très bien faites sur le sujet, on peut lire la
documentation au format info du GNU cpp ;
elle est assez partiale dans certains cas (condamnation explicite
des trigraphes, par exemple) mais assez riche en enseignements.


Pour le reste, chaque compilateur C vient avec un préprocesseur,
et il existe quelques préprocesseurs indépendants (un écrit
par Dennis Ritchie en personne, qu'on voit inclus dans
lcc, et aussi ucpp, une oeuvre à moi :
<http://www.di.ens.fr/~pornin/ucpp/>).


14. Fonctions de la bibliothèque


14.1 Comment convertir un nombre en une chaîne de caractères ?


Il suffit d'utiliser sprintf().


char sz[4];
sprintf(sz, "%d", 123);


Pour convertir un double ou un long,
il faut utiliser %f ou %ld.


Le problème de sprintf() est qu'il faut réserver
assez de place pour la chaîne résultat. Ainsi le code


char sz[6];
sprintf(sz, "%u", i);


marchera sur des machines où i est un entier 16 bits,
mais il plantera si i est un entier plus grand (>
99999).


En C99, la fonction snprintf permet d'éviter les débordements :


char sz[4];
n = snprintf(sz, sizeof sz, "%d", 123);
if (n < 0 || n >= sizeof sz)
erreur();


14.2 Comment convertir une chaîne en un nombre ?


Si le nombre espéré est un entier, il faut utiliser la fonction
strtol(). Elle convertit une chaîne en un entier long,
dans une base donnée.


Si le nombre est un réel (float ou double),
alors la fonction strtod() fera très bien l'affaire.


char test[] = " -123.45e+2";
char * err = NULL;

errno = 0;
double result = strtod(test, &err);

if (err == test) {
printf("Erreur de conversion :");
}
else if (errno == ERANGE) {
printf("Depassement :");
}
else {
printf("Conversion reussie :");
if(*err == '') {
printf("Pour toute la chaine");
}
}


Si le nombre est un long double (C99 seulement) alors la
fonction strtold() est à préférer.


14.3 Comment découper une chaîne ?


La fonction strtok() est faite pour ça !


char sz1[] = "this is,an example ; ";
char sz2[] = ",; ";
char *p;

p = strtok(sz1, sz2);
if (p != NULL) {
puts(p);
while ((p = strtok(NULL, sz2)) != NULL) {
puts(p);
}
}


Attention, la fonction strtok() souffre au moins des
problèmes / caractéristiques suivants :

- Elle fusionne les délimiteurs adjacents. En cas
d'utilisation d'une virgule comme séparateur, "a,,b,c" est
séparée en trois éléments et non quatre.
- Le caractère du délimiteur est perdu, car il est
remplacé par un caractère nul (0).
- Elle modifie la chaine qu'elle analyse. C'est un
défaut, car cela oblige à faire une copie de la chaine en cas
d'utilisation ultérieure. Cela signifie aussi que l'on ne peut
pas séparer une chaine littérale avec.
- On ne peut utiliser qu'un appel de cette fonction à la
fois. Si une séquence de strok() est en cours, et
qu'une autre démarre, l'état de la première est perdue. Ce
n'est pas grave pour les petits programmes, mais il est facile
de se perdre dans la hiérarchie des fonctions imbriquées dans
des programmes plus importants. En d'autre termes,
strtok() brise les principes de l'encapsulation.


Dans des cas simples, on pourra utiliser la fonction
strchr().


14.4 Pourquoi ne jamais faire fflush(stdin) ?


La fonction fflush() a un comportement défini
uniquement sur les flux ouverts en écriture tels que
stdout. Il est possible que sur votre système, appliquer
cette fonction à stdin soit possible, mais c'est alors
une extension non standard. Le comportement est indéterminé, et
imprévisible.


Il faut bien comprendre que stdin n'est pas forcément
relié au clavier, mais peut être rattaché à un réseau, un fichier,
etc.


14.5 Comment vider le buffer associé à stdin ?


Une bonne manière est de lire sur le flux tant qu'il n'est pas
vide, avec les fonctions habituelles comme fgets() ou
getchar().
Voici un exemple avec cette dernière :


c = getchar();
if (c != '')
while ( (getchar()) != '') {
};


Ce morceau de code permet de lire un caractère, et vide ce qui
peut rester dans le buffer, notamment le '' final.


14.6 Pourquoi mon printf() ne s'affiche pas ?


Le flux standard stdout, sur lequel écrit
printf() est bufferisé. C'est à dire que les caractères
sont écrits dans un tampon (une zone mémoire). Lorque celui-ci est
plein, ou lorsqu'une demande explicite est faite, il est vidé dans
le flux proprement dit (sur l'écran généralement).
Tant que le buffer n'est pas vidé, rien ne s'affiche.


Pour vider le buffer, il y a trois possibilités :


- Le buffer est plein
- Il est vidé explicitement par l'appel de la
fonction fflush()
(cf. 14.4)
- La chaîne de caractères se termine par un
''


14.7 Comment obtenir l'heure courante et la date ?


Il faut simplement utiliser les fonctions time(),
ctime() et/ou
localtime(), qui contrairement à leurs noms donnent
l'heure et la date.


Voici un petit exemple :


#include <stdio.h>
#include <time.h>

int main(void) {
time_t now;
time(&now);

printf("Il est %.24s.", ctime(&now));

return 0;
}


14.8 Comment faire la différence entre deux dates ?


Il faut simplement utiliser la fonction difftime().
Cette fonction prend deux time_t en paramètres et renvoie
un double.


14.9 Comment construire un générateur de nombres aléatoires ?


Ce n'est pas possible.
La bibliothèque standard inclut un générateur pseudo-aléatoire,
la fonction rand().

Toutefois, l'implémentation dépend du système, et celle-ci n'est
généralement pas très bonne (en terme de résultats statistiques).
Si rand() ne vous suffit pas (simulation numérique ou
cryptologie), il vous faudra regarder du coté de
bibliothèques mathématiques, dont de nombreuses se trouvent sur
Internet. En particulier, on consultera les paragraphes 7-0 et 7-1
des Numerical Recipes in C (cf.
4.5) et le volume 2 de TAoCP
(cf. 3.9).


14.10 Comment obtenir un nombre pseudo-aléatoire dans un intervalle ?


La méthode la plus simple à faire,


rand() % N


qui renvoie un nombre entre 0 et N-1 est aussi
la moins bonne.
En effet, les bits de poids faibles ont une distribution très peu
aléatoire. Par exemple, le bit de poids le plus faible a une
distribution qui peut être celle-ci sur un mauvais générateur :

0 1 0 1 0 1 0 1 0 1


Voici la méthode préconisée dans Numerical Recipes
(cf.4.5) :


(int)((double)rand() / ((double)RAND_MAX + 1) * N)


RAND_MAX est défini dans stdlib.h,
et N doit être plus petit que RAND_MAX.


14.11 À chaque lancement de mon programme, les nombres pseudo-aléatoires sont toujours les mêmes ?


C'est normal, et c'est fait exprès. Pour contrer cela, il faut
utiliser une graine pour le générateur qui change à chaque
lancement du programme. C'est la fonction srand() qui
s'en charge.


On peut utiliser l'heure système, avec time(), de la
facon suivante :


srand(time(NULL));


Notez qu'il est peu utile d'appeler la fonction srand()
plus d'une fois par programme.


14.12 Comment savoir si un fichier existe ?


En C ISO, le seul moyen de savoir si un fichier existe, c'est
d'essayer de l'ouvrir.


{
FILE *fp = fopen ("fichier.txt", "r");

if (fp == NULL)
{
fputs ("Le fichier n'existe pas,"
"ou vous n'avez pas les droits necessaires"
"ou il est inaccessible en ce moment"
, stderr);
}
else
{
/* operation sur le fichier */

fclose(fp);
}
}


Dans la norme POSIX, il existe la fonction
access(), mais certains systèmes n'implémentent pas
cette interface.


14.13 Comment connaître la taille d'un fichier ?


Malheureusement, les fonctions stat() et
fstat() de la norme POSIX ne sont pas
reprises dans la norme ISO.
La seule solution standard est d'utiliser fseek() et
ftell().

Toutefois, cela ne marche pas pour les très gros fichiers
(supérieurs à LONG_MAX).


14.14 Comment lire un fichier binaire proprement ?


Il faut ouvrir le fichier en mode « binaire », en passant la chaîne
"rb"
en mode d'ouverture à la fonction fopen().
Cela évite les transformations inopportunes et les problèmes des
caractères de contrôle.


De même, pour écrire dans un fichier binaire, on utilise le mode
"wb".


14.15 Comment marquer une pause dans un programme ?


Il n'y a pas de fonction standard pour cela.
Il existe toutefois la fonction sleep() en
POSIX, elle provoque une attente passive pour une
durée donnée en secondes.


14.16 Comment trier un tableau de chaînes ?


La fonction qsort() est une bonne fonction de tri,
qui implémente le Quick Sort.
Le plus simple est de donner un exemple :


/* Fonction qui compare deux pointeurs
vers des chaines pour qsort */
int pstrcmp(const void * p1, const void * p2){
return strcmp(*(char * const *)p1, *(char * const *)p2);
}


Les paramètres doivent être des pointeurs génériques pour
qsort().
p1 et p2 sont des pointeurs sur des chaînes.
Un tableau de chaînes doit être pris au sens d'un tableau de
pointeurs vers des char *.


L'appel à qsort() ressemble alors à :


qsort(tab, sizeof tab, sizeof *tab, pstrcmp);


14.17 Pourquoi j'ai des erreurs sur les fonctions de la bibliothèque, alors que j'ai bien inclus les entêtes ?


Les en-têtes (les .h) ne contiennent que les
prototypes des fonctions.
Le code proprement-dit de ces fonctions se trouve dans des
fichiers objets. Ce code doit être « lié » au tien. Cela est fait
par un éditeur de liens.


Pour certaines fonctions, il faut spécifier explicitement à
l'éditeur de liens où il peut les trouver (et ce particulièrement
pour les fonctions non-standard).


Par exemple, sous Unix, pour utiliser les fonctions mathématiques,
il faut généralement lier le programme avec la bibliothèque adéquate :


cc -lm monfic.o -o monprog


15. Styles


15.1 Comment bien programmer en C ?


La chose la plus importante est de commenter un programme.
Il ne s'agit pas de décrire en français tout ce que fait chaque
ligne de code, mais de préciser le fonctionnement des opérations
complexes, d'expliquer le rôle des variables, de dire à quoi
servent les fonctions. Choisir des noms de variables et de
fonctions explicites est une bonne façon de commenter un
programme.


Tout morceau de code qui n'est pas standard doit être abondamment
commenté afin de rendre le portage vers d'autres cibles le moins
fastidieux possible, l'idéal étant d'utiliser des macros.


Il est également important de bien structurer son programme en
modules, puis en fonctions.
Certains vont jusqu'à dire qu'une fonction ne doit pas dépasser la
taille d'un écran.


Les déclarations et prototypes doivent être regroupés dans des
fichiers d'en-têtes, avec les macros.


Enfin, il est très important de bien présenter le code, avec une
indentation judicieuse et des sauts de
ligne. (cf. 15.2)
Il est généralement admis que les lignes ne doivent pas dépasser
80 caractères.


Pour le reste, c'est une histoire de goût.


15.2 Comment indenter proprement du code ?


L'indentation du code est une chose essentielle pour la
lisibilité. Certaines personnes utilisent des tabulations, ce qui
est une mauvaise habitude. La largeur de ces tabulations varie
d'un éditeur à un autre. Des éditeurs remplacent les tabulation
par un nombre d'espaces fixe et d'autres encore utilisent des
tabulations de taille variable. Ne parlons pas des imprimantes ou
des lecteurs de news
Tout ceci rend l'utilisation des tabulations difficile.


Pour éviter tout problème, et améliorer la lisibilité du code,
il faut utiliser uniquement des espaces.
Un éditeur correct doit pouvoir générer un nombre d'espaces fixe
(voire une indentation automatique) lorsqu'on appuye sur la touche
<TAB> (ou autre raccourci).
Personnellement, je règle à 4 espaces par tabulation.


15.3 Quel est le meilleur style de programmation ?


Comme vous vous en doutez, il n'y en a pas.
Le plus important est d'en avoir un, et de le suivre.
Quand on utilise un type d'indentation ou un style de nommage,
il faut l'utiliser dans tout le programme (voire dans tout ses
programmes). C'est la régularité qui donne la lisibilité.


Il existe des styles de programmations fréquemment utilisés en C,
comme les styles K&R ou GNU.
Le style K&R est le style « historique », et
c'est pourquoi il est très utilisé.
Le style GNU est utilisé pour tous les projets de la
Free Software Fundation.


Sur le site FTP <ftp://caramba.cs.tu-berlin.de>,
le repertoire /pub/doc/style contient quelques
documents intéressants sur la question.


15.4 Qu'est-ce que la notation hongroise ?


C'est une convention de nommage des objets, inventée par Charles
Simonyi. Le principe est de faire précéder le nom des
variables par un identificateur de type.
Par exemple, une chaîne de caractère représentant un nom sera
nommée szname, sz signifiant « string
zero », ou chaîne terminée par un ''.


Personnellement, je ne trouve pas cette convention toujours
pratique. Le nom de la variable doit avant tout refléter son rôle.


15.5 Pourquoi certains écrivent-ils if(0==x) et non if(x==0) ?


Il arrive souvent que l'on écrive = au lieu de
==. Comme 0 n'est pas une lvalue
(cf. 10.10), cette étourderie
provoquera une erreur, simple à détecter.
Dans le même genre, on peut écrire while (0 == x).


Certains compilateurs préviennent lorsque l'on fait une
affectation là ou est attendu un test. C'est le cas de GNU
CC, avec l'option -Wall.


Lorsque l'on écrit while ( c = fct() ), certains
compilateurs râlent en croyant que l'on s'est trompé entre le
= et le ==. Pour éviter cela, il suffit de
rajouter un paire de parenthèses.


while ( (c= fct()) ) {
/* */
}


15.6 Pourquoi faut-il mettre les '{' et '}' autour des boucles ?


C'est une précaution contre les erreurs du genre :


for(i = 0; i < N; i++);
tab[i] = i;


De plus, cela permet une plus grande simplicité dans l'évolution
du code. En effet, les programmes ont tendance à s'épaissir avec
le temps.


15.7 Pourquoi certains disent-ils de ne jamais utiliser les goto ?


Le goto est une instruction qui permet de casser l'aspect
structuré d'un programme. Des goto mal utilisés
permettent de rendre un code totalement illisible (code
spaghetti), d'autant plus qu'avec les structures de
boucles traditionnelles, on peut souvent s'en passer.


Toutefois, il arrive parfois que l'utilisation d'un goto
rende le code plus propre. C'est le cas, par exemple, des sorties
de boucles imbriquées en cas d'erreur.
Cela rejoint le cas plus général des gestions d'exceptions
internes.


Poser comme règle de ne jamais utiliser les
goto est une absurdité.
Par contre, avertir le programmeur de l'utiliser avec parcimonie,
et avec beaucoup de précautions me semble une bonne chose.


15.8 Pourquoi commenter un #endif ?

#endif ne peut être suivi par autre chose qu'un
commentaire. On commente donc
pour savoir à quelle directive il correspond :


#if FOO
/* du code ou des directives */
#ifdef BAR
/* encore du code ou des directives */
#endif /* BAR */
/* encore du code ou des directives */
#endif /* FOO */


15.9 Où trouver de la doc sur les différents styles ?

Le document suivant contient des règles de base à suivre pour
programmer proprement :
<ftp://ftp.laas.fr/pub/ii/matthieu/c-superflu/c-superflu.pdf>


Sachez également qu'il existe un programme indent
issu de BSD qui réindente automatiquement du code,
suivant un style donné. Les options classiques de la version
GNU de cet utilitaire sont -kr (pour le
style décrit dans K&R), -gnu (pour le
style utilisé dans les projets GNU) ou encore
-orig (pour le style BSD).


Sous Unix, on trouve également la commande
cb avec l'option -sj pour avoir le style
K&R.


15.10 Comment bien structurer son programme ?


Il n'y a pas de réponse définitive, mais voici une piste :


- Suite à l'analyse (conception) il est possible de découper
le projet en une multitude de systèmes et de sous-systèmes.

- Chaque sous-système fait l'objet d'un module composé d'un
fichier source(xxx.c) et d'un fichier d'en-tête (xxx.h).

- Les systèmes sont organisés hiérarchiquement (arbre) de
façon à éviter les dépendences croisées.


Evidement, tout ça est peu théorique. De plus, il existes des
techniques avancées (ADT, callback) qui permettent les appels
croisés propres. (c à d sans dépendances croisées). L'avantage est
que chaque module est testable individuellement (et le plus
souvent réutilisable).


La phase suivante du développement est l'intégration. Elle consiste
à mettre la colle qui va bien pour faire tenir tout les modules
ensembles et à tester le fonctionnement de l'ensemble.


Nous ne traitons pas ici des différentes méthodes d'analyse et de
conception. Rappelons toutefois les 5 phases (en 'V') de
développement d'un projet :


1 - Spécification 5 - Validation
/
2 - Conception 4 - Integration
/
3 - Codage et test unitaire


Voir à ce sujet les questions 9.3
et 13.5.

Merci à Emmanuel Delahaye pour cette réponse.


16. Autres


16.1 Comment rendre un programme plus rapide ?


Il y a deux raisons possibles à la « lenteur » d'un programme.


La première vient de l'écriture du code lui-même,
les entrées/sorties, les allocations dynamiques,
de nombreux appels de fonctions, des boucles, etc.
Le programmeur a généralement peu intérêt à modifier son code,
tout au plus pourra-t-il remplacer les petites fonctions le plus
souvent appelées par des macros et tenter de limiter les
boucles. On pourra aussi améliorer les entrées/sorties et les
allocations si c'est possible. Il reste enfin les options de
compilation sur lesquelles on peut jouer.


L'autre raison vient de la complexité théorique des algorithmes
utilisés. Dans ce dernier cas, il faut chercher un meilleur
algorithme. Cet aspect est développé par exemple dans
<http://www.enseignement.polytechnique.fr/profs/informatique/Jean-Jacques.Levy/poly/polyx-cori-levy.ps.gz>


Il existe des outils de profilage de programmes. Il
s'agit de compiler les sources avec une bibliothèque puis de
lancer le programme. On lance alors un lociciel associé à la
bibliothèque. Le résultat est un fichier où est détaillé le temps
passé dans chaque fonction,
le nombre d'appels, etc. Sur Unix-like,
le projet GNU propose gprof
(cf. 4.6).


Rappelons tout de même que la vitesse d'exécution d'un programme
(hors problèmes d'algorithmique) est peu souvent critique, et
qu'il est bien plus important de fournir un code lisible.


16.2 Quelle est la différence entre byte et octet ?


L'unité de mémoire élémentaire du C est le byte.
Le byte au sens anglo-saxon et donc pas
l'octet au sens francophone (caractère).


En fait un byte correspond à un caractère non-signé
(unsigned char), lequel peut prendre plus (ou moins) de 8
bits. En principe, en français, on parle dans ce cas de
multiplet (et peut-être bientôt de
codet) comme traduction officielle de
byte dans ce sens.


(Merci à Antoine Leca).


16.3 Peut-on faire une gestion d'exceptions en C ?


Oui, c'est possible, en utilisant le couple
setjmp()/longjmp().


#include <stdio.h>
#include <stdlib.h>
#include <setjmp.h>

jmp_buf env;

long fact(long x) {
long i, n;

if (x < 0)
longjmp(env, 1);
for (n = 1, i = 2; i <= x; i ++)
n *= i;
return n;
}

long comb(long k, long n) {
if (k < 0 || n < 1 || k > n)
longjmp(env, 2);
return fact(n) / (fact(k) * fact(n - k));
}

int main(int argc, char *argv[]) {
if (argc < 3) {
fprintf(stderr, "pas assez d'arguments");
return EXIT_FAILURE;
}
if (setjmp(env)) {
fprintf(stderr, "erreur de calcul");
return EXIT_FAILURE;
}
printf("%ld", comb(strtol(argv[1], 0, 0),
strtol(argv[2], 0, 0)));
return 0;
}


Voilà un programme qui calcule le coefficient binomial des deux
arguments ; main() appelle comb() qui
appelle fact().
Ces fonctions vérifient un peu les arguments, et on voudrait
renvoyer une erreur en cas de problème ; mais :


- Je renvoie déjà le résultat, il faudrait qu'il y ait des
résultats « impossibles » pour y coder les erreurs ; c'est le cas
ici (le résultat est toujours supérieur ou égal à 1) mais ça
demande une analyse mathématique pas toujours facile.
- Je ne veux pas tester les codes d'erreurs à chaque
invocation d'une fonction. Cela peut devenir lourd syntaxiquement,
et ça consomme du temps CPU.


setjmp() sauvegarde l'état du programme au moment de
l'appel, et renvoie 0.
longjmp() remplace le contenu de la pile d'exécution
par la sauvegarde, et le programme se trouve à nouveau à l'endroit
de l'appel à setjmp(). Celle-ci renvoie alors une
valeur passée en paramètre de longjmp() (dans
l'exemple, 1 pour une erreur dans fact() et
2 pour une erreur dans comb()).


La méthode présentée ici est assez rustique. Il existe des
mécanismes de POO [5]
en C bien plus évolués. Vous pouvez à ce sujet
allez voir l'excellent document :
<http://ldeniau.home.cern.ch/ldeniau/html/oopc/oopc.html>.
Allez voir aussi le document suivant :
<http://cern.ch/Laurent.Deniau/html/exception/exception.html>.

Voir aussi la question 3.10.


16.4 Comment gérer le numéro de version de mon programme ?


Serge Paccalin propose la chose suivante :


#define STR_(a) #a
#define STR(a) STR_(a)
#define VERSION 4
printf("This is version " STR(VERSION) " of the program");


En effet, quelque chose comme :

#define STR(a) #a
#define VERSION a
printf("This is version " STR(VERSION) " of the program");


fait intervenir la concaténation des chaînes trop tôt ce qui fait
que le résultat de cette dernière séquence renvoie :


This is version VERSION of the program


16.5 Pourquoi ne pas mettre de `_' devant les identifiants ?

Well well well, ce n'est pas si facile.


Les vrais identificateurs réservés sont :


- les mots clés tels que if, for,
switch, long,
- les identificateurs commençant par deux `_'
- les identificateurs commençant par un `_'
suivi d'une lettre majuscule.


Ensuite, il y a les headers standards et la bibliothèque. Les
headers sont libres d'utiliser des identificateurs commençant par
un `_' et suivis d'une lettre minuscule, comme `_liste', mais
c'est pour définir quelque chose qui a un « file scope »,
c'est-à-dire une portée globale à la « translation unit » (le
fichier source C et les fichiers qu'il inclut). Donc, globalement,
on ne doit pas s'en servir pour définir quelque chose qui a ce «
file scope ».


Ça veut dire quoi ? Que les choses suivantes sont interdites :


typedef int _foo;
struct _bar {
int x;
char * y;
};
void _f(void);


En revanche, les choses suivantes sont autorisées :


void f(int _x[]) {
typedef int _foo;
struct _bar {
int x;
char *y;
};
extern void _g(void);
}
struct qux {
long _truc;
};


J'attire l'attention du public ébahi sur les quatre points
suivants :


- En définissant des identificateurs à portée réduite (bloc,
prototype, fonction), on peut masquer des identificateurs définis
par les headers standards, identificateurs qui pouvaient
intervenir dans des macros définies dans lesdits headers ; comme
toutes les fonctions standards peuvent être surchargées par des
macros, à l'intérieur d'un bloc où on a joué à définir un
identificateur de type _foo, toute utilisation d'une
facilité fournie par un header peut déclencher un « undefined
behaviour » (par exemple, l'ordinateur utilise spontanément son
modem pour téléphoner à la belle-mère du programmeur et l'inviter
à venir dîner à la maison).

- Le extern void _g(void); explicite le fait que la
restriction est sur le scope et pas le linkage. Bien entendu, il
faut que la fonction _g() soit définie quelque part, avec
un external linkage, ce qui ne peut pas se faire en C
standard. Donc cette déclaration, quoique valide, doit avoir un
pendant dans une autre translation unit, qui ne peut pas être
fabriqué de façon standard. Pour compléter, rajoutons que la
définition n'a besoin d'exister que si on se sert effectivement de
la fonction. Donc on est en fait autorisé à faire une déclaration
inutile qui pourrait faire planter des implémentations non
conformes mais courantes. How useful.

- Quand bien même on aurait le droit, rares sont les
implémentations complètement conformes de ce point de vue. Par pur
pragmatisme, on évite donc de jouer avec des identificateurs
commençant par un `_' suivi d'une lettre minuscule.

- Chaque header apporte ses propres restrictions ; par
exemple, <ctype.h> peut déclarer n'importe quel
identificateur qui commence par "is" ou "to" suivi d'une lettre
minuscule. Ces identificateurs sont réservés pour ce qui est de
l'external linkage, ce qui veut dire que même si on n'inclut pas
<ctype.h>, on ne doit pas définir, entre autres, de
variable globale "iszoinx" qui ne soit pas protégé par le mot clé
static.


16.6 À quoi peut servir un cast en (void) ?


Il y a principalement deux utilités à caster une expression en
(void).


La première ut
Vos réponses
Gagnez chaque mois un abonnement Premium avec GNT : Inscrivez-vous !
Trier par : date / pertinence
Laurent Deniau
Le #982622
Guillaume Rumeau wrote:
Archive-Name: fr/comp/lang/faq-c-4


En passant rapidement a travers:

Dans le point 13.19, je croyais que c'etait Thomas Pornin qui avait
ecrit ucpp ? Et le lien n'est plus valide (en tout cas il ne marche pas
pour moi).

Dans le point 16.2, 'lequel peut prendre plus (ou moins) de 8 bits'. Un
char ne peut pas avoir moins de 8 bits.

a+, ld.

Benoit Izac
Le #982621
Bonjour,

le 27/02/2007 à 15:19, Laurent Deniau a écrit dans le message

Archive-Name: fr/comp/lang/faq-c-4


En passant rapidement a travers:

Dans le point 13.19, je croyais que c'etait Thomas Pornin qui avait
ecrit ucpp ? Et le lien n'est plus valide (en tout cas il ne marche
pas pour moi).


Personne n'a dit le contraire (c'est Thomas qui a écrit le passage dans
la FAQ).
--
Benoit Izac


Laurent Deniau
Le #982619
Benoit Izac wrote:
Bonjour,

le 27/02/2007 à 15:19, Laurent Deniau a écrit dans le message

Archive-Name: fr/comp/lang/faq-c-4
En passant rapidement a travers:


Dans le point 13.19, je croyais que c'etait Thomas Pornin qui avait
ecrit ucpp ? Et le lien n'est plus valide (en tout cas il ne marche
pas pour moi).


Personne n'a dit le contraire (c'est Thomas qui a écrit le passage dans
la FAQ).

Oui, sauf que son nom apparait nul part alors quand on lit 'une oeuvre à
moi', cela refere a Guillaume Rumeau pour moi.

a+, ld.



Benoit Izac
Le #982618
Bonjour,

le 27/02/2007 à 15:47, Laurent Deniau a écrit dans le message

Oui, sauf que son nom apparait nul part alors quand on lit 'une oeuvre
à moi', cela refere a Guillaume Rumeau pour moi.


Car tu commences à lire par la fin (cf. 2.3). Mais il est vrai que ça
prête à confusion.

--
Benoit Izac

Publicité
Poster une réponse
Anonyme