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
2. stderr
4. strerror
5. assert
7. exit
8. abort
9. atexit
|
||||
Plusieurs sortes d'erreurs | ||||
On classe les fonctions retournant une valeur en
deux catégories :
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...
|
||||
|
||||
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 :
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 :
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 :
|
||||
exit | ||||
La fonction exit stop
l'ex&écution d'un programme après avoir :
|
||||
int exit(int status); |
||||
|
||||
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)); |
||||
|
||||
par Valentin BILLOTTE vbillotte@programmationworld.com http://www.programmationworld.com Dernière mise à jour: |