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

Guillaume Rumeau <guillaume.rumeau@wanadoo.fr>


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

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

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

     18 avril 2003
     Partie 2/4 (sections 5 à 9)
---------------------------------------------------------------------------


5. Déclarations et initialisations


5.1 Quels types utiliser ?


    Il existe en C plusieurs types de nombres entiers.
    Si vous avez à gérer de grands nombres, il faut utiliser le type
    long. Avec la norme C99 (cf. 3.7), un type long long est disponible.
    Pour des nombres de petites tailles, et si la place mémoire
    manque, c'est le type short qu'il faut prendre.

    
    Le type char peut parfois être utilisé comme très petit
    entier. Mais cela doit être évité au maximum. En effet, outre les
    problèmes de signe, le code généré peut être plus complexe et
    risque finalement de prendre plus de place et de faire perdre du
    temps. Même les short font parfois perdre du temps.

    
    Dans les autres cas, int est bien adapté.

    
    Le mot clé unsigned est à utiliser pour les nombres
    positifs, si vous avez des problèmes dus aux débordements ou pour
    les traitements par bits.

    
    Le choix entre float et double ne se pose pas.
    On devrait toujours utiliser double, sauf si on a
    vraiment des contraintes de mémoire (cf. 11.1 
    et 11.10).


5.2 Comment définir une structure qui pointe sur elle-même ?


    Il y a plusieurs façons correctes de le faire.
    Le problème est que dans la définition de la structure avec un
    typedef, le type n'est pas encore défini. Voici la
    manière la plus simple, où pour contourner le problème, on ajoute
    un tag à la structure.

    
    typedef struct node {
        char * item;
        struct node * next;
    } node_t;
    

    Une autre solution consiste à déclarer le type de la structure
    avant sa définition, avec un pointeur.

    
    typedef struct node * node_p;
    typedef struct node {
        char * item;
        node_p next;
    } node_t;
    

    Cette construction récursive est utilisée pour obtenir des listes
    chaînées ou des arborescences.


5.3 Comment déclarer une variable globale ?


    Sauf dans des cas précis, vous devriez éviter d'utiliser des
    variables globales.

    
    Si vous y tenez vraiment, la meilleure solution est de déclarer la
    variable dans un fichier xxx.c, et de mettre la
    déclaration extern dans un xxx.h associé. Ceci
    évitera des redéfinitions de la variable lors des inclusions
    d'en-têtes.

    
    Le mot clé static permet d'avoir des variables locales
    persistantes, ce qui peut être une bonne alternative aux
    globales.


5.4 Quelle est la différence entre const et #define ?


    Cela n'a rien à voir.
    Le #define permet de définir une macro, alors que le mot
    clé const indique que l'objet qualifié est protégé en
    écriture.

    
    Lors de la compilation, la première phase est effectuée par le
    pré-processeur qui remplace les macros par leurs valeurs. C'est un
    simple copier/coller.
    Une variable déclarée const reste quant à elle une
    variable, mais on ne peut lui affecter une valeur que lors de
    l'initialisation. Après, elle est n'est plus modifiable.

    
    Le mot-clé const permet aussi au compilateur d'effectuer
    des optimisations en plaçant par exemple la variable dans un
    registre.

    
    Il est à noter qu'avec la nouvelle norme C99, il est possible de
    déclarer un tableau dont la taille est donnée par une variable
    const. Auparavant, il fallait utiliser une macro.

    
    Voir aussi les questions 13.7 et 5.5.


5.5 Comment utiliser const avec des pointeurs ?


    Le mot-clé const permet de protéger une variable de
    modifications ultérieures. Une variable constante n'est modifiable
    qu'une fois, lors de l'initialisation.

    
    Un pointeur étant une variable comme les autres, const
    s'utilise de la même façon.
    Voici un exemple :

    
    const char * sz1;
    char const * sz2;
    char * const sz3;
    char const * const sz4;
    

    Les variables sz1 et sz2 sont des pointeurs sur
    objet constant de type char. La variable sz3 est
    un pointeur constant sur un objet (non constant) de type
    char. Enfin, sz4 est un pointeur constant sur un
    objet constant de type char.

    
    Un petit « amusement » pour terminer, que signifie la déclaration
    const char * (* f)(char * const * s); ?

    
    Voir aussi la question 5.4.


