[FAQ] fr.comp.lang.c - partie 3/4
Guillaume Rumeau <guillaume.rumeau@wanadoo.fr>
Archive-Name: fr/comp/lang/faq-c-3
Archive-Name: fr/comp/lang/faq-c-3
---------------------------------------------------------------------------
FAQ de fr.comp.lang.c
18 avril 2003
Partie 3/4 (sections 10 à 12)
---------------------------------------------------------------------------
10. Expressions
10.1 Le type Booléen existe-t-il en C ?
Oui, il existe un type booléen en C. C'est un ajout de la dernière
version de la norme (C99). Il s'agit du type _Bool
défini dans <stdbool.h>. Cet en-tête contient
également les définitions de true et false.
Une macro bool est souvent définie comme équivalent à
_Bool.
Rappelons également qu'en C, une valeur est « fausse » si elle est
nulle (ou équivalent), et « vraie » sinon. Les définitions de
true et false suivent cette règle. Ainsi, la
valeur entière de true est 1 et celle de
false est 0.
10.2 Un pointeur NULL est-il assimilé à une valeur fausse ?
Oui, la valeur NULL est apparentée à un 0.
Les écritures if(p != NULL) et if(p)
sont donc équivalentes. De même, if(p == NULL) est
équivalent à if(!p).
Voir aussi les questions 10.1 et 7.5.
10.3 Que donne l'opérateur ! sur un nombre négatif ?
L'opérateur ! sur un nombre négatif donne bien ce que l'on
attend, à savoir 0.
Voir aussi la question 10.1.
10.4 Que vaut l'expression a[i] = i++ ?
Ce genre d'expression fait partie des « undefined
behaviour », ou comportement indéfini. Cela signifie que le
résultat d'une telle opération dépend du compilateur.
L'opérateur ++ modifie la valeur de i, alors que
celle-ci est utilisée ailleurs dans l'expression.
C'est ce que l'on appelle un « effet de bords ».
10.5 Pourtant, i++ vaut i ?
Effectivement, i++ vaut i, avant
l'incrémentation. Toutefois, rien ne dit dans quel sens est
calculée l'expression a[i] = i++.
Est-ce le i++ qui est évalué avant le a[i], ou
le contraire ?
On n'en sait rien, c'est pourquoi l'on dit que c'est un
comportement indéfini.
10.6 En est-il de même pour i++ * i++ ?
Oui. La norme ne précise pas pour les opérateurs binaires dans
quel ordre les opérandes sont évalués.
10.7 Peut-on utiliser les parenthèses pour forcer l'ordre d'évaluation d'une expression ?
Pas en général.
Les parenthèses ne donnent qu'un ordre partiel d'évaluation, entre
les opérateurs.
Voici un petit exemple :
a = f() + g() * h();
b = (f() + g()) * h();
Les parenthèses dans la deuxième expression modifient l'ordre
d'évaluation de l'addition et de la multiplication.
Par contre, l'ordre dans lequel seront évaluées les fonctions est
indéfini, dans l'une ou l'autre des deux expressions. Cela ne
dépend que du compilateur.
Pour forcer un ordre d'évaluation, il faut utiliser une écriture
séquentielle, avec des variables temporaires.
tf = f();
tg = g();
th = h();
b = (tf + tg) * th;
On a alors un comportement parfaitement défini sur toutes les
cibles, quel que soit le compilateur.
10.8 Qu'en est-il des opérateurs logiques && et || ?
Ces deux opérateurs forment une exception à la règle.
La norme prévoit que les opérandes de ces deux opérateurs soient
évalués de gauche à droite.
De plus, l'évaluation s'arrête dès que le résultat est connu.
10.9 Comment sont évaluées les expressions comprenant plusieurs types de variables ?
Le principe est simple.
Si les variables sont de types différents, il y aura un
cast implicite vers le type le plus précis. On parle
de promotion.
Voici les règles de base :
- Si l'un des opérandes est un long double, l'autre
est converti en un long double.
- Sinon, si l'un des opérandes est un double,
l'autre est converti en un double.
- Sinon, si l'un des opérandes est un float,
l'autre est converti en un float.
- Sinon, les opérandes de types char et
short sont convertis en int.
- Enfin, si l'un des opérandes est un long, l'autre
est converti en un long.
En C99, il faut rajouter le type long long.
Cela se complique dans le cas d'opérandes unsigned.
Les comparaisons entre valeurs signées et non signées dépendent de
la machine et de la taille des différents types.
10.10 Qu'est-ce qu'une lvalue ?
lvalue est un terme qui est utilisé pour définir les
expressions que l'on peut mettre à gauche d'une affection.
Toute variable modifiable est une lvalue.
Quand le compilateur prévient que le membre gauche d'une
affectation n'est pas une lvalue, c'est souvent parce
que l'on ne voulait pas faire une affectation, mais une
comparaison (cf. 15.5).
11. Nombres en virgule flottante
11.1 J'ai un problème quand j'imprime un nombre réel.
Sur la plupart des architectures, les nombres réels (dits
flottants) sont représentés en base 2, comme pour les
entiers. Ainsi, le nombre 3.1 ne peut s'écrire exactement en base
2. La représentation binaire est un arrondi qui dépend de la
précision du codage des flottants, et des choix du compilateur.
De plus, avec une fonction comme printf(), le nombre à
imprimer est converti en base 2 puis reconverti en base 10, ce qui
augmente encore les imprécisions.
Il est préférable d'utiliser les double, qui ont une
précision supérieure aux float, sauf si l'économie de
mémoire est vraiment critique.
Voir à ce sujet la question 11.10.
11.2 Pourquoi mes extractions de racines carrées sont erronées ?
Assurez-vous d'avoir inclus math.h, d'avoir correctement
déclaré les autres fonctions renvoyant des double. Une
autre fonction de la bibliothèque standard avec laquelle il faut
faire attention est atof(), dans stdlib.h.
11.3 J'ai des erreurs de compilation avec des fonctions mathématiques
Il faut s'assurer d'avoir linké (lié) son code avec
la bibliothèque mathématique. Par exemple, sous Unix,
vous devez généralement passer l'option -lm à la fin de la
ligne de commande.
11.4 Mes calculs flottants me donnent des résultats étranges et/ou différents selon les plateformes
Pour commencer, relisez 11.1.
Si le problème est plus complexe, il convient de se rappeler que
les ordinateurs utilisent des formats de représentation des
flottants qui ne permettent pas des calculs exacts. Pertes de
précision, accumulation d'erreurs et autres anomalies sont le lot
commun du numéricien.
Rappelez-vous qu'aucun calcul sur des flottants n'a de chance
d'être exact, en particulier, n'utilisez jamais == entre
deux flottants.
Ces problèmes ne sont pas spécifiques au C.
Dans certains problèmes, une solution peut être d'introduire un
petit paramètre de relaxation, par exemple #define EPS
1e-10, puis de multiplier l'un des termes (judicieusement
choisi) de vos calculs par (1 + EPS).
Pour plus de renseignements, on se reportera par exemple aux
Numerical Recipes ou à Numerical Algorithms
with C (cf. 3.9).
11.5 Comment simuler == entre des flottants ?
Étant donné qu'il y a perte de précision très vite, pour comparer
deux valeurs flottantes, on teste si elles sont assez
proches. Plutôt que d'écrire une horreur du genre :
double a, b;
/* ... */
if (a == b) /* HORREUR ! */
/* ... */
on écrira :
#include <math.h>
/* ... */
double a, b;
/* ... */
if (fabs (a - b) <= epsilon * fabs (a) )
/* ... */
où l'on aura judicieusement choisi epsilon
(non-nul !).
11.6 Comment arrondir des flottants ?
La méthode la plus simple et la plus expéditive est (int)(x +
0.5). Cette technique ne fonctionne pas correctement pour
les nombres négatifs aussi vaut-il mieux utiliser
(int)(x < 0 ? x - 0.5 : x + 0.5)
11.7 Pourquoi le C ne dispose-t-il pas d'un opérateur d'exponentiation ?
Parce que certains processeurs ne disposent pas d'une telle
instruction. Il existe une fonction pow() déclarée dans
math.h bien que la multiplication soit préférable pour de
petits exposants.
11.8 Comment obtenir Pi ?
Parfois une constante prédéfinie M_PI est déclarée dans
math.h mais ce n'est pas standard aussi vaut-il mieux
calculer pi soi-même via 4 * atan
(1.0).
11.9 Qu'est-ce qu'un NaN ?
« NaN is Not a Number », ce qui signifie
« Ce n'est pas un nombre ».
Un NaN est un nombre flottant qui est le résultat d'une
opération non conforme, par exemple 0/0.
Lorsqu'un NaN est produit, la plupart des architectures
produisent une interruption (ou un signal) qui termine le
programme, au moment de l'utilisation de celui-ci.
Il est parfois possible de vérifier si un nombre est
NaN. Un bon test est celui-ci :
#define isNaN(x) ((x) != (x))
Certains compilateurs fournissent des facilités quant à la gestion
des NaN. GCC fournit dans la bibliothèque
mathématique (math.h) les fonctions isnan(),
isinf() et finite().
11.10 Faut-il préférer les double aux float ?
La vitesse de traitement d'un double n'est pas forcément plus
longue qu'un float, cela dépend du compilateur (de ses options)
et du processeur. Ainsi avec l'exemple suivant, en remplaçant le
typedef par float ou double, on s'aperçoit
que sur un Pentium ou un PowerPC, le double est plus rapide
à calculer que le float tout en ayant une précision plus grande.
#include <stdio.h>
#include <math.h>
typedef float reel; /* float ou double */
int main(void) {
long i ;
reel d = 3.0 ;
for (i = 0; i < 100000000; i++) {
d = cos(d) ;
}
(void)printf("%f\n", d);
return 0;
}
Le C comprend des instructions mathématiques pour traiter les float
directement au lieu de toujours passer par des double depuis la
dernière norme (C99). Par exemple il existe cosf() en plus
de cos(). En faisant des essais on s'aperçoit que dans
notre exemple, le cosf() appliqué à un float devient
aussi rapide que le cos() appliqué à un double.
En conclusion, nous pouvons dire qu'il est préférable d'utiliser des
double à la place des float, sauf lorsque la place
mémoire devient critique.
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 */
}
Traduit en HTML par faq2html.pl le Wed Nov 3 05:42:13 2010 pour le site Web Usenet-FR.