Twitter iPhone pliant OnePlus 11 PS5 Disney+ Orange Livebox Windows 11

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

2 réponses
Avatar
Guillaume Rumeau
Archive-Name: fr/comp/lang/faq-c-3

---------------------------------------------------------------------------
FAQ de fr.comp.lang.c

18 avril 2003
Partie 3/3 (sections 12 à 17)
---------------------------------------------------------------------------


12. Allocation dynamique


12.1 Doit-on ou ne doit-on pas caster malloc() ?


Cette question est probablement celle qui revient le plus souvent
dans la discussion. Et à chaque fois, elle engendre une longue
discussion.


Certains intervenants pensent que caster la valeur de retour
de malloc() est inutile, voire dangereux.
En effet, malloc() renvoie un void *.
Or, en C, un pointeur void * est implicitement casté
lors d'une affectation vers le type de la variable affectée.
Bien sûr, expliciter le cast n'est pas interdit, et est parfois
utile.
Toutefois, caster le retour de malloc()
risque de cacher au compilateur l'oubli du prototype de
malloc().
Ce prototype se trouve dans le fichier d'en-tête <stdlib.h>.
Sans lui, malloc() sera par défaut une fonction
retournant un int et dont les paramètres seront du type
des arguments passés, ce qui peut provoquer de sérieux bugs.


La véritable erreur est l'oubli du fichier d'en-tête
<stdlib.h>, et non pas le cast de malloc() en
lui même.
Mais le cast de malloc() risque de cacher au compilateur
cette erreur.
À noter qu'il existe des outils de vérification de code et des
options sur la plupart des compilateurs [4] qui permettent de détecter ce genre d'erreur.


D'autres intervenants jugent qu'il faille tout de même caster le
retour de malloc(), afin de conserver une compatibilité
avec d'anciens compilateurs pré-ANSI, ou pour intégrer plus
facilement le code avec C++.
Evidemment, les programmeurs avertis sauront dans quelles
situations il est utile ou non de caster les void *.


Voir aussi la question 7.8


12.2 Comment allouer proprement une variable ?


Le plus portable et le plus simple est de faire ainsi :

var_t * ma_var = malloc(N * sizeof *ma_var);


Si le type de la variable change, l'allocation est toujours
valide. À noter que l'on ne caste pas le retour de
malloc()


Voir la question 12.1 à ce sujet, ainsi que la
question 12.10.


12.3 Pourquoi mettre à NULL les pointeurs après un free() ?


La fonction free() libère l'espace mémoire pointé par
le pointeur en question. Mais la valeur de celui-ci ne peut-être
changée, car en C les arguments sont passés par valeur aux
fonctions.


La variable pointeur contient après le free() une
adresse invalide.
Son utilisation peut entraîner de sérieux embêtements.
Pour éviter cela, une bonne solution consiste à affecter la valeur
NULL au pointeur après l'appel à free().


Il existe aussi certaines implémentations de l'allocation
dynamique qui fonctionnent en Garbage Collector,
c'est-à-dire, que la mémoire n'est réellement libérée que lorsque
le pointeur est mis à NULL.


Dans tous les cas, cela permet de tester facilement la validité
des pointeurs.


12.4 Pourquoi free() ne met pas les pointeurs à NULL ?


Rappelons que les paramètres des fonctions sont passés par valeur
(ou par copie). Ainsi, pour modifier la valeur du pointeur, il
faudrait passer un pointeur sur le pointeur, ce qui compliquerait
l'utilisation de free().
Mais ce n'est pas le cas, il faut donc le faire soi-même.


12.5 Quelle est la différence entre malloc() et calloc() ?


Pratiquement, calloc() est équivalent à :


/* p = calloc(m, n); */
p = malloc(m * n);
memset(p, 0, m * n);


Chaque élément est initialisé à 0. Ce 0
est un « tout bit à zéro ».
La valeur des éléments n'est pas forcément valide, suivant leur type.


Voir aussi la question 5.6.


12.6 Que signifie le message « assignment of pointer from integer » quand j'utilise malloc() ?


Cela signifie que vous avez oublié d'inclure le fichier
stdlib.h.


Voir à ce sujet la question 12.1.


12.7 Mon programme plante à cause de malloc(), cette fonction est-elle buggée ?


Il est assez facile de corrompre les structures de données
internes de malloc(). Les sources les plus plausibles
du problème sont :


- l'emploi de malloc(strlen(s)) au lieu de
malloc(strlen(s)+1).
- la libération d'un pointeur deux fois.


Il y en a d'autres...


Voir aussi les questions 12.2
et 12.8.


12.8 Que signifient les erreurs « segmentation fault » et « bus error » ?


