
le C ne dispose pas d'une seule façon claire pour gérer les erreurs
Le langage de programmation C offre au développeur une marge de contrôle importante sur la machine (notamment sur la gestion de la mémoire) et est de ce fait utilisé pour réaliser les « fondations » (compilateurs, interpréteurs…) des langages plus modernes. C'est un langage de programmation impératif généraliste, de bas niveau. Inventé au début des années 1970 pour réécrire Unix, C est devenu un des langages les plus utilisés, encore de nos jours. De nombreux langages plus modernes comme C++, C#, Java et PHP ou JavaScript ont repris une syntaxe similaire au C et reprennent en partie sa logique.
En tant que tel, le langage C ne fournit pas de support direct pour la gestion des erreurs, mais étant un langage de programmation système, il vous fournit un accès à un niveau inférieur sous la forme de valeurs de retour. La plupart des appels de fonctions C ou même Unix renvoient -1 ou NULL en cas d'erreur et définissent un code d'erreur errno. Ainsi, un programmeur C peut vérifier les valeurs retournées et peut prendre les mesures appropriées en fonction de la valeur de retour.
Essayons de simuler une condition d'erreur et d'ouvrir un fichier qui n'existe pas. Ici, les deux fonctions sont utilisées pour montrer leur usage, mais il est possible d'utiliser une ou plusieurs façons d'imprimer les erreurs. Le deuxième point important à noter est qu'il est possible d'utiliser le flux de fichiers stderr pour afficher toutes les erreurs.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <stdio.h> #include <errno.h> #include <string.h> extern int errno ; int main () { FILE * pf; int errnum; pf = fopen ("unexist.txt", "rb"); if (pf == NULL) { errnum = errno; fprintf(stderr, "Value of errno: %d\n", errno); perror("Error printed by perror"); fprintf(stderr, "Error opening file: %s\n", strerror( errnum )); } else { fclose (pf); } return 0; } |
Lorsque le code ci-dessus est compilé et exécuté, il produit le résultat suivant
Value of errno: 2
Error printed by perror: No such file or directory
Error opening file: No such file or directory
Le C n'a pas une seule façon claire de gérer les erreurs
Statut de sortie
Il fournit une fonction exit() qui prend deux valeurs pour afficher une fin réussie ou non réussie en utilisant EXIT_SUCCESS et EXIT_FAILURE. Cette fonction exit() est définie dans le fichier d'en-tête stdlib.h de la bibliothèque standard.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | #include <stdio.h> #include <errno.h> #include <string.h> #include <stdlib.h> int main () { FILE * f; f = fopen ("article.txt", "rb"); if (f == NULL) { printf("The Value of errno printed is : %d\n", errno); printf("Error message printed while opening the file with errno: %s\n", strerror(errno)); perror("Error message printed by perror"); exit(EXIT_FAILURE); printf("The message will not be printed\n"); } else { fclose (f); exit(EXIT_SUCCESS); printf("The message will be printed\n"); } return 0; } |
L'algorithme de l'autruche
Si une condition d'erreur est suffisamment rare, il est toujours possible de faire l'autruche et choisir d'ignorer cette possibilité. Cela peut rendre le code beaucoup plus joli, mais au détriment de la robustesse.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | #include <stdio.h> int parse_natural_base_10_number(const char* s) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { parsed *= 10; parsed += s[i] - '0'; } return parsed; } int main() { printf("Expecting garbage or crash on bad values\n"); const char* examples[] = { "10", "foo", "42", "" }; for (size_t i = 0; i < 4; i++) { const char* example = examples[i]; int parsed = parse_natural_base_10_number(example); printf("parsed: %d\n", parsed); } return 0; } |
Expecting garbage or crash on bad values
parsed: 10
parsed: 6093
parsed: 42
parsed: 0
Un exemple concret de cela peut être observé avec l'utilisation de malloc par le micrologiciel des dispositifs de flipper.
Crash
Parfois, les erreurs sont pratiquement irrécupérables. Selon McCue, la plupart des applications devraient probablement abandonner lorsque malloc renvoie NULL. Si vous êtes sûr qu'il n'y a pas de moyen de récupérer une condition d'erreur et que l'appelant ne voudra pas la gérer d'une autre manière, il est simplement possible d'imprimer un message disant ce qui s'est mal passé et quitter le programme.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | #include <stdio.h> #include <stdlib.h> int parse_natural_base_10_number(const char* s) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { printf( "Got a bad character ('%c') in %s, crashing.", s[i], s ); exit(1); } else { parsed *= 10; parsed += s[i] - '0'; } } return parsed; } int main() { const char* examples[] = { "10", "42", "foo" }; for (size_t i = 0; i < 3; i++) { const char* example = examples[i]; int parsed = parse_natural_base_10_number(example); printf("parsed: %d\n", parsed); } return 0; } |
parsed: 10
parsed: 42
Got a bad character ('f') in foo, crashing.
Retourner un nombre négatif
Si la fonction renvoie normalement un nombre naturel, il est possible d'utiliser un nombre négatif pour indiquer un échec. Cela s'applique aussi bien à l'exemple qu'à des cas tels que le renvoi du nombre d'octets lus dans un fichier. S'il existe différents types d'erreurs pour ce genre de cas,il est également possible d'utiliser des nombres négatifs spécifiques pour indiquer les différentes catégories.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | #include <stdio.h> int parse_natural_base_10_number(const char* s) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { return -1; } else { parsed *= 10; parsed += s[i] - '0'; } } return parsed; } int main() { const char* examples[] = { "10", "foo", "42" }; for (size_t i = 0; i < 3; i++) { const char* example = examples[i]; int parsed = parse_natural_base_10_number(example); if (parsed < 0) { printf("failed: %s\n", example); } else { printf("worked: %d\n", parsed); } } return 0; } |
worked: 10
failed: foo
worked: 42
Retourner NULL
Si la fonction renvoie normalement un pointeur, il est possible d'utiliser NULL pour indiquer que quelque chose s'est mal passé. La plupart des fonctions qui renverraient des pointeurs effectueraient une allocation au tas pour que cela soit sain, donc ce schéma n'est probablement pas applicable lorsque vous voulez éviter les allocations. Pour McCue, il serait stupide d'allouer un [/C]int[/C] au tas.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | #include <stdio.h> #include <stdlib.h> int* parse_natural_base_10_number(const char* s) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { return NULL; } else { parsed *= 10; parsed += s[i] - '0'; } } int* result = malloc(sizeof (int)); *result = parsed; return result; } int main() { const char* examples[] = { "10", "foo", "42" }; for (size_t i = 0; i < 3; i++) { const char* example = examples[i]; int* parsed = parse_natural_base_10_number(example); if (parsed == NULL) { printf("failed: %s\n", example); } else { printf("worked: %d\n", *parsed); } free(parsed); } return 0; } |
worked: 10
failed: foo
worked: 42
Un exemple concret de ce schéma est celui de malloc. Si malloc ne parvient pas à allouer de la mémoire, au lieu de renvoyer un pointeur vers la mémoire nouvellement allouée, il renvoie un pointeur nul.
Retourner un booléen et prendre un paramètre externe
L'une des choses les moins évidentes qu'il est possible de faire en C est d'avoir un ou plusieurs arguments d'une fonction out params. Cela signifie qu'il fait partie du contrat de la fonction qu'elle écrira dans la mémoire derrière un pointeur. Si une fonction peut échouer, une traduction naturelle de ceci peut être de retourner un booléen indiquant si elle l'a fait et de passer un paramètre out qui n'est n'inspecté que lorsque true est retourné.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 | #include <stdio.h> #include <stdbool.h> bool parse_natural_base_10_number(const char* s, int* out) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { return false; } else { parsed *= 10; parsed += s[i] - '0'; } } *out = parsed; return true; } int main() { const char* examples[] = { "10", "foo", "42" }; for (size_t i = 0; i < 3; i++) { const char* example = examples[i]; int parsed; bool success = parse_natural_base_10_number( example, &parsed ); if (!success) { printf("failed: %s\n", example); } else { printf("worked: %d\n", parsed); } } return 0; } |
Retourner une énumération et prendre un paramètre de sortie
Un booléen peut seulement indiquer que quelque chose a réussi ou échoué. Si vous voulez savoir pourquoi quelque chose a échoué, remplacer un enum par un booléen est un mécanisme assez naturel.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | #include <stdio.h> enum ParseNaturalNumberResult { PARSE_NATURAL_SUCCESS, PARSE_NATURAL_EMPTY_STRING, PARSE_NATURAL_BAD_CHARACTER } ; enum ParseNaturalNumberResult parse_natural_base_10_number( const char* s, int* out ) { if (s[0] == '\0') { return PARSE_NATURAL_EMPTY_STRING ; } int parsed = 0 ; for (size_t i = 0 ; s[i] != '\0' ; i++) { if (s[i] < '0' || s[i] > '9') { return PARSE_NATURAL_BAD_CHARACTER ; } else { parsed *= 10 ; parsed += s[i] - '0' ; } } *out = parsed ; return PARSE_NATURAL_SUCCESS ; } int main() { const char* examples[] = { "10", "foo", "42", "" } ; for (size_t i = 0 ; i < 4 ; i++) { const char* exemple = exemples[i] ; int parsed ; switch (parse_natural_base_10_number(example, &parsed)) { cas PARSE_NATURAL_SUCCESS : printf("a fonctionné : %d\n", parsed) ; pause ; cas PARSE_NATURAL_EMPTY_STRING : printf("failed because empty string\n") ; pause ; cas PARSE_NATURAL_BAD_CHARACTER : printf("failed because bad char : %s\n", exemple) ; break ; } } return 0 ; } |
failed because bad char: foo
worked: 42
failed because empty string
Retourner un booléen et prendre deux paramètres en sortie
Alors qu'un enum peut donner la "catégorie" d'une erreur, il n'a pas de place pour enregistrer des informations plus spécifiques que cela. Par exemple, il est assez raisonnable de vouloir savoir, si vous rencontrez un caractère inattendu, où se trouve ce caractère dans la chaîne de caractères. En ajoutant un deuxième paramètre out, il est possible d'avoir un endroit pour mettre cette information.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 | #include <stdio.h> #include <stdbool.h> bool parse_natural_base_10_number( const char* s, int* out_value, size_t* out_bad_index ) { int parsed = 0; for (size_t i = 0; s[i] != '\0'; i++) { if (s[i] < '0' || s[i] > '9') { *out_bad_index = i; return false; } else { parsed *= 10; parsed += s[i] - '0'; } } *out_value = parsed; return true; } int main() { const char* examples[] = { "10", "foo", "42", "12a34" }; for (size_t i = 0; i < 4; i++) { const char* example = examples[i]; int parsed; size_t bad_index; bool success = parse_natural_base_10_number( example, &parsed, &bad_index ); if (!success) { printf("failed: %s\n ", example); for (size_t j = 0; j < bad_index; j++) { printf(" "); } printf("\n"); } else { printf("worked: %d\n", parsed); } } return 0; } |
worked: 10
failed: foo
worked: 42
failed: 12a34
Retourner un enum et plusieurs paramètres de sortie
Une extension naturelle des deux modèles précédents est que si vous avez plusieurs façons dont un calcul peut échouer, il est possible de retourner un enumavec chaque façon et prendre un paramètre out pour chaque façon qui nécessiterait des données.
[CODE]#include <stdio.h>
#include <string.h>
enum ParseNaturalNumberResult {
PARSE_NATURAL_SUCCESS,
PARSE_NATURAL_EMPTY_STRING,
PARSE_NATURAL_BAD_CHARACTER,
PARSE_NUMBER_TOO_BIG
};
struct BadCharacterInfo {
size_t index;
};
struct TooBigInfo {
size_t remaining_characters;
};
enum ParseNaturalNumberResult parse_natural_base_10_number(
const char* s,
int* out_value,
struct BadCharacterInfo* bad_character_info,
struct TooBigInfo* too_big_info
) {
if (s[0] == '\0') {
return PARSE_NATURAL_EMPTY_STRING;
}
int parsed = 0;
for (size_t i = 0; s[i] != '\0'; i++) {
if (s[i] < '0' || s[i] > '9') {
bad_character_info->index = i;
return PARSE_NATURAL_BAD_CHARACTER;
}
else {
int digit = s[i] - '0';
int new_parsed = (parsed * 10) + digit;
if ((new_parsed - digit) / 10 != parsed) {
too_big_info->remaining_characters = strlen(s) - i;
return PARSE_NUMBER_TOO_BIG;
}
else {
parsed = new_parsed;
}
}
}
*out_value = parsed;
return PARSE_NATURAL_SUCCESS;
}
int main() {
const char* examples[] = { "10",
"foo",
"42",
"",
"99999999999999" };
for (size_t i = 0; i < 5; i++) {
const char* example = examples;
int parsed;
struct BadCharacterInfo bad_character_info;
struct TooBigInfo too_big_info;
switch (parse_natural_base_10_number(
example,
&parsed,
&bad_character_info,
&too_big_info
)) {
case PARSE_NATURAL_SUCCESS:
printf("worked: %d\n", parsed);
break;
case PARSE_NATURAL_EMPTY_STRING:
printf("failed because empty string\n");
break;
case PARSE_NATURAL_BAD_CHARACTER:
printf(
"failed because bad char at index %zu: %s\n",...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.