Skip to content
Tags

Erreurs de programmation en C courantes

15 décembre 2013

Cet article est une traduction du texte Common C Programming Errors par Dr Paul Carter.
Dr Carter m’a gentiment autorisé à faire cette traduction et la publier ici ; qu’il en soit remercié.
Merci de me signaler toute erreur ou remarque dans les commentaires.


Table des matières

1. Introduction

2. Erreurs de débutants
2.1 Oublier de mettre un break dans une instruction switch
2.2 Utiliser = au lieu de ==
2.3 Erreurs de scanf()
2.3.1 Oublier de mettre un & à un argument
2.3.2 Utiliser le mauvais format pour l’opérande
2.4 Taille des tableaux
2.5 Division entière
2.6 Erreurs de boucles
2.7 Ne pas utiliser les prototypes
2.8 Ne pas initialiser les pointeurs

3. Erreurs de chaînes de caractères
3.1 Confondre les constantes caractère et chaîne de caractères
3.2 Comparer des chaînes avec ==
3.3 Chaînes sans zéro terminal
3.4 Ne pas laisser la place pour le zéro terminal

4. Erreurs d’Entrées/Sorties
4.1 Utilisation incorrecte de fgetc(), etc.
4.2 Utilisation incorrecte de feof()
4.3 Laisser des caractères dans le tampon d’entrée
4.4 Utiliser la fonction gets()

5. Remerciements


1. Introduction

Ce document dresse une liste des erreurs de programmation en C courantes que l’auteur voit maintes et maintes fois. Des solutions à ces erreurs y sont aussi présentées.

Une autre excellente ressource est la C FAQ (NdT : ou sa version française). Gimpel Software a aussi une liste de bugs difficiles à détecter en C/C++ qui peut être utile.

2. Erreurs de débutants

Ce sont des erreurs que les étudiants débutant en C font souvent. Toutefois, les professionnels les font encore parfois eux aussi !

2.1 Oublier de mettre un break dans une instruction switch

Rappelez-vous que le C ne sort pas d’une instruction switch quand un case est rencontré. Par exemple :

    int x = 2;
    switch(x) {
    case 2:
      printf("Deux\n");
    case 3:
      printf("Trois\n");
    }

affiche en sortie :

    Deux
    Trois

Mettez un break pour sortir du switch :

    int x = 2;
    switch(x) {
    case 2:
      printf("Deux\n");
      break;
    case 3:
      printf("Trois\n");
      break;   /* pas nécessaire, mais utile si des case supplémentaires sont ajoutés plus tard */
    }

2.2 Utiliser = au lieu de ==

L’opérateur = du C est utilisé exclusivement pour l’affectation et renvoie la valeur affectée. L’opérateur == est utilisé exclusivement pour la comparaison et renvoie une valeur entière (0 pour faux, 1 pour vrai). À cause de ces valeurs de retour, le compilateur C ne signale souvent pas d’erreur quand = est utilisé alors que l’on voulait en réalité un ==. Par exemple :

    int x = 5;
    if ( x = 6 )
      printf("x égale 6\n");

Ce code affiche en sortie x égale 6 ! Pourquoi ? L’affectation dans le if met 6 dans x et renvoie la valeur 6 au if. Puisque 6 n’est pas 0, cela est interprété comme vrai.

Une façon de faire en sorte que le compilateur trouve ce genre d’erreurs est de mettre toute constante (ou toute expression r-value) du côté gauche. Alors, si un = est utilisé, ce sera une erreur :

if ( 6 = x)

2.3 Erreurs de scanf()

Il y a deux types d’erreurs de scanf() courantes :

2.3.1 Oublier de mettre un & à un argument

scanf() doit avoir l’adresse de la variable pour stocker l’entrée dedans. Cela veut dire que souvent l’opérateur d’adresse & (« et commercial » ou « esperluette ») est nécessaire pour calculer les adresses. Voici un exemple :

    int x;
    char * st = malloc(31);

    scanf("%d", &x);   /* & nécessaire pour passer l'adresse à scanf() */
    scanf("%30s", st); /* PAS DE & ici, st pointe lui-même sur la variable ! */

