Gestion des erreurs




        Déterminer qu'une méthode a générée une erreur est bien. Déterminer la cause de l'erreur est mieux. Le C est un langage qui n'est pas du tout sécurisé. Cette tache est laissée au programmeur qui n'a droit à aucune erreur sous peine de risque de plantage lamentable.
Pourtant si cette tache est fastidieuse (controler le programme en tout point) elle est très simple et s'avère (comme on va le voir) très utile...


Sommaire

1. Plusieurs sortes d'erreurs
2. stderr
3. errno et perror
4. strerror
5. assert
6. Stopper l'exécution d'un programme
7. exit
8. abort
9. atexit


Plusieurs sortes d'erreurs

        On classe les fonctions retournant une valeur en deux catégories :
  • les fonctions retournant un pointeur
  • les fonctions retournant une valeur entière

Les fonctions du premier type retourne généralement NULL en cas d'erreur, les fonctions du second type retournent, elle une valeur négative (le plus souvent -1) en cas de problèmes.
Un traitement usuel des erreurs en C se fait ainsi de cette manière :


pointeur = malloc(20);

if (pointeur == NULL) //ou if (!pointeur)
{
  fprintf(stderr, "impossible d'allouer la mémoire");
  exit(1);
}

        ou bien :


descripteur = socket(AF_UNIX, SOCK_STREAM, 0);

if (descripteur == -1)
{
  perror("erreur dans la création de la socket");
  exit(1);
}

        Les deux exemples ci-dessus montrent une gestion d'erreur parfaite. Un bon programmeur C vérifie toujours la valeur renvoyée par une fonction afin de déterminer si oui ou non son exécution s'est bien passée. Il s'agit là de la première cause de bug en C...


  Danger
Toujours tester le retour d'une fonction et agit en conséquence
si fonction renvoie errer alors :
-gérer le problème

        Les deux exemples ci-dessus montrent deux manières de gérer les erreurs :
avec fprintf redirigé vers stderr, et avec la routine perror. Voyons cela en détail.


stderr

        Un premiere question peut nous venir à l'esprit :
  • Pourquoi faire fprintf(stderr, "blabla"); plutot que printf("blabla"); plus simple à écrire et à lire ?


En fait utiliser printf pour signaler une erreur est une TRES MAUVAISE habitude !
Pourquoi ?
Imaginons que le programme que nous avons créé génère sur la sortie standard le contenu d'un document par exemple :


$ ls -l

rwxr-xr-x   1 gh          user               1 Nov 23  15:17 monfichier.txt

$ mon prog monfichier.txt

voici le contenu de mon fichier txt

$

        comme on le voit dans cet exemple, le programme appelé "monprog" a affiché le contenu du fichier "monfichier.txt" qu'on lui a passé en paramètre.
Imaginons que noptre programme génère une erreur du style pas assez de mémoire gérée de cette manière dans le code :


pointeur = malloc(20);

if (pointeur == NULL) 
{
  printf("impossible d'allouer la memoire");
  exit(1);
}

        Si nous tentons d'afficher notre fichier avec un système sans mémoire ram nbous obtiendrons la sortie :


$ ls -l

rwxr-xr-x   1 gh          user               1 Nov 23  15:17 monfichier.txt

$ mon prog monfichier.txt

impossible d'allouer la memoire

$

        A première vue, il n'y a aucun problème. Pourtant si nous tentons de rédiriger la sortie vers un fichier


$ ls -l

rwxr-xr-x   1 gh          user               1 Nov 23  15:17 monfichier.txt

$ mon prog monfichier.txt > autrefichier.txt

$ ls -l

rwxr-xr-x   1 gh          user               1 Nov 23  15:17 monfichier.txt
rwxr-xr-x   1 gh          user               1 Nov 23  15:17 autrefichier.txt

$ cat autrefichier.txt

impossible d'allouer la memoire

