Pour l'amour du C, n'utilisez pas les fonctions atoi, atol, atof et dérivées

Quand c'est beaucoup trop simple, c'est qu'il y a un truc

Image d'entête

par skywodd | | Licence (voir pied de page)

Catégories : Cours | Mots clefs : Programmation C/C++ libc

Cet article n'a pas été mis à jour depuis un certain temps, son contenu n'est peut être plus d'actualité.


Dans cet article, nous allons discuter d'une erreur très fréquente en développement informatique et tout particulièrement en programmation C/C++ (et par extension en programmation Arduino) : l'utilisation des fonctions atoi(), atol(), atof() et dérivées. Nous verrons ensemble pourquoi ces fonctions posent problème et comment les remplacer par du code plus robuste.

Sommaire

Bonjour à toutes et à tous !

La bibliothèque standard C/C++ contient un très grand nombre de fonctions, plus de 1300 si l'on se réfère à la longue liste des fonctions de la bibliothèque GNU "libc" utilisée à peu près partout en informatique.

Parmi ces fonctions, on trouve de tout : des fonctions pour manipuler des fichiers, des fonctions pour traiter du texte ou des blocs de données, mais aussi des fonctions pour convertir du texte en nombre et vice versa.

Vous vous en doutez, parmi toutes ces fonctions on trouve du bon et du mauvais. Il y a par exemple des fonctions obsolètes présentes depuis les premières versions de la libc et jamais retirées par souci de compatibilité. C'est donc sans grande surprise qu'on découvre parfois de sacrées perles dans la libc.

Parmi ces perles se trouvent les fonctions atoi(), atol() et atof(), permettant respectivement de transformer une chaine de caractères en nombre entier court, en nombre entier long et en nombre à virgule.

Toutes ces fonctions ont un point commun : elles prennent en paramètres une chaine de caractères et retourne en sortie un nombre (entier ou à virgule). C'est simple et efficace, où est donc le problème me diriez-vous ?

Le problème

On y vient au problème justement !

Prenons le code C suivant :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// Inclut la bibliothèque standard pour les entrées/sorties en mode terminal
#include <stdio.h>

/** Point d'entré du programme */
int main(int argc, char* argv[]) {

    /* Affiche l'aide si aucun paramètre */
    if (argc == 1) {
        printf("Usage: %s NOMBRE\n", argv[0]);
        puts("Affiche le nombre passé en paramètre sur la sortie standard.");
        return 0;
    }

    // Affiche le nombre passé en paramètre
    printf("Vous avez saisi : %d\n", atoi(argv[1]));

    // Pas d'erreur
    return 0;
}

Ce code est parfaitement inutile, mais va nous servir d'exemple idéal pour cet article.

Le but de ce code est simple : afficher sur la sortie standard (stdout) le nombre passé en paramètre du programme. C'est tout.

PS Le code C ci-dessus est à compiler sur PC avec le compilateur GCC. Mais le problème décrit dans le chapitre suivant est valable aussi bien sur PC, Mac, ou microcontrôleurs (Arduino, AVR, ARM, PIC, etc).

Testons le programme :

1
2
C:\Users\Fabien\Desktop>test.exe 42
Vous avez saisi : 42

Rien d'anormal à première vue.

Essayons avec un nombre négatif :

1
2
C:\Users\Fabien\Desktop>test.exe -17
Vous avez saisi : -17

Tout marche au poil !

Essayons maintenant un cas d'erreur classique, en remplaçant le chiffre par du texte :

1
2
C:\Users\Fabien\Desktop>test.exe azerty
Vous avez saisi : 0

Aie.

Je n'ai pas saisi zéro, mais comme par défaut atoi(), atol() et atof() retournent une valeur nulle en cas d'erreur, cela est bien le comportement attendu.

Place à la réflexion

Maintenant que nous avons vu comment se comporte notre programme de test, je vous pose, à vous lecteurs, la question qui tue : comment faire la différence entre zéro (la valeur saisie par l'utilisateur) et zéro (la valeur de retour en cas d'erreur) ?

Ne cherchez pas trop longtemps, il n'existe en effet aucun moyen de faire la différence entre une saisie utilisateur nulle et une saisie utilisateur erronée.

Ce n'est pas plus compliqué que cela, ces fonctions n'ont pas été conçues pour permettre une détection de saisie erronée ! Il devait y a voir une très bonne raison de faire cela à une époque, mais aujourd'hui, ce comportement est devenu un vrai problème.

Ok Google.

Récemment, je me baladais sur le net à la recherche d'un morceau de code pour un autre article.

Entre deux recherches, je suis tombé sur un message de forum avec un bout de code en pièce jointe.

Je vous fais grâce du bout de code en question, l'important à retenir est que ce code utilisait atof() pour convertir une saisie utilisateur en nombre à virgule, qui était ensuite utilisé par le programme pour faire un traitement.

StackOverflow Driven Development

StackOverflow Driven Development