La dernière ligne ci-dessus montre que parfois il est correct de ne pas mettre de & !

2.3.2 Utiliser le mauvais format pour l’opérande

Les compilateurs C ne vérifient pas que le format correct est utilisé pour les arguments d’un appel à scanf(). Les erreurs les plus courantes sont d’utiliser le format %f pour des double (qui doivent utiliser le format %lf) et de confondre %c et %s pour les caractères et les chaînes.

2.4 Taille des tableaux

Les tableaux en C commencent toujours à l’indice 0. Ce qui veut dire qu’un tableau de 10 entiers défini par :

    int a[10];

a des indices valides de 0 à 9, pas 10 ! Il est très courant que les étudiants dépassent d’une case la fin d’un tableau. Cela peut conduire à un comportement imprévisible du programme.

2.5 Division entière

Contrairement au Pascal, le C utilise l’opérateur / pour les divisions réelle et entière. Il est important de comprendre comment le C détermine laquelle il fera. Si les deux opérandes sont d’un type entier, la division entière est utilisée, sinon la division réelle est utilisée. Par exemple :

double moitie = 1/2;

Ce code met dans moitie la valeur 0 pas 0.5 ! Pourquoi ? Parce que 1 et 2 sont des constantes entières. Pour corriger cela, changez au moins l’une d’elle en une constante réelle.

double moitie = 1.0/2;

Si les deux opérandes sont des variables entières et que vous voulez une division réelle, convertissez le type d’une des variables en double (ou float) par un transtypage (cast).

    int x = 5, y = 2;
    double d = ((double) x)/y;

2.6 Erreurs de boucles

En C, une boucle répète l’instruction qui vient juste après l’instruction de boucle. Le code suivant :

    int x = 5;
    while( x > 0 );
      x--;

est une boucle infinie. Pourquoi ? Le point-virgule après le while définit l’instruction à répéter comme étant l’instruction nulle (qui ne fait rien). Enlevez le point-virgule et la boucle fonctionnera comme prévu.

Une autre erreur courante avec les boucles est de faire une itération de trop ou une de moins. Vérifiez les conditions des boucles attentivement !

2.7 Ne pas utiliser les prototypes

Les prototypes indiquent au compilateur des caractéristiques importantes d’une fonction : le type de retour et les paramètres de la fonction. Si aucun prototype n’est donné, le compilateur suppose que la fonction retourne un int et peut prendre n’importe quel nombre de paramètres de n’importe quel type.

Une raison importante pour laquelle utiliser des prototypes est de permettre au compilateur de vérifier s’il y a des erreurs dans les listes d’arguments des appels de fonctions. Cependant, un prototype doit obligatoirement être utilisé si la fonction ne retourne pas un int. Par exemple, la fonction sqrt() retourne un double, pas un int. Le code suivant :

    double x = sqrt(2);

ne fonctionnera pas correctement si un prototype:

    double sqrt(double);

n’apparaît pas au-dessus de lui. Pourquoi ? Sans prototype, le compilateur C suppose que sqrt() retourne un int. Puisque la valeur retournée est stockée dans une variable de type double, le compilateur insère du code pour convertir la valeur en double. Cette conversion n’est pas requise et résultera en une valeur erronée.

La solution à ce problème est d’inclure le fichier d’en-tête C correct qui contient le prototype de sqrt(), à savoir math.h. Pour les fonctions que vous écrivez, vous devez soit placer le prototype en haut du fichier source soit créer un fichier d’en-tête et l’inclure.

2.8 Ne pas initialiser les pointeurs