$

        le fichier vers lequel on a redirigé contient notre texte d'erreur ! Ce texte n'a absolument rien à voir avec le texte de monfichier.txt mais l'utilisateur n'est pas obligé de le savoir (peu d'utilisateurs feraient un cat pour lire le contenu).
Tout traitement effectué sur autrefichier.txt sera donc faussé. Si maintenant nous redirigeons notre erreur vers la sortie d'erreur stderr


pointeur = malloc(20);

if (pointeur == NULL)
{
  fprintf(stderr, "impossible d'allouer la memoire");
  exit(1);
}

        alors dans le meme contexte, voici ce qui va se passer :


$ ls -l

rwxr-xr-x   1 gh          user               1 Nov 23  15:17 monfichier.txt

$ mon prog monfichier.txt > autrefichier.txt

impossible d'allouer la memoire

$ ls -l

rwxr-xr-x   1 gh          user               1 Nov 23  15:17 monfichier.txt
rwxr-xr-x   1 gh          user               1 Nov 23  15:17 autrefichier.txt

$ cat autrefichier.txt

$

        l'utilisateur a vu l'erreur, et le fichier autrefichier.txt est toujours vide. Tout est donc normal et non sujet a des problèmes d'interprétation.
L'exemple que nous avons pris ici mettait en jeu un utilisateur, il y'a donc possibilité de vérification, mais dans le cas de taches automatisées, les résultats d'une tel opération peuvent etre désastreux...


errno et perror

        La bibliothèque standard fournit un moyen efficace au programmeur pour identifier les erreurs. Le fichier d'entete errno.h déclare un grand nombre d'identificateurs représentants un grand nombre de messages d'erreurs


extern int errno;
  
#ifndef __STRICT_ANSI__

#define E2BIG   3
#define EACCES    4
#define EAGAIN    5
#define EBADF   6
#define EBUSY   7
#define ECHILD    8
#define EDEADLK   9
#define EEXIST    10
#define EFAULT    11
#define EFBIG   12
#define EINTR   13
#define EINVAL    14
#define EIO   15
#define EISDIR    16
#define EMFILE    17
#define EMLINK    18
#define ENAMETOOLONG  19
#define ENFILE    20
#define ENODEV    21
#define ENOENT    22
#define ENOEXEC   23
#define ENOLCK    24
#define ENOMEM    25
#define ENOSPC    26
#define ENOSYS    27
#define ENOTDIR   28
#define ENOTEMPTY 29
#define ENOTTY    30
#define ENXIO   31
#define EPERM   32
#define EPIPE   33
#define EROFS   34
#define ESPIPE    35
#define ESRCH   36
#define EXDEV   37


        Chacun de ces codes sont des entiers positifs. Chacun de ces entiers correspond à une erreur précise d'une fonction donnée.
En cas d'erreur dans le programme, la variable errno est positionnée avec la valeur du code d'erreur correspondant.
Le fichier stdio.h contient en outre la définition de la méthode suivante :


void perror(const char *chaine)

        Le fonctionnement de cette routine est fort simple : elle affiche le contenu de la chaine de caractères passée en paramètre suivit de deux points ';' puis du message d'erreur lui meme. Ce message d'erreur justement correspond au nombre contenu dans la variable entière errno.
Prenons un exemple pour etre plus clair : tentons d'ouvrir en C un fichier qui n'existe pas. La méthode open va donc retourner -1 pour indiquer qu'il y a eu un problème. Cette valeur négative n'indique en rien la nature de l'erreur.
Pourtant si nous faisons un :


perror("erreur open");

        nous obtiendrons l'affichage :


erreur open: no such file or directory

        on reconnait là, la chaine que l'ont a passé en argument, suivit des deux points ';' puis du message d'erreur en lui meme.


strerror

        La fonction strerror dont le prototype est :


char * strerror(int code);

        retourne un pointeur sur la chaine de caractère contenant le message d'erreur correspondant au code qu'on lui a passé en paramètre.
En clair :


perror("open");

        et


fprintf(stderr, "open :%s\n", strerror(errno));

        aboutissent au meme résultat.


assert

        Terminons cet apprentissage des erreurs par la fonction assert. Celle-ci se trouve définie à l'intérieur du fichier assert.h et se trouvé déclarée ainsi :


void assert(int expression);

        Le principe de la macro assert est fort simple. Elle détermine si l'expression qu'on lui a passé en argument est vraie (true). Si c'est le cas, elle ne réagit pas. Dans le cas contraire est provoque deux actions :
  1. elle génère un message sur la sortie d'erreur en indiquant la ligne du programme où se trouve l'erreur
  2. la routine abort est alors appelée afin de mettre fin à l'exécution du programme

L'avantage de assert est qu'il est possible de l'inniber en définissant à l'aide de #define le symbole NDEBUG avant d'inclure le fichier assert.h :


#define NDEBUG
#include <assert.h>

        Comme on le voit à l'intérieur de ce fichier d'entete :


#undef  assert

#ifdef  NDEBUG

#define assert(exp)     ((void)0)

#else

#ifdef  __cplusplus
extern "C" {
#endif

_CRTIMP void __cdecl _assert(void *, void *, unsigned);

#ifdef  __cplusplus
}
#endif

#define assert(exp) (void)( (exp) || (_assert(#exp, __FILE__, __LINE__), 0) )

#endif  /* NDEBUG */


        si NDEBUG est défini, alors assert ne fait plus rien :


#define assert(exp)     ((void)0)

        Prennons un exemple concret pour mieux comprendre l'avantage certain d'assert. Imaginons que vous ayez écrit un programme énorme. Généralement on place un peu partout dans le programme des printf() affichant la valeur d'une variable afin de pouvoir suivre le déroulement du programme et de pouvoir comprendre la cause d'une erreur si elle survient. Malheureusement si l'ont veut créer une version du programme sans tout ces affichages , il va falloir effacer à la main tous les printf en trop. Si on avait fait nos tests avec assert, il aurait suffit de définir NDEBUG avant d'inclure assert.h afin d'enlever toutes les sorties d'erreurs...
Terminons sur un bout de code montrant assert en action :


int retour = mafonction();

assert(retour > 0);

        dans le code si dessus on teste si le retour de la fonction est supérieur à 0. Dans le cas contraire, et si NDEBUG n'est pas défini, le programme sera stoppé et la ligne de code fautive sera affichée.


Stopper l'exécution d'un programme

        Le bibliotèque standard du C offre au programmeur trois fonction permettant d'arreter un programme :
  1. void exit(int status);
  2. void abort(void)
  3. void atexit(void (*fonction)(void));


exit

        La fonction exit stop l'ex&écution d'un programme après avoir :
  • exécuté les fonctions enregistrées à l'aide de la méthode atexit (voir plus loin)
  • vidé tous les tampons associés à des fichiers et fermé tous les fichiers ouverts.
  • renvoyé à l'environnement où est exécuté le programme la valeur qu'on lui a passé en paramètre (status)


int exit(int status);


  Remarque
Il est à noter que si l'ont se trouve à l'intérieur de la fonction main ,alors une instruction de la forme :

exit(valeur);

est identique à

return(valeur);


abort

        La fonction abort arrête un programme de manière "anormale" en envoyant au processus le signal SIGABRT.


void abord(void);


atexit

        La fonctoin atexit enregistre la fonction qu'on lui passe en argument. Cette fonction est alors appelée automatiquement si un appel à exit intervient.
D'après la norme ANSI, jusqu'à 32 fonctions peuvent être ainsi enregistrées. Il est à noter qu'elles sont exécutée dans l'ordre inverse de leur enregistrement.


void atexit(void (*fonction)(void));


  Remarque
Comme on le voit sur le prototype d'atexit ci-dessus, seules les fonctions n'acceptant aucun argument (void) et ne renvoyant rien (void) peuvent être enregistrées.


[ Précédent | Index | Suivant ]



par Valentin BILLOTTE
vbillotte@programmationworld.com
http://www.programmationworld.com
Dernière mise à jour: