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


Valid XHTML 1.0! [Retour au sommaire] Valid CSS!

Traduit en HTML par faq2html.pl le Wed Nov 3 05:42:13 2010 pour le site Web Usenet-FR.