Chaque fois que vous utilisez un pointeur, vous devriez être capable de répondre à la question : Quelle est la variable pointée par ce pointeur ? Si vous ne pouvez pas y répondre, c’est probablement qu’il ne pointe aucune variable. Ce type d’erreur va souvent résulter en une « erreur de segmentation/coredump » sous UNIX/Linux ou une erreur de protection générale sous Windows. (Sous le bon vieux DOS (beurk!), tout pouvait arriver !)

Voici un exemple de ce type d’erreur.

    #include <string.h>
    int main()
    {
      char * chn;   /* définit un pointeur sur un char ou un tableau de char */

      strcpy(chn, "abc");  /* sur quel tableau de char pointe chn ?? */
      return 0;
    }

Comment faire cela correctement ? Il faut soit utiliser un tableau soit allouer dynamiquement un tableau.

    #include <string.h>
    int main()
    {
      char chn[20];   /* définit un tableau de char */

      strcpy(chn, "abc");  /* chn pointe sur un tableau de char */
      return 0;
    }

ou

    #include <string.h>
    #include <stdlib.h>
    int main()
    {
      char *chn = malloc(20);   /* chn pointe sur un tableau alloué */

      strcpy(chn, "abc");  /* chn pointe sur un tableau de char */
      free(chn);                /* n'oubliez pas de désallouer une fois fini ! */
      return 0;
    }

En fait, la première solution est beaucoup plus préférable pour ce que fait ce code. Pourquoi ? L’allocation dynamique devrait seulement être utilisée quand elle est requise. Elle est plus lente et plus sujette aux erreurs que la simple définition d’un tableau normal.

3. Erreurs de chaînes de caractères

3.1 Confondre les constantes caractère et chaîne de caractères

C considère les constantes caractères et chaînes de caractères comme des choses très différentes. Les constantes caractères sont entourées d’apostrophes (guillemets simples) et les constantes chaînes sont entourées de guillemets (doubles). Les constantes chaînes agissent comme des pointeurs sur la véritable chaîne. Examinez le code suivant :

    char c = 'A';     /* correct */
    char c = "A";     /* erreur  */

La deuxième ligne affecte à la variable caractère c l’adresse d’une constante chaîne. Cela devrait générer une erreur du compilateur. Il devrait arriver la même chose si à un pointeur sur une chaîne est affectée une constante caractère :

    const char * chn = "A";     /* correct */
    const char * chn = 'A';     /* erreur  */

3.2 Comparer des chaînes avec ==

N’utilisez jamais l’opérateur == pour comparer la valeur de deux chaînes ! Les chaînes sont des tableaux de char. Le nom d’un tableau de caractères agit comme un pointeur sur la chaîne (exactement comme d’autres types de tableaux en C). Et alors ? Examinez le code suivant :

    char chn1[] = "abc";
    char chn2[] = "abc";
    if ( chn1 == chn2 )
      printf("Oui");
    else
      printf("Non");

Ce code affiche en sortie Non. Pourquoi ? Parce que l’opérateur == compare les valeurs des pointeurs chn1 et chn2, pas les données sur lesquelles ils pointent. La manière correcte de comparer deux valeurs chaînes est d’utiliser la fonction strcmp() de la bibliothèque (assurez-vous d’avoir inclus string.h). Si l’instruction if ci-dessus est remplacée par ce qui suit:

    if ( strcmp(chn1,chn2) == 0 )
      printf("Oui");
    else
      printf("Non");

alors le code affichera en sortie Oui. Pour des raisons similaires, n’utilisez pas les autres opérateurs relationnels (<, > etc.) avec les chaînes non plus. Utilisez strcmp() ici aussi.

3.3 Chaînes sans zéro terminal

C considère qu’une chaîne est un tableau de caractères avec un caractère nul terminal. Ce caractère nul a une valeur ASCII de 0 et peut être représenté simplement par 0 ou

'\0'

Cette valeur est utilisée pour marquer la fin des données significatives dans la chaîne. Si cette valeur manque, plusieurs fonctions de la bibliothèque string du C continueront à traiter les données qui suivent la fin des données significatives et souvent en dépassant la fin du tableau de caractères lui-même jusqu’à ce qu’elles finissent par trouver un octet zéro dans la mémoire !