5.6 Comment bien initialiser ses variables ?


    Les variables globales, ou déclarées static, sont
    initialisées automatiquement lors de leur définition.
    Si aucune valeur n'est spécifiée, c'est un zéro qui est pris
    (suivant le type de la variable, 0, 0.0 ou
    NULL).

    
    Ce n'est pas le cas pour les variables automatiques (les autres).
    Il est donc nécessaire de le faire « à la main ».

    
    Les variables allouées dynamiquement avec malloc() ne
    le sont pas non plus.
    On pourra utiliser calloc() qui initialise les
    variables allouées. Il est à noter que calloc() met les
    bits à 0 comme le ferait memset(). Cette
    initialisation est valide pour les types entiers (char,
    short, int et long), mais non portable
    pour les pointeurs et les flottants.

    
    Une bonne méthode qui initialise correctement les variables
    suivant leur type est celle-ci :

    
    {
        type a[10]={0};
        struct s x ={0};
    }
    

    Avec la variante dynamique :

    
    [static] const struct s x0 ={0};
    {
        struct s *px =malloc(sizeof *px);
        *px=x0;
    }
    

5.7 Comment déclarer un tableau de fonctions ?


    Deux cas se présentent. Si toutes les fonctions ont le même
    prototype, il suffit de faire ainsi :

    
    extern char * f(int, int); /* une fonction                     */
    char * (*fp[N])(int, int); /* Un tableau de N fonctions        */
    char * sz;
    fp[4] = f;                 /* affectation de f dans le tableau */
    sz = fp[4](42, 12);        /* utilisation                      */
    

    Si les fonctions ont des prototypes différents, il faut déclarer
    un tableau de fonctions génériques. Les fonctions génériques
    n'existent pas à proprement parler. Il faut utiliser une fonction
    sans argument spécifié et retournant un int.

    
    int (*fp[N])(); /* Un tableau de N fonctions quelconques*/
    

    Cela « capte » la plupart des cas, sauf les fonctions
    au nombre d'arguments variables, comme printf().

    
    Voir aussi la question 7.4.


5.8 Comment connaître le nombre d'éléments d'un tableau ?


    On peut utiliser une macro de ce type là :
    
    #define NELEMS(n) (sizeof(n) / sizeof *(n))
          

5.9 Quelle est la différence entre char a[] et char * a?


    Il faut bien se rappeler qu'en C, un tableau n'est pas un
    pointeur, même si à l'usage ça y ressemble beaucoup.

    
    char a[] déclare un tableau de char de taille
    inconnue (type incomplet). Dès l'initialisation, par

    
	char a[] = "Hello";
    

    a se transforme en char[6], soit un tableau de
    six char (type complet).

    
    Il arrive souvent de voir la confusion entre « tableau » et «
    pointeur constant » (moi même je la fais parfois ;-) )

    
    Ce « pointeur constant » est inspiré par K&R, 1ère
    édition. C'était une métaphore malheureuse de K&R qui voulait
    exprimer qu'un tableau se comporte en général comme
    une « valeur (rvalue) du type pointeur », et bien entendu une
    valeur est toujours constante. Par cette métaphore ils ont essayé
    d'expliquer pourquoi on ne pouvait pas prendre l'adresse d'un
    tableau.

    
    Malheureusement ceci n'explique pas pourquoi sizeof a
    donne la taille du tableau et non la taille d'un pointeur.

    
    Sur ce sujet, la norme est plus précise en disant qu'un tableau
    dans une expression --- sauf dans &a ou
    sizeof a ou dans des initialisations par des chaînes
    littérales "xyz" --- est converti automatiquement en
    une valeur du type pointeur qui pointe sur l'élément initial du
    tableau (merci à Horst Kraemer pour ces précisions).

    
    Voir aussi la question 7.1.


5.10 Peut-on déclarer un type sans spécifier sa structure ?


    Plus précisément, est-il possible de définir un type de données
    dont on veut cacher à l'utilisateur de la bibliothèque
    l'implémentation ?

    
    Oui c'est possible, en encapsulant ce type dans une structure.
    Il y a au moins deux méthodes. La première est de définir une
    structure (publique) qui contient un pointeur void *, qui
    pointe vers une variable du type privé. Ce type privé est défini
    avec les fonctions, dans un .c.
    Il faut alors prévoir des fonctions d'initialisation et de
    destruction des objets.

    
    Une autre solution consiste à déclarer une structure qui contient
    une donnée du type à protéger, et à accéder aux types par des
    pointeurs uniquement.
    Voici un exemple :

    
    /* data.h */
    struct Data_s ;
    typedef struct Data_s Data_t ;

    extern Data_t * DataNew(int x, int y);
    extern int DataFonction(Data_t * this);

    /* data.c */
    #include <stdio.h>
    #include <stdlib.h>
    #include "data.h"

    struct Data_s {
        int x;
        int y;
    };

    Data_t * DataNew(int x , int y) {
        Data_t * pData;

        pData = malloc(sizeof *pData) ;
        if (pData) {
            pData->x = x;
            pData->y = y;
        }
        return pData;
    }

    int DataFonction(Data_t * this) {
        if (!this)
            return 0;
        return (this->x * this->x) + (this->y * this->y);
    }

    /* main.c */
    #include <stdio.h>
    #include "data.h"

    int main(void) {
        Data_t *psData;

        psData = DataNew(3, 4);
        printf("%d\n", DataFonction(psData));

        return 0;
    }
    

    (Merci à Yves Roman pour cet exemple.)