Cela signifie que vous avez essayé d'accéder à une zone mémoire
non autorisée.
C'est souvent l'utilisation d'un pointeur non initialisé ou
NULL qui en est la cause.
Ce genre d'erreur peut aussi provenir d'une mauvaise allocation
(cf. 12.7 et
12.2)
ou de l'oubli du 0 en fin de chaîne.


12.9 Doit-on libérer explicitement la mémoire avant de quitter un programme ?


Oui, car tous les systèmes ne le font pas d'eux-mêmes.


12.10 Du bon usage de realloc()


La fonction realloc() permet de modifier la taille de
l'espace mémoire alloué à une variable.
Elle est souvent utilisée pour augmenter cette taille.


Rappelons que la mémoire allouée par malloc(),
calloc() et realloc() est fournie sous la
forme d'une zone continue (en un seul bloc).
Or, il peut arriver que la nouvelle taille demandée dépasse l'espace
disponible derrière la zone initiale.
Dans ce cas, la fonction realloc() alloue une nouvelle
zone ailleur, là ou il y a de la place, et y recopie les données
initiales. L'ancienne zone est alors libérée.


C'est pourquoi realloc() renvoie un pointeur sur
la nouvelle zone mémoire, même si l'augmentation de taille (ou la
réduction) a pu se faire sur place.
Bien sûr, comme malloc(), realloc() peut
échouer.


Voici pour résumer une bonne manière d'utiliser
realloc() :


#include <stdlib.h> /* pour realloc() et free() */

/* ... */

int * var = NULL ;
var = malloc(sizeof * var * 42) ;
if (!var) {
/* gestion des erreurs */
}

/* ... */

int * tmp = NULL ;
tmp = realloc(var, 84) ;
if (tmp) {
var = tmp ;
}
else {
/* gestion de l'erreur */
}


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 ??!\n");


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 !\n");


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 !\n");


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'\n", bla)


sera remplacé par ceci :


{ fprintf(stderr, "line %d: ", 5); \
fprintf(stderr, "boo: '%s'\n", 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\n", #x, x);
BLA(5 * x + y);


ce qui donne le résultat suivant :


printf("l'expression '%s' retourne %d\n", "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\n"); } 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\n")


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 :\n");
}
else if (errno == ERANGE) {
printf("Depassement :\n");
}
else {
printf("Conversion reussie :\n");
if(*err == '\0') {
printf("Pour toute la chaine\n");
}
}


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 != '\n')
while ( (getchar()) != '\n') {
};


Ce morceau de code permet de lire un caractère, et vide ce qui
peut rester dans le buffer, notamment le '\n' 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
'\n'


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.\n", 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,\n"
"ou vous n'avez pas les droits necessaires\n"
"ou il est inaccessible en ce moment\n"
, 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 '\0'.


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)

2 réponses

Avatar
Emmanuel Delahaye
In 'fr.comp.lang.c', wrote:

J'ai noté quelques imperfection dans cette FAQ, les voici dans le texte
:


Très bien.

13.3 À quoi sert un backslash en fin de ligne ?

printf("Hello"

" World !n");

serait mieux pour avoir l'espace entre les 2 mots.


Ok.

13.19 Approfondir le sujet.

lcc, et aussi ucpp, une oeuvre à moi :
<http://www.di.ens.fr/~pornin/ucpp/>).


http://pornin.nerim.net/ucpp/
me semble être la nouvelle URL correcte pour ce programme.


Je te crois.

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

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();


Peut-être que ça vaut le coup d'inclure l'exemple fourni dans la page de
man anglaise pour Linux :


S'agit-il bien du snprintf () de C99, et non de celui de gcc d'avant C99?

--
-ed- [remove YOURBRA before answering me]
The C-language FAQ: http://www.eskimo.com/~scs/C-faq/top.html
<blank line>
FAQ de f.c.l.c : http://www.isty-info.uvsq.fr/~rumeau/fclc/


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

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();


Peut-être que ça vaut le coup d'inclure l'exemple fourni dans la page de
man anglaise pour Linux :


S'agit-il bien du snprintf () de C99, et non de celui de gcc d'avant C99?


A partir de la glibc 2.1, snprintf est conforme à C99, il renvoie le
nombre de caractères qui aurait du être écrits.

Pour les versions précédentes (jusqu'à la glic 2.0.6), snprintf
renvoyait -1 quand la taille était dépassée.

Le morceau de code cite dans mon précédent post et présent dans la page
de man tient compte de cette particularité de la glibc. Il n'est pas
très difficile de le modifier si on est sur d'utiliser un snprintf C99
compatible.

--
Loïc

"heaven is not a place, it's a feeling"