La plupart des fonctions de la bibliothèque string du C qui créent des chaînes vont toujours les terminer proprement par un caractère nul. Certaines ne le font pas (comme strncpy() ). Assurez-vous de lire leurs descriptions attentivement.

3.4 Ne pas laisser la place pour le zéro terminal

Une chaîne en C doit avoir un zéro terminal (caractère nul de terminaison) à la fin des données significatives dans la chaîne. Une erreur courante est de ne pas allouer d’espace pour ce caractère supplémentaire. Par exemple, la chaîne définie ci-dessous

    char chn[30];

n’a de place que pour 29 (pas 30) caractères de données véritables seulement, puisqu’un caractère nul doit être présent après le dernier caractère de données.

Cela peut aussi être un problème avec l’allocation dynamique. Voici la manière correcte d’allouer une chaîne à la taille exacte nécessaire pour contenir une copie d’une autre chaîne :

    char * chn_copie = malloc( strlen(chn_orig) + 1);
    strcpy(chn_copie, chn_orig);

L’erreur courante est d’oublier d’ajouter un à la valeur de retour de strlen(). La fonction strlen() retourne le compte des caractères de la donnée, qui n’incluent pas le zéro terminal.

Ce type d’erreur peut être très difficile à détecter. Il peut ne causer aucun problème, ou seulement en causer dans des cas extrêmes. Dans le cas d’une allocation dynamique, il peut altérer le tas (la zone de la mémoire du programme utilisée pour l’allocation dynamique) et faire échouer la prochaine opération de tas (malloc(), free(), etc.).

4. Erreurs d’Entrées/Sorties

4.1 Utilisation incorrecte de fgetc(), etc.

Les fonctions fgetc(), getc() et getchar() renvoient toutes une valeur entière en retour. Par exemple, le prototype de fgetc() est :

    int fgetc( FILE * );

Parfois cette valeur entière est vraiment un simple caractère, mais il y a un cas très important dans lequel la valeur de retour n’est pas un caractère !

Quelle est cette valeur ? EOF. Une idée fausse courante chez les étudiants est que les fichiers ont un caractère EOF spécial à la fin (NdT : EOF=End Of File=Fin de fichier). Il n’y a pas de caractère spécial enregistré à la fin d’un fichier. EOF est un code d’erreur entier retourné par une fonction. Voici la mauvaise manière d’utiliser fgetc() :

    int count_line_size( FILE * fp )
    {
      char ch;
      int  cnt = 0;

      while( (ch = fgetc(fp)) != EOF && ch != '\n')
        cnt++;
      return cnt;
    }

Qu’est-ce qui ne va pas ici ? Le problème réside dans la condition de la boucle while. Afin de mieux voir, voici la boucle réécrite pour montrer ce que le C fera en coulisses.

    while( (int) ( ch = (char) fgetc(fp) ) != EOF && ch != '\n')
      cnt++;

Le type de la valeur de retour de fgetc(fp) est converti en char pour stocker le résultat dans ch. Ensuite, la valeur de ch doit être reconvertie en int à nouveau pour la comparer à EOF. Et alors ? Convertir une valeur du type int au type char puis à nouveau en int peut ne pas redonner la valeur de type int originale. Ce qui veut dire dans l’exemple ci-dessus que si fgetc() renvoie en retour la valeur EOF, la conversion peut modifier la valeur de façon que la comparaison ultérieure avec EOF soit fausse.

Quelle est la solution ? Rendre la variable ch de type int comme ceci :

    int count_line_size( FILE * fp )
    {
      int ch;
      int  cnt = 0;

      while( (ch = fgetc(fp)) != EOF && ch != '\n')
        cnt++;
      return cnt;
    }

Maintenant, la seule conversion de type cachée est dans la deuxième comparaison.

    while( (ch = fgetc(fp)) != EOF &&  ch != ((int) '\n') )
      cnt++;