6. Structures, unions, énumérations


6.1 Quelle est la différence entre struct et typedef struct ?


    struct x1 { ... };
    typedef struct { ... } x2;
    

    La première écriture déclare un tag de structure
    x1. La deuxième déclare un nouveau type nommé
    x2.

    
    La principale différence est l'utilisation.
    La deuxième écriture permet un peu plus l'abstraction de type.
    Cela permet de cacher le véritable type derrière x2,
    l'utilisateur n'étant pas sensé savoir que c'est une structure.

    
    struct x1 v1;
    x2 v2;
    

6.2 Une structure peut-elle contenir un pointeur sur elle-même ?


    Oui.

    
    Voir aussi la question 5.2.


6.3 Comment implémenter des types cachés (abstraits) en C ?


    L'une des bonnes façons est d'utiliser des pointeurs sur des
    structures qui ne sont pas publiquement définies. On peut en plus
    cacher les pointeurs par des typedef.

    
    Voir aussi la question 5.10.


6.4 Peut-on passer des structures en paramètre de fonctions ?


    Oui, c'est parfaitement autorisé. Toutefois, rappelons que les
    paramètres en C sont passés par valeurs et copiés dans la pile.
    Pour des grosses structures, il est préférable de passer un
    pointeur dessus, et éventuellement un pointeur constant.


6.5 Comment comparer deux structures ?


    Il n'existe pas en C d'opérateur ou de fonction
    pour comparer deux structures. Il faut donc le faire à la main,
    champs par champs.

    
    Une comparaison bit à bit n'est pas portable, et risque de ne pas
    marcher, en raison du padding (alignement sur
    certains octets).


6.6 Comment lire/écrire des structures dans des fichiers ?


    Il faut utiliser les fonctions fread() et
    fwrite(). Attention : les fichiers obtenus ne sont pas
    portables.

    
    Une méthode plus portable consiste à enregistrer les structures
    dans un fichier texte.


6.7 Peut-on initialiser une union ?


    La norme prévoit d'initialiser le premier membre d'une union.
    Pour le reste, ce n'est pas standard.
    En C99, on peut initialiser les champs d'une union :

    
    union { /* ... */ } u = { .any_member = 42 };
    

6.8 Quelle est la différence entre une énumération et des #define ?


    Il y a peu de différences.

    
    L'un des avantages de l'énumération est que les valeurs numériques
    sont assignées automatiquement.
    De plus, une énumération se manipule comme un type de données.
    Certains programmeurs reprochent aux énumérations de réduire le
    contrôle qu'ils ont sur la taille des variables de type énumération.


6.9 Comment récupérer le nombre d'éléments d'une énumération ?


    Ceci n'est possible de façon automatique que si les valeurs se
    suivent.

    
    typedef enum { A, B, C, D} type_e;
    

    Dans cette énumération, les valeurs sont données par le
    compilateur dans l'ordre croissant, à partir de 0 et avec un pas de
    1. Ainsi, le nombre d'éléments de type_e est D + 1.

    
    On peut rajouter un élément à l'énumération qui donne directement
    le nombre d'éléments :

    
    typedef enum { RED, BLUE, GREEN, YELLOW, NB_COLOR} color_e;
    

    le nombre d'éléments de color_e est donc NB_COLOR.

    
    Si l'on est obligé, pour des raisons diverses et variées,
    de fixer d'autres valeurs aux constantes, alors cette solution ne
    marche pas. On peut toujours rajouter un champs dans l'énumération
    et fixer manuellement sa valeur.


