Yahoo refuse tous les emails du site. Si vous avez une adresse chez un autre prestataire, c'est le moment de l'utiliser
En cas de soucis, n'hésitez pas à aller faire un tour sur la page de contact en bas de page.
Topic "Pour l'amour du C, n'utilisez pas les fonctions atoi, atol, atof et dérivées"
Flux RSS des posts récents dans ce topic ( Flux Atom)
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.
#108 |
Ne pas utiliser les fonctions ato*
est une bonne chose, mais je ne suis pas certain que sscanf
soit une bonne alternative.
Il est bien plus simple d’utiliser strtol
ou strtod
.
#109 |
Il est bien plus simple d’utiliser
par Sanpistrtol
oustrtod
.
On m'a fait la même remarque sur Twitter. Je vais donc profiter du forum pour expliquer mon point de vue en plus de 140 caractères
Pour moi, les fonctions strtoX()
sont très pratiques quand on sait ce que l'on fait et qu'on lit la doc.
C'est aussi le cas des fonctions atoX()
, si il y a vérification de saisie en amont.
MAIS, cet article existe car justement, plus personne ne prend le temps de lire la doc avant de copier coller du code.
Avec strtod par exemple, pour détecter une erreur il y a deux solutions :
tester la valeur du pointeur (optionnel) de fin de chaîne de caractères,
tester la valeur de
errno
après l'appel à la fonction.
Dans le cas du pointeur de fin de chaîne de caractères, il faut déjà prendre le temps de l'utiliser, ce n'est pas un paramètre obligatoire. Il faut ensuite prendre bien soin de passer un pointeur sur le pointeur et pas le pointeur lui même. Il faut ensuite faire le test de la valeur du pointeur après l'appel.
Pour résumer, il faut faire ça :
1 2 3 4 5 6 7 | char* str = "Foobar";
char* endPtr;
double valeur = strtod(str, &endPtr);
if (endPtr == str) {
// Erreur ...
}
|
Pour un débutant ou un développeur en manque de café, ça va automatiquement se finir par un "la flemme" ou "un pointeur sur pointeur !? HEIN !?".
N.B. La façon dont la documentation explique la valeur du pointeur en cas d'erreur est si bien écrite que je suis incapable de vous dire si mon test si dessus gère des cas d'erreur comme "42azerty".
Dans le cas de errno
, il faut d'abord mettre errno
à zéro, faire l'appel, puis vérifier errno
.
Pour résumer, il faut faire ça :
1 2 3 4 5 6 7 | char* str = "Foobar";
errno = 0;
double valeur = strtod(str, NULL);
if (errno == ERANGE) {
// Erreur ...
}
|
Sauf que, pas de bol, suivant l'implémentation, errno
n'est pas forcément utilisé.
Sur Twitter par exemple, on me parlait de Nuttx (un RTOS pour microcontrôleurs ARM). Et bien dans Nuttx, errno
n'est pas utilisé.
Par conséquent, en suivant les bonnes pratiques, voila ce qu'il faudrait normalement faire :
1 2 3 4 5 6 7 8 | char* str = "Foobar";
char* endPtr;
errno = 0;
double valeur = strtod(str, &endPtr);
if (endPtr == str || errno == ERANGE) {
// Erreur ...
}
|
N.B. Il faudrait même tester valeur
en duo avec errno
pour vraiment bien faire.
Conclusion personnelle : oui, sscanf
est lourd, très lourd même. Mais l'API est simple, le test d'erreur est simple, ça marche avec tous les formats de données et n'importe quel développeur qui sait faire un printf
sait faire une sscanf
.
En toute franchise, vous vous voyez écrire ça :
1 2 3 4 5 6 7 | char* endPtr;
errno = 0;
long valeur = strtol(str, &endPtr, 10);
if (endPtr == str || errno == ERANGE) {
// Erreur ...
}
|
Ou ça :
1 2 3 4 5 6 | char c;
long valeur;
if (sscanf(str, "%ld%c", &valeur, &c) != 1) {
// Erreur ...
}
|
Pour moi, la seconde solution est beaucoup plus lisible et moins error-prone.
#110 |
Même avec toute la bonne volonté du monde, c'est pas encore ça :
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 | // Inclut la bibliothèque standard pour les entrées/sorties en mode terminal
#include <stdio.h>
#include <errno.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
char* str = argv[1];
char* endPtr;
errno = 0;
long valeur = strtol(str, &endPtr, 10);
if (endPtr == str || errno == ERANGE) {
puts("Erreur: Le paramètre NOMBRE doit être un nombre entier !");
return 1;
}
// Affiche le nombre passé en paramètre
printf("Vous avez saisi : %ld\n", valeur);
// Pas d'erreur
return 0;
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 0
Vous avez saisi : 0
C:\Users\Fabien\Desktop>test.exe azerty
Erreur: Le paramÞtre NOMBRE doit Ûtre un nombre entier !
C:\Users\Fabien\Desktop>test.exe 42azerty
Vous avez saisi : 42
|
Voila ce que je suis obligé de faire pour avoir le même comportement que "%ld%c" :
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 | // Inclut la bibliothèque standard pour les entrées/sorties en mode terminal
#include <stdio.h>
#include <string.h>
#include <errno.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
char* str = argv[1];
char* endPtr;
errno = 0;
long valeur = strtol(str, &endPtr, 10);
if (endPtr != (str + strlen(str)) || errno == ERANGE) {
puts("Erreur: Le paramètre NOMBRE doit être un nombre entier !");
return 1;
}
// Affiche le nombre passé en paramètre
printf("Vous avez saisi : %ld\n", valeur);
// Pas d'erreur
return 0;
}
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | 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 0
Vous avez saisi : 0
C:\Users\Fabien\Desktop>test.exe azerty
Erreur: Le paramÞtre NOMBRE doit Ûtre un nombre entier !
C:\Users\Fabien\Desktop>test.exe 42azerty
Erreur: Le paramÞtre NOMBRE doit Ûtre un nombre entier !
|
Si j'ai complètement raté un truc, qu'on me le dise, mais en l'état, je ne vois vraiment pas en quoi il est plus simple d'utiliser les fonctions strtoX()
.
#672 |
Je tombe un peu par hasard sur cet article et je ne suis pas d'accord pour 2 raisons
1/ Vous parlez d'un avantage de la fonction sscanf() mais en fait pour être sûr de ce que vous avez eu en retour, vous devez effectuer un test. donc pour être équitable vous devriez accepter un test avec atoi()
Voici un petit exemple Arduino avec 3 messages et un test qui vérifie si le résultat de l'analyse est 0 si c'est bien le cas (on regarde si le message commence par 0, on pourrait améliorer et vérifiant "-0")
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | char message1[] = "123";
char message2[] = "0";
char message3[] = "ooops";
int v;
void setup() {
Serial.begin(115200);
v = atoi(message1);
if ((v == 0) && (*message1 != '0')) Serial.println(F("Erreur de saisie"));
else Serial.println(v);
v = atoi(message2);
if ((v == 0) && (*message2 != '0')) Serial.println(F("Erreur de saisie"));
else Serial.println(v);
v = atoi(message3);
if ((v == 0) && (*message3 != '0')) Serial.println(F("Erreur de saisie"));
else Serial.println(v);
}
void loop() {}
|
On obtient bien en sotie sur la console série
123
0
Erreur de saisie
bien sûr avec votre approche on peut aussi écrire
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | char message1[] = "123";
char message2[] = "0";
char message3[] = "ooops";
int v;
void setup() {
Serial.begin(115200);
if (sscanf(message1, "%d", &v) != 1)Serial.println(F("Erreur de saisie"));
else Serial.println(v);
if (sscanf(message2, "%d", &v) != 1)Serial.println(F("Erreur de saisie"));
else Serial.println(v);
if (sscanf(message3, "%d", &v) != 1)Serial.println(F("Erreur de saisie"));
else Serial.println(v);
}
void loop() {}
|
qui donnera exactement la même sortie
123
0
Erreur de saisie
Donc du moment que l'on sait ce que l'on fait et que l'on teste il n'y a pas trop de soucis dans les 2 cas.
2/ MAIS sur un micro-controleur rajouter un seul appel à sscanf() rajoute l'intégralité de cette fonction complexe à notre code et augmente considérablement la mémoire programme…
Voici les informations de commpilatoin:
le premier programme ci dessus avec atoi()
Le croquis utilise 2140 octets (6%) de l'espace de stockage de programmes. Le maximum est de 32256 octets. Les variables globales utilisent 202 octets (9%) de mémoire dynamique, ce qui laisse 1846 octets pour les variables locales. Le maximum est de 2048 octets.
Le second programme avec sscanf()
Le croquis utilise 3902 octets (12%) de l'espace de stockage de programmes. Le maximum est de 32256 octets. Les variables globales utilisent 204 octets (9%) de mémoire dynamique, ce qui laisse 1844 octets pour les variables locales. Le maximum est de 2048 octets.
le simple appel à sscanf() a donc rajouté plus de 1.5 kilo-octets à notre programme. Suivant ce que vous faites comme code, c'est bcp pour quelque chose qui peut se resoudre en une ou quelques lignes de test.
Derniére modification le
#1020 |
Je reviens sur votre article très intéressant. Je suis sur mac. J'ai essayé de compiler votre code mais tel qu'il est, j'obtiens le message : > error: use of undeclared identifier 'atoi'
Ca ne marche que si je définis donc atoi avec : double const nombre(atoi(argv[1]));
Vous confirmez ?
#1092 |
> error: use of undeclared identifier 'atoi'
par supyel
Il faut inclure manuellement le header standard qui définit atoi()
avec #include <stdlib.h>
(C) ou #include <cstdlib>
(C++). L’IDE Arduino inclut par défaut la plupart de ces headers. Il est fortement déconseillé de déclarer le symbole soi-même, car la déclaration réelle peut inclure certains modificateurs qui sont susceptible de différer d’une bibliothèque à l’autre, voire d’une version à l’autre.