Cette conversion n’a pas d’effet néfaste du tout ! Ainsi, la morale de tout cela est : utilisez toujours une variable de type int pour stocker le résultat de fgetc(), getc() et getchar().

4.2 Utilisation incorrecte de feof()

Il y a une méprise très largement répandue sur la façon de fonctionner de la fonction feof() du C. Beaucoup de programmeurs l’utilisent comme la fonction eof() du Pascal. Cependant, la fonction du C fonctionne différemment !

Quelle est la différence ? La fonction du Pascal renvoie vrai si la prochaine lecture va échouer à cause de la fin de fichier. La fonction du C renvoie vrai si la dernière fonction a échoué. Voici un exemple d’une mauvaise utilisation de feof() :

    #include <stdio.h>
    int main()
    {
      FILE * fp = fopen("test.txt", "r");
      char ligne[100];

      while( ! feof(fp) ) {
        fgets(ligne, sizeof(ligne), fp);
        fputs(ligne, stdout);
      }
      fclose(fp);
      return 0;
    }

Ce programme affichera en sortie la dernière ligne du fichier en entrée deux fois. Pourquoi ? Après que la dernière ligne est lue en entrée et affichée en sortie, feof() va toujours renvoyer 0 (faux) et la boucle va continuer. Le prochain fgets() échoue et ainsi la variable ligne portant le contenu de la dernière ligne n’est pas modifiée et est encore affichée en sortie. Après cela, feof() va renvoyer vrai (puisque fgets() a échoué) et la boucle se termine.

Comment réparer cela ? Voici une manière de faire :

    #include <stdio.h>
    int main()
    {
      FILE * fp = fopen("test.txt", "r");
      char ligne[100];

      while( 1 ) {
        fgets(ligne, sizeof(ligne), fp);
        if ( feof(fp) )    /* voir s'il y a EOF juste après fgets() */
          break;
        fputs(ligne, stdout);
      }
      fclose(fp);
      return 0;
    }

Toutefois, ce n’est pas la meilleure manière. Il n’y a aucune raison véritable d’utiliser feof() du tout. Les fonctions d’entrées du C renvoient des valeurs qui peuvent être utilisées pour voir s’il y a fin de fichier. Par exemple, fgets renvoie le pointeur NULL en cas de fin de fichier. Voici une meilleure version du programme :

    #include <stdio.h>
    int main()
    {
      FILE * fp = fopen("test.txt", "r");
      char ligne[100];

      while( fgets(ligne, sizeof(ligne), fp) != NULL )
        fputs(ligne, stdout);
      fclose(fp);
      return 0;
    }

L’auteur attend toujours de voir un étudiant utiliser la fonction feof() correctement !

Entre parenthèses, cette discussion s’applique également à C++ et Java. La méthode eof() d’un istream fonctionne exactement comme le feof() du C.

4.3 Laisser des caractères dans le tampon d’entrée

Les fonctions d’entrées (et de sorties) du C mettent en tampon les données. La mise en tampon consiste à stocker les données en mémoire et ne lire (ou écrire) les données des (ou dans les) périphériques d’entrée/sortie que lorsque c’est nécessaire. La lecture et l’écriture de données en grands blocs est beaucoup plus efficace qu’un octet (ou caractère) à la fois. Souvent la mise en tampon n’a pas d’effet sur la programmation.

Il y a une situation où la mise en tampon est visible, ce sont les entrées utilisant scanf(). Le clavier est en général mis en tampon ligne par ligne. Cela veut dire que chaque ligne d’entrée est stockée dans un tampon. Des problèmes peuvent apparaître quand un programme ne traite pas toutes les données dans une ligne avant de vouloir traiter la prochaine ligne d’entrée. Par exemple, examinez le code suivant :

    int x;
    char chn[31];

    printf("Entrez un entier : ");
    scanf("%d", &x);
    printf("Entrez une ligne de texte : ");
    fgets(chn, 31, stdin);