6.10 Comment imprimer les valeurs symboliques d'une énumération ?


    On ne peut pas le faire simplement. Il faut écrire une fonction
    qui le fait. Un problème qui se pose alors est la maintenance, car
    une modification des valeurs de l'énumération entraîne la
    nécessité d'une mise à jour de cette fonction.

    
    Voici un code qui limite les problèmes de mise à jour :

    
    /* Fichier foo.itm */
    ITEM(FOO_A)
    ITEM(FOO_B)
    ITEM(FOO_C)
    /**/

    /* Fichier foo.h */
    #ifndef FOO_H
    #define FOO_H
    #define ITEM(a) a,
    typedef enum {
    #include "foo.itm"
        FOO_NB
    } foo_t;
    #undef ITEM

    #define ITEM(a) #a,
    const char * const aFoo[] = {
    #include "foo.itm"
    };
    #undef ITEM
    #endif
    /**/

    /* Fichier foo.c */
    #include <stdio.h>
    #include "foo.h"
    int main(void) {
        foo_t foo;
        for (foo = FOO_A; foo < FOO_NB; foo++) {
            printf("foo=%d ('%s')\n",foo, aFoo[foo]);
        }

        return 0;
    }
    

    Merci à Emmanuel Delahaye pour cet exemple.


7. Tableaux et pointeurs


7.1 Quelle est la différence entre un tableau et un pointeur ?


    Un tableau n'est pas un pointeur.
    Un tableau est une zone mémoire pouvant contenir N
    éléments consécutifs de même type.
    Un pointeur est une zone mémoire qui contient l'adresse d'une
    autre zone mémoire. Toutefois, dans un grand nombre de cas, tout
    se passe comme si c'était la même chose.

    
    À ce titre, il faut bien faire la différence entre a[i]
    pour un tableau et ap[i] pour un pointeur.
    Voici un exemple :

    
    char a[] = "Bonjour";
    char *ap = "Au revoir";
    

    L'expression a[3] signifie que l'on accède aux quatrième
    élément du tableau. ap[3] signifie que l'on accède à la
    zone mémoire pointée par (ap+3).
    Autrement dit, a[3] est l'objet situé 3 places après
    a[0] (a est le tableau entier),
    alors que ap[3] est l'objet situé 3 places après l'objet
    pointé par ap. Dans l'exemple, a[3] vaut
    'j' et ap[3] vaut 'r'.

    
    Voir aussi la question 5.9.


7.2 Comment passer un tableau à plusieurs dimensions en paramètre d'une fonction ?


    Ce n'est pas si facile.
    La règle de base est qu'il faut connaître la taille des
    N-1 dernières dimensions. Pour un tableau à deux
    dimensions, la deuxième doit être connue, et la fonction doit être
    déclarée ainsi :

    
    int f1(int a[][NCOLUMNS]);
                /* a est un tableau a deux dimensions (cf. remarque) */
    int f2(int (*ap)[NCOLUMNS]);
                /* ap est un pointeur sur un tableau  */
    

    Si elle n'est pas connue, il faut passer la taille du tableau en
    paramètre (ligne ET colonne) et un pointeur sur le tableau :

    
    int f(int * a, int nrows, int ncolumns);
    

    On accède aux éléments du tableau ainsi :

    
    a[i * ncolumns + j] /* element de la ieme ligne
                         * et de la jeme colonne */
    

    Une remarque :
    Dans une déclaration de paramètre

    
	int f1(int a[][NCOLUMNS])
    
    ou
    
	int f1(int a[42][NCOLUMNS])
    

    a est un pointeur sur int[NCOLUMS] malgré
    l'écriture. La déclaration est interprétée comme

    
    int (*a) [NCOLUMS]
    

    Ainsi les déclarations de f1() et f2() dans
    l'exemple initial sont exactement les mêmes.


7.3 Comment allouer un tableau à plusieurs dimensions ?


    La première solution est d'allouer un tableau de pointeurs, puis
    d'initialiser chacun de ces pointeurs par un tableau dynamique.

    
    #include <stdlib.h>

    int ** a = malloc(nrows * sizeof *a );
    for(i = 0; i < nrows; i++)
       a[i] = malloc(ncolumns * sizeof *(a[i]));
    

    Dans la vraie vie, le retour de malloc() doit être
    vérifié.

    
    Une autre solution est de simuler un tableau multi-dimensions
    avec une seule allocation :

    
    int *a = malloc(nrows * ncolumns * sizeof *a);
    

    L'accès aux éléments se fait par :

    
    a[i * ncolumns + j] /* element de la ieme ligne
	                      * et de la jeme colonne */
    

7.4 Comment définir un type pointeur de fonction ?


    On utilise typedef, comme pour n'importe quel autre
    type. Voici un exemple :

    
    int f(char * sz); /* une fonction                 */
    int (*pf)(char *);/* un pointeur sur une fonction */
    typedef int (*pf_t)(char *);
                      /* un type pointeur sur fonction*/
    

    Il est toutefois préférable de ne pas cacher le pointeur dans un
    typedef. La solution suivante est plus jolie :

    
    typedef int (f_t)(char *); /* un type fonction        */
    f_t * pf;                  /* un pointeur sur ce type */
    

    On l'utilise alors de cette façon :

    
    pf = f;
    int ret = pf("Merci pour cette reponse");
    

    Voir aussi la question 5.7.