Aujourd'hui, beaucoup de développeurs ne prennent pas le temps de lire les documentations avant de faire quelque chose. Concevoir un programme se limite trop souvent à enchainer des copier-coller de réponses trouvées sur StackOverflow.

Moi-même, je me retrouve régulièrement dans ce genre de scénario :

  • J'ai besoin de transformer une chaine de caractère en nombre ?

  • Que dit Google à ce sujet ?

  • Premier résultat : "Fonction Atoi(), converti une chaine de caractère en nombre entier".

  • Merci Google !

Sans même se rendre compte du drame qu'ils sont eux même en train d'écrire, des centaines de développeurs au moment même où moi-même j'écris cette phrase doivent être en train de caler des appels à atoi(), atol() ou atof() un peu partout dans leurs programmes. Et un jour, ces appels auront des conséquences dramatiques, car aucune erreur n'aura été détectée durant les tests, et pour cause, il n'y a aucun moyen de détecter qu'il y a eu une erreur.

Faire un programme qui marche est une chose. Faire un programme qui résiste aux bêtises que peut saisir un utilisateur (et dieu sait qu'un utilisateur peut être inventif parfois), c'est une autre chose.

Si l'utilisateur peut, de manière directe ou indirecte, jouer sur le contenu d'une variable d'un programme, il faut tester le contenu de cette variable avant même de l'enregistrer en mémoire. C'est le principe même de la programmation défensive, principe que tout développeur devrait suivre.

Convertir une chaine de caractère en nombre

Pour convertir une chaine de caractères en nombre, il n'y a qu'une seule solution simple et solide à la fois : sscanf().

La fonction sscanf() permet de convertir une chaine de caractères en une série de valeurs utilisables par le reste du programme. Elle est plus compliquée à utiliser que atoi() / atol() / atof(), car plus robuste et générique.

L'avantage de la fonction sscanf() réside dans le fait que la valeur de retour correspond au nombre de valeurs extraites de la chaine de caractères. Pour détecter une erreur de saisie, il suffit de regarder si le nombre de valeurs extraites correspond au nombre de valeurs attendues.

Voici la version "corrigée" du code d'exemple :

 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
// Inclut la bibliothèque standard pour les entrées/sorties en mode terminal
#include <stdio.h>

/** Point d'entré du programme */
int main(int argc, char* argv[]) {

    /* Affiche l'aide si aucun paramètre */
    if (argc == 1) {
        printf("Usage: %s NOMBRE\n", argv[0]);
        puts("Affiche le nombre passé en paramètre sur la sortie standard.");
        return 0;
    }
    
    // Converti le texte en nombre
    int valeur;
    if (sscanf(argv[1], "%d", &valeur) != 1) {
        puts("Erreur: Le paramètre NOMBRE doit être un nombre entier !");
        return 1;
    }

    // Affiche le nombre passé en paramètre
    printf("Vous avez saisi : %d\n", valeur);

    // Pas d'erreur
    return 0;
}

Et son résultat :

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
C:\Users\Fabien\Desktop>test.exe
Usage: test.exe NOMBRE
Affiche le nombre passé en paramètre sur la sortie standard.

C:\Users\Fabien\Desktop>test.exe 42
Vous avez saisi : 42

C:\Users\Fabien\Desktop>test.exe -17
Vous avez saisi : -17

C:\Users\Fabien\Desktop>test.exe azerty
Erreur: Le paramètre NOMBRE doit être un nombre entier !

C:\Users\Fabien\Desktop>test.exe 0
Vous avez saisi : 0

C'est tout de suite beaucoup mieux, vous ne trouvez pas ;)

PS Il reste possible de saisir "42azerty" est d'obtenir 42 en sortie, sans générer d'erreur. Pour éviter cela, il suffit d'ajouter un caractère bidon en fin de format "%d". Si le caractère est extrait par sscanf, le if ne retournera pas 1 et boom, erreur. Exemple de code :

1
2
3
4
5
6
7
// Converti le texte en nombre
int valeur;
char c;
if (sscanf(argv[1], "%d%c", &valeur, &c) != 1) {
    puts("Erreur: Le paramètre NOMBRE doit être un nombre entier !");
    return 1;
}

Conclusion

N'utilisez pas les fonctions atoi(), atol(), atof(). Que se soit dans un programme PC ou embarqué.

Il n'y a aucun moyen de savoir si la saisie est erronée ou non. A jouer avec le feu, on finit toujours par se bruler. Si vous voulez convertir une chaine de caractères en nombre, il existe une bien meilleure solution.

De manière plus générale, utilisez uniquement des fonctions proposant un moyen de vérifier l'intégrité et la cohérence des résultats fournis. N'utilisez pas non plus des fonctions obsolètes, prenez le temps de regarder la documentation de la fonction pour voir si celle-ci est toujours d'usage.

Pour finir, ne faite jamais, ô grand jamais, confiance à l'utilisateur.