Le fgets() ne lira pas la ligne de texte qui est tapée en entrée. Au lieu de cela, il va probablement seulement lire une ligne vide. En fait, le programme n’attendra même pas d’entrée pour l’appel à fgets(). Pourquoi ? L’appel à scanf() lit les caractères dont il a besoin qui représentent le nombre entier lu en entrée, mais il laisse le '\n' dans le tampon d’entrée. Le fgets() commence alors à lire les données du tampon d’entrée. Il trouve un '\n' et s’arrête sans avoir besoin de plus d’entrées du clavier.

Quelle est la solution ? Une méthode simple est de lire et jeter tous les caractères venant du tampon d’entrée jusqu’à un '\n' après l’appel à scanf(). Puisque c’est une chose qui pourra être utilisée dans beaucoup de situations, il est logique d’en faire une fonction. Voici une fonction qui fait exactement cela :

    /* fonction vider_ligne
    *  Cette fonction lit et jette tous caractères restants
    *  de la ligne d'entrée courante d'un fichier.
    *  Paramètres :
    *     fp - pointeur sur un fichier FILE dont on lit les caractères
    *  Précondition:
    *     fp pointe sur un fichier ouvert
    *  Postcondition:
    *     le fichier référencé par fp est positionné à la fin 
    *     de la ligne suivante ou la fin du fichier.
    */
    void vider_ligne( FILE * fp )
    {
      int ch;

      while( (ch = fgetc(fp)) != EOF && ch != '\n' )
        /* corps nul */;
    }

Voici le code précédent réparé en utilisant la fonction ci-dessus :

    int x;
    char chn[31];

    printf("Entrez un entier : ");
    scanf("%d", &x);
    vider_ligne(stdin);
    printf("Entrez une ligne de texte : ");
    fgets(chn, 31, stdin);

Une solution incorrecte est d’utiliser ce qui suit :

    fflush(stdin);

Cette instruction sera compilée mais son comportement est indéfini par la norme ANSI du C. La fonction fflush() est seulement faite pour être utilisée sur des flux ouvert en sortie, pas en entrée. Cette méthode semble fonctionner avec certains compilateurs C, mais n’est pas du tout portable ! Par conséquent elle ne doit pas être utilisée.

4.4 Utiliser la fonction gets()

N’utilisez pas cette fonction ! Elle ne sait pas combien de caractères peuvent être stockés sans danger dans la chaîne qui lui est transmise. Ainsi, si trop de caractères sont lus, le contenu de la mémoire sera endommagé. Beaucoup de bugs de sécurité ont été exploités sur Internet en utilisant cela ! Utilisez la fonction fgets() plutôt (en lisant depuis stdin). Mais souvenez-vous que contrairement à gets(), fgets() n’élimine pas le \n final de l’entrée.

L’utilisation des fonctions scanf() peut également être dangereuse. Le format %s peut écraser la chaîne de destination. Toutefois, il peut être utilisé sans risque en spécifiant une largeur. Par exemple, le format %20s ne va pas lire plus de 20 caractères.

5. Remerciements

L’auteur désire remercier Stefan Ledent pour avoir suggéré la section « Ne pas laisser la place pour le zéro terminal ».

Copyright © 2008 Paul Carter. Tous droits réservés.
Traduction : Amine Brikci-Nigassa 2013.

Advertisements

From → C, Programmation

Laisser un commentaire

Laisser un commentaire

Entrez vos coordonnées ci-dessous ou cliquez sur une icône pour vous connecter:

Logo WordPress.com

Vous commentez à l'aide de votre compte WordPress.com. Déconnexion / Changer )

Image Twitter

Vous commentez à l'aide de votre compte Twitter. Déconnexion / Changer )

Photo Facebook

Vous commentez à l'aide de votre compte Facebook. Déconnexion / Changer )

Photo Google+

Vous commentez à l'aide de votre compte Google+. Déconnexion / Changer )

Connexion à %s

%d blogueurs aiment cette page :