7.5 Que vaut (et signifie) la macro NULL ?


    NULL est une macro qui représente une valeur spéciale
    pour désigner un pointeur nul lorsque converti au type approprié.
    Elle est définie dans <stddef.h> ou dans
    <stdio.h>.

    
    La valeur réelle de NULL est dépendante de
    l'implémentation, et n'est pas nécessairement un pointeur, ni de
    type pointeur. Des valeurs possibles sont ((void *)0) ou
    0.

    
    NULL permet de distinguer les pointeurs valides des
    pointeurs invalides. Par exemple, malloc() renvoie une
    valeur comparable à NULL quand elle échoue.

    
    Voir aussi la question 12.3.


7.6 Que signifie l'erreur « NULL-pointer assignment » ?


    Cela signifie que vous avez essayé d'accéder à l'adresse 0 de la
    mémoire. Vous avez probablement déréférencé un pointeur
    NULL, ou oublié de tester la valeur retour d'une
    fonction, avant de l'utiliser.


7.7 Comment imprimer un pointeur ?


    La seule manière prévue par la norme pour imprimer correctement un
    pointeur est d'utiliser la fonction printf() avec le
    code de format %p.
    Le pointeur doit être d'abord casté en un pointeur
    générique void *.

    
    char * p;
    printf("Pointeur p avant initialisation: %p\n",
                                             (void *)p);
    

7.8 Quelle est la différence entre void * et char * ?


    Le premier est un pointeur générique, qui peut recevoir l'adresse
    de n'importe quel type d'objet.
    Le second est un pointeur sur un caractère, généralement utilisé
    pour les chaînes.

    
    Avant la norme ANSI, le type void n'existait pas. C'était
    donc char * qui était utilisé pour faire des pointeurs
    génériques. Depuis la norme, ce n'est plus valide.
    De nombreux programmeurs ont toutefois gardé cette habitude,
    notamment dans le cast de fonctions comme
    malloc() (cf. 12.1).


8. Chaînes de caractères


8.1 Comment comparer deux chaînes ?


    Pour comparer deux chaînes entre elles, il faut utiliser la
    fonction strcmp(), et non l'opérateur
    ==. Celui-ci comparera les pointeurs entre eux, ce qui
    n'est probablement pas ce qui est voulu !

    
    const char * sz = "non";
    if(strcmp(sz, "oui") == 0) {
        /* sz et "oui" sont egaux */
    }
    

    Il existe aussi la fonction strncmp() qui permet de
    contrôler la longueur de comparaison.


8.2 Comment recopier une chaîne dans une autre ?


    Il faut utiliser la fonction strcpy(),
    et non l'opérateur d'affectation =.
    Il faut s'assurer que l'on dispose d'espace suffisant dans la
    chaîne cible avant d'utiliser strcpy(), qui ne fait
    aucun contrôle de débordement.

    
    Pour une copie plus sécurisée, on préférera la fonction
    strncpy().


8.3 Comment lire une chaîne au clavier ?


    Il y a de nombreuses fonctions qui le font.
    Le plus sûr est d'utiliser fgets().

    
    char tab[20];
    fgets(tab, sizeof tab, stdin);
    

    Voici un exemple complet d'utilisation propre de
    fgets() pour la lecture d'une ligne, avec un contrôle
    d'erreurs. Cette fonction est fournie par Emmanuel Delahaye.

    
    #include <stdio.h>
    #include <string.h>
    
    int get_line(char *buf, size_t size) {
       int ret; /* 0=Ok 1=Err 2=Incomplet  
                 * On peut aussi definir des constantes. (Macros, enum...)
                 */
    
       if (fgets(buf, size, stdin) != NULL) {
          char *p = strchr(buf, '\n'); /* search ... */
          if (p != NULL) {
             *p = 0; /* ... and kill */
             ret = 0;
          }
          else {
             ret = 2;
          }
       }
       else {
          ret = 1;
       }
       return ret;
    }
    
    
    Il peut être intéressant dans certains cas de faire un traitement
    particulier dans le cas 2 (lecture incomplète), comme vider le
    buffer du flux ou redimentionner la zone de réception (voir la question 
    14.5).

    
    La fonction gets() est à proscrire, car il n'y a aucun
    contrôle de débordement, ce qui peut engendrer de nombreux bugs
    (stack overflow) (cf. 8.7).


