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 : | 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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 | #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[i]; 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", bad_character_info.index, example ); break; case PARSE_NUMBER_TOO_BIG: printf( "number was too big. had %zu digits left: %s\n", too_big_info.remaining_characters, example ); break; } } return 0; } |
worked: 10
failed because bad char at index 0: foo
worked: 42
failed because empty string
number was too big. had 5 digits left: 99999999999999
Définir une valeur statique locale de thread
Une autre option consiste à définir, en cas d'erreur, une variable statique locale. Cela évite d'avoir à propager explicitement une erreur tout le long de la pile à partir de l'endroit où elle se produit et rend l'API "normale" de la fonction aussi propre et nette que les approches autruche ou crash. Une fois que vous avez défini la valeur statique locale du thread, vous pouvez soit :
- Retourner une valeur prévisible indiquant un problème (NULL, un nombre négatif, etc.), ce qui incite le programmeur à vérifier la valeur statique locale du thread.
- Retourner une valeur non initialisée et compter sur le programmeur pour savoir que la valeur pourrait être fausse à moins qu'il ne vérifie la valeur statique locale du thread.
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 | #include <stdio.h> #include <stdbool.h> _Thread_local static bool parse_number_error = false; 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') { parse_number_error = true; } 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); if (parse_number_error) { parse_number_error = false; printf("error: %s\n", example); } else { printf("parsed: %d\n", parsed); } } return 0; } |
parsed: 10
parsed: 42
error: foo
Un grand nombre d'apis intégrées utilisent une constante statique partagée appelée errno et si elles échouent, elles lui attribuent une valeur non nulle. Il existe ensuite des fonctions comme perror qui peuvent extraire des messages à partir du code d'erreur spécifique. Techniquement, il est possible utiliser errno aussi, tant que vos conditions d'erreur peuvent tenir dans son encodage int.
Des langages tels C2 ou C3 pour remplacer le C ?
Comme dit précédemment, la gestion des erreurs dans le langage de programmation C n'est pas prise en charge, car il fournit quelques fonctions et des valeurs de numéros d'erreur qui sont imprimées comme des messages d'erreur. Le lamgage ne dispose que d'un support de bibliothèque très limité : il faut ajouter des chemins de recherche pour les fichiers d'en-tête, inclure certains fichiers d'en-tête et établir des liens avec des bibliothèques statiques ou dynamiques. Ces étapes sont toutes séparées. Si vous appelez des fonctions de bibliothèque sans les lier, vous pouvez avoir des références non définies.
C2 a corrigé ce problème en faisant de l'utilisation de la bibliothèque une chose totalement automatique. Vous utilisez la bibliothèque ou vous ne l'utilisez pas. De plus, C2 supporte les bibliothèques sources. Il s'agit de bibliothèques qui sont utilisées sous forme de source (=C2). Cela permet une meilleure intégration et optimisation, en particulier lors de l'utilisation de nombreuses fonctions "simples" qui ne font que renvoyer un membre d'une structure opaque, par exemple. Cela permet également aux développeurs d'organiser leurs archives de code d'une manière beaucoup plus facile.
C3 est un langage de programmation système basé sur le C. C'est une évolution du C permettant les mêmes paradigmes et conservant la même syntaxe dans la mesure du possible. C3 a commencé comme une extension du langage C2 par Bas van den Berg. Il a évolué de manière significative, non seulement au niveau de la syntaxe mais aussi en ce qui concerne la gestion des erreurs, les macros, les génériques et les chaînes de caractères.
Source : Mccue
Et vous ?
Que pensez-vous du langage C ? Dépassé ou d'actaulité
Quel langage de programmation utilisez-vous pour contourner les manquements du C ?
À votre avis, C2 et C3 pourraient remplacer le C ?
N'aurait-il pas été plus bénéfique de conjuguer les efforts pour améliorer le C plutôt que de créer C2 ou encore C3 et peut être C4 ?
Voir aussi :
C3 : un langage de programmation système basé sur le C, permet un accès sécurisé aux tableaux, les conteneurs de haut niveau et manipulation des chaînes de caractères
Microsoft célèbre les 20 ans de .NET, son Framework de développement, les dépôts .NET seraient dans le top 30 des projets open source à plus haute vélocité sur GitHub depuis 2017
Microsoft a publié la version stable de Visual Studio 2022 avec une nouvelle expérience de rechargement à chaud pour les applications natives C++, cette version est disponible uniquement en 64 bits
Un développeur publie un langage de programmation qui peut être traduit automatiquement en C, C++, C#, Java, JavaScript, etc., avec une traduction rapide et sans machine virtuelle