8.4 Comment obtenir la valeur numérique d'un char (et vice-versa) ?


    En C, un char est un petit entier. Il n'y a donc aucune
    conversion à faire. Quand on a un char, on a aussi sa
    valeur, et vice-versa.


8.5 Que vaut sizeof(char) ?


    Un char vaut et vaudra toujours 1 indépendamment de
    l'implémentation. En effet, les tailles d'allocation en C se
    calculent en char (size_t). Or, un char
    a une taille de 1 char. Donc
    
    sizeof(char) == (size_t) 1
    

    Pour autant, un char ne fait pas forcément 8 bits (un
    octet) (voir aussi 16.2).


8.6 Pourquoi sizeof('a') ne vaut pas 1 ?


    En C, les caractères constants sont des int, et non des
    char. Ainsi,
    
    sizeof('a') == sizeof(int)
    
    ce qui peut valoir 2 ou 4 sur votre machine, ou autre chose.


8.7 Pourquoi ne doit-on jamais utiliser gets() ?


    Serge Paccalin donne l'exemple suivant :

    
    #include <stdio.h>

    int main(void)
    {
       char chaine[16];

       printf("Tapez votre nom :\n");
       gets(chaine);
       printf("Vous vous appelez %s.\n",chaine);
       return 0;
    }
    

    Quand on tape une chaîne de plus de 15 caractères, rien ne va plus :
    gets() accepte sans broncher la chaîne mais lorsqu'il
    est question de la stocker dans
    
    char chaine[16]
    

    on peut obtenir un magnifique
    Segmentation fault (core dumped).
    Dans certains cas, avec certains compilateurs
    sur certaines machines et certains OS, il est possible que ça
    passe. Mais attention ! C'est un leurre, et le changement de cible
    démontrera la malfaçon.


8.8 Pourquoi ne doit-on presque jamais utiliser scanf() ?


    scanf() est une fonction de la bibliothèque standard, qui est 
    souvent la première que l'on apprend pour lire des données au clavier.
    Cette fonction n'est pas plus dangereuse qu'une autre, à condition de 
    bien savoir l'utiliser, ce qui n'est pas donné à tout le monde.

    
    Par exemple, regardez le programme suivant donné par Serge
    Paccalin : 

    
    #include <stdio.h>

    int main(void) {
       int val = 0;

       while (val != 1){
          printf("Tapez un nombre"
                 "(1 pour arreter le programme) :\n");
          scanf(" %d",&val);
          printf("Vous avez saisi %d.\n",val);
       }
       return 0;
    }
    

    Et il explique : « Quand le programme demande un nombre, taper
    "toto" suivi de la touche
    Entrée. Le programme part en boucle parce que tous les
    scanf() successifs butent sur "toto" qui reste
    indéfiniment dans stdin. »

    
    Dans la plupart des cas, l'utilisation de la fonction fgets()
    sera plus simple et moins risquée.

    
    scanf() est une fonction qui peut être utilisée dans
    certaines conditions, et à condition de bien savoir ce que l'on
    fait. L'utilisation de scanf() pour la lecture de
    nombres (entiers ou flottants) est l'une des plus acceptable. 
    Par contre, la lecture d'une chaîne avec scanf() sans 
    contrôle de format est aussi dangereux que gets().

    
    scanf("%s", astring) ;
    

    est donc a proscrire.

    
    scanf() n'est préférable à fgets() que dans
    le cas ou l'on veut lire mot par mot et non ligne par ligne,
    et conserver le reste dans le buffer du flux
    d'entrée. 
    Sinon, lire un mot avec fgets() ne pose pas de problème
    et est même plus simple. 

    
    scanf("%4s", astring) ;
    
    
    Cette construction, par exemple, ne pose pas les problèmes cités
    plus haut (si le contrôle d'erreurs est fait), et est parfois
    utile.

    
    Voir aussi les questions 8.7 et 8.3


9. Fonctions et prototypes


9.1 Pour commencer ...


    Il y a trois notions :
    
     -  déclaration,
     -  définition,
     -  prototype.
    

    La déclaration d'une fonction, c'est annoncer que tel
    identificateur correspond à une fonction, qui renvoie tel type. La
    définition d'une fonction est une déclaration où, en plus, on
    donne le code de la fonction elle-même. Le prototype est une
    déclaration de fonction où le type des arguments est également
    donné.

    
    Par exemple :
    
    int f();    /* declaration de f(), renvoyant un int, pas de prototype  */

    int f(void);/* declaration de f(), renvoyant un int, prototype (0 arg) */

    int f(void) /* definition de f() avec declaration avec prototype       */
    {
        return 42;
    }

    int f(x)    /* definition de f() avec declaration sans prototype       */
    int x;
    {
        return x;
    }

    int f()     /* definition de f() avec declaration sans prototype       */
    {
        return 42;
    }
    

    Ce qui n'est pas possible :
    
        -  avoir une définition sans déclaration,
        -  avoir un prototype sans déclaration.
    

    Ce qui est autorisé :
    
     -  appeler une fonction déclarée, qu'elle ait un prototype ou
    pas.
    

    Ce qui était autorisé en C90 mais ne l'est plus en C99 :
    
     -  appeler une fonction non déclarée ; l'appel valait
       déclaration dite « implicite », sans prototype et avec type de
       retour int.
    

    Ce qui est encore autorisé en C99 mais disparaîtra bientôt :
    
     -  déclarer/définir une fonction sans prototype.
    

    Ce qu'il faut faire quand on veut programmer lisiblement, en
    détectant les bugs et en gardant du code maintenable et compatible
    avec le futur :
    
     -  utiliser des déclarations et définitions avec prototype.
    

9.2 Qu'est-ce qu'un prototype ?


    Un prototype est une signature de fonction.
    Comme tout objet en C, une fonction doit être déclarée avant son
    utilisation. Cette déclaration est le prototype de la fonction.
    Le prototype doit indiquer au compilateur le nom de la fonction,
    le type de la valeur de retour et le type des paramètres (sauf
    pour les fonctions à arguments variables, comme printf().
    (cf. 9.6).

    
    int fa(int a, char const * const b);
    int fb(int, char const * const);
    

    Les noms de paramètre sont optionnels, mais il est fortement
    conseillé de les laisser. Cela donne une bonne indication sur
    leurs rôles.

    
    Les fonctions de la bibliothèque ont également leur prototype.
    Avant l'utilisation de celles-ci, il faut inclure les fichiers
    d'en-tête contenant les prototypes.
    Par exemple, le prototype de malloc() se trouve dans
    stdlib.h.

    
    Certains préfèrent ajouter le mot clé extern au
    prototype, afin de rester cohérent avec la déclaration des
    variables globales.

    
    Voir aussi les questions 9.3,
    12.1, 13.5 et
    14.17.


9.3 Où déclarer les prototypes ?


    Un prototype de fonction doit être déclaré avant l'utilisation de
    la fonction. Pour une plus grande lisibilité, mais aussi pour
    simplifier la maintenance du code, il est conseillé de regrouper
    tous les prototypes
    d'un module (fichier xxx.c) dans un en-tête 
    (<xxx.h>). Ce dernier n'a plus alors qu'à être inclus 
    dans le code qui utilise ces fonctions. C'est le cas des fonctions de
    la bibliothèque standard.

    
    Voir aussi les questions 13.5 et 
    9.10.


9.4 Quels sont les prototypes valides de main() ?


    La fonction main() renvoie toujours un int.
    Les prototypes valides sont :

    
    int main(void);
    int main(int argc, char * argv[]);
    

    Tout autre prototype n'est pas du tout portable et ne doit jamais
    être utilisé (même s'il est accepté par votre compilateur).

    
    En particulier, vous ne devez pas terminer la fontion
    main() sans retourner une valeur positive (non nulle en
    cas d'erreur).
    Les valeurs de retour peuvent être 0,
    EXIT_SUCCESS ou EXIT_FAILURE.

    
    On pourra aussi rencontrer (sous Unix) le
    prototype suivant :

    
    int main (int argc, char* argv[], char** arge);
    

    dans le but d'utiliser les variables d'environnement du
    shell actif. Ce n'est ni portable ni standard,
    d'autant plus que les fonctions getenv(),
    setenv() et putenv() le sont et suffisent
    largement.

    
    Enfin, rappelons que le prototype suivant
    
    int main () ;
    
    est parfaitement valide en C++ (et est synonyme du premier
    présenté ici), mais ne l'est pas en C.


9.5 Comment printf() peut recevoir différents types d'arguments ?


    printf() est une fonction à nombre variable de
    paramètres. Son prototype est le suivant :

    
    int printf(const char * format, ...); /* C 90 */
    int printf(const char * restrict format, ...); /* C 99 */
    

    Le type et le nombre des paramètres n'est pas défini dans le
    prototype, c'est le traitement effectué dans la fonction qui doit
    les vérifier.

    
    Pour utiliser cette fonction, il est donc impératif d'inclure
    l'en-tête <stdio.h>.

    
    Pour écrire une fonction de ce type, lire la question suivante
    (9.6).


9.6 Comment écrire une fonction à un nombre variable de paramètres ?


    La bibliothèque standard fournit des outils pour faciliter la
    gestion de ce type de fonctions.
    On les trouve dans l'en-tête <stdarg.h>.

    
    Le prototype d'une fonction à nombre variable de paramètres doit
    contenir au moins un paramètre explicite, puis se termine par ...
    Exemple :

    
    int f(int nombre, ...);
    

    Il faut, d'une façon ou d'une autre, passer dans les paramètres le
    nombre d'arguments réellement transmis.
    On peut le faire en donnant ce nombre explicitement (comme
    printf()), ou passer la valeur NULL en dernier.

    
    Attention toutefois avec la valeur NULL dans ce cas.
    En effet, NULL n'est pas nécessairement une valeur du
    type pointeur mais une valeur qui donne un pointeur
    nul si elle est affectée ou passée ou comparée à un type
    pointeur. Le passage d'une valeur à un paramètre n'est pas une
    affectation à un pointeur mais une affectation qui obéit aux lois
    spéciales pour les paramètres à nombre variable (ou pour les
    paramètres d'une fonction sans prototype). Les lois de promotion
    pour les types arithmétiques sont appliquées). Si NULL est
    défini par
    #define NULL 0
    alors (int)0 est passé à la fonction. Si un pointeur n'a
    pas la même taille qu'un int ou si un pointeur nul n'est
    pas représenté par « tous les bits 0 » le passage d'un 0 ne passe
    donc pas de pointeur nul. La méthode portable est
    donc

    
    f(toto,titi,(void*)NULL);
    

    ou

    
	f(toto,titi,(void*)0);
    

    C'est le seul cas où il faut caster NULL parce qu'il ne s'agit pas
    d'un contexte syntactique « de pointeur », seulement d'un contexte
    « de pointeur par contrat ».

    
    Après cela, les fonctions va_start(),
    va_arg() et va_end() permettent de parcourir
    la liste des paramètres.

    
    Voici un petit exemple :
    
    #include <stdarg.h>

    int vexemple(int nombre, ...){
        va_list argp;
        int i;
        int total = O;

        if(nombre < 1)
                return 0;

        va_start(argp, nombre);
        for (i = 0; i < nombre; i++) {
            total += va_arg(argp, int);
        }
        va_end(argp);

        return total;
    }
    

    Merci à Horst Kraemer pour ces remarques.


9.7 Comment modifier la valeur des paramètres d'une fonction ?


    En C, les paramètres sont passés par valeur. Dans la plupart des
    implémentations, cela se fait par une copie dans la pile.
    Lors du retour de la fonction, ces valeurs sont simplement
    dépilées, et les modifications éventuelles sont perdues.
    Pour pallier cela, il faut simuler un passage des paramètres par
    référence, en passant un pointeur sur les variables à modifier.
    Voici l'exemple classique de l'échange des valeurs entre deux
    entiers :

    
    void echange(int * a, int * b) {
        int tmp = *a;
        *a = *b;
        *b = tmp;
    }
    

9.8 Comment retourner plusieurs valeurs ?


    Le langage C ne permet pas aux fonctions de renvoyer plusieurs
    objets. Une solution consiste à passer l'adresse des objets à
    modifier en paramètre.
    Une autre solution consiste à renvoyer une structure, ou un
    pointeur sur une structure qui contient l'ensemble des valeurs.
    Généralement, quand on a ce genre de choses à faire, c'est qu'il
    se cache une structure de données que l'on n'a pas identifiée.
    La pire des solutions est d'utiliser des variables globales.


9.9 Peut-on, en C, imbriquer des fonctions ?

    Non, on ne peut pas.
    Les concepteurs ont jugé cela trop compliqué à mettre en oeuvre
    (portée des variables, gestion de la pile etc.).
    Certaines implémentations, comme GNU CC le supportent
    toutefois. Ceci dit, on peut très bien s'en passer, en utilisant
    des pointeurs sur les structures de données à partager, ou en
    utilisant des pointeurs de fonctions.


9.10 Qu'est-ce qu'un en-tête ?
.

    Un en-tête est un ensemble de déclarations, définitions et prototypes 
    nécessaires pour compiler et pour utiliser un module.
    Par exemple, pour utiliser les fonctions d'entrées/sorties de la 
    bibliothèque standard, il est nécessaire d'inclure dans son programme
    l'en-tête <stdio.h>.

     
    Par abus, on parle souvent de fichier d'en-tête, car historiquement, 
    et encore aujourd'hui pour de nombreuses implémentations, ces en-têtes
    sont des fichiers. C'est également le cas pour les en-têtes personnels.
    Toutefois, la norme n'exige pas que les en-têtes standards soient des 
    fichiers à proprement parlé.


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.