Différence entre scanf () et strtol () / strtod () dans l'analyse des nombres
-
07-07-2019 - |
Question
Remarque: j'ai complètement retravaillé la question pour mieux refléter ce pour quoi je fixe la prime. Veuillez excuser les incohérences avec les réponses déjà données que cela aurait pu créer. Je ne voulais pas créer une nouvelle question, car les réponses précédentes pourraient être utiles.
Je travaille sur l'implémentation d'une bibliothèque standard C et je suis confus au sujet d'un coin spécifique de la norme.
La norme définit les formats numériques acceptés par la famille de fonctions scanf
(% d,% i,% u,% o,% x) en termes de définitions pour strtol
, strtoul
et strtod
.
La norme indique également que fscanf ()
ne restituera qu'un maximum de caractères dans le flux d'entrée et que, par conséquent, certaines séquences acceptées par strtol
, strtoul
et strtod
sont inacceptables pour fscanf
(ISO / CEI 9899: 1999, note de bas de page 251).
J'ai essayé de trouver des valeurs présentant de telles différences. Il se trouve que le préfixe hexadécimal "0x", suivi d'un caractère qui n'est pas un chiffre hexadécimal, est un cas dans lequel les deux familles de fonctions diffèrent.
Assez drôle, il est devenu évident qu’il n’ya pas deux bibliothèques C disponibles qui ne semblent s’accorder sur le résultat. (Voir programme de test et exemple de sortie à la fin de cette question.)
Ce que j'aimerais entendre, c'est ce qui serait considéré comme un comportement conforme à la norme lors de l'analyse de "0xz"? . En citant idéalement les parties pertinentes de la norme pour faire valoir votre point de vue.
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
int main()
{
int i, count, rc;
unsigned u;
char * endptr = NULL;
char culprit[] = "0xz";
/* File I/O to assert fscanf == sscanf */
FILE * fh = fopen( "testfile", "w+" );
fprintf( fh, "%s", culprit );
rewind( fh );
/* fscanf base 16 */
u = -1; count = -1;
rc = fscanf( fh, "%x%n", &u, &count );
printf( "fscanf: Returned %d, result %2d, consumed %d\n", rc, u, count );
rewind( fh );
/* strtoul base 16 */
u = strtoul( culprit, &endptr, 16 );
printf( "strtoul: result %2d, consumed %d\n", u, endptr - culprit );
puts( "" );
/* fscanf base 0 */
i = -1; count = -1;
rc = fscanf( fh, "%i%n", &i, &count );
printf( "fscanf: Returned %d, result %2d, consumed %d\n", rc, i, count );
rewind( fh );
/* strtol base 0 */
i = strtol( culprit, &endptr, 0 );
printf( "strtoul: result %2d, consumed %d\n", i, endptr - culprit );
fclose( fh );
return 0;
}
/* newlib 1.14
fscanf: Returned 1, result 0, consumed 1
strtoul: result 0, consumed 0
fscanf: Returned 1, result 0, consumed 1
strtoul: result 0, consumed 0
*/
/* glibc-2.8
fscanf: Returned 1, result 0, consumed 2
strtoul: result 0, consumed 1
fscanf: Returned 1, result 0, consumed 2
strtoul: result 0, consumed 1
*/
/* Microsoft MSVC
fscanf: Returned 0, result -1, consumed -1
strtoul: result 0, consumed 0
fscanf: Returned 0, result 0, consumed -1
strtoul: result 0, consumed 0
*/
/* IBM AIX
fscanf: Returned 0, result -1, consumed -1
strtoul: result 0, consumed 1
fscanf: Returned 0, result 0, consumed -1
strtoul: result 0, consumed 1
*/
La solution
Communication avec Fred J. Tydeman, vice-président de PL22.11 (ANSI "C"), sur comp.std.c, a permis de mieux comprendre:
fscanf
Un élément d’entrée est défini comme étant le plus longue séquence de caractères d'entrée [...] qui est, ou est un préfixe, un séquence d'entrée correspondante. (7.19.6.2 P9)
Cela fait " 0x " la plus longue séquence qui est un préfixe d'une séquence d'entrée correspondante. (Même avec la conversion % i
, car l'hex "0x" est une séquence plus longue que la décimale "0".)
Le premier caractère, le cas échéant, après le l'élément d'entrée reste non lu. (7.19.6.2 P9)
Ceci permet à fscanf
de lire le "z", et de le replacer comme non concordant (en respectant la limite de repoussement à un caractère de la note de bas de page 251)).
Si l'élément en entrée ne correspond pas séquence, l'exécution du directive échoue: cette condition est un échec correspondant. (7.19.6.2 P10)
Cela fait " 0x " échec de la correspondance, c'est-à-dire que fscanf
ne doit attribuer aucune valeur, renvoyer zéro (si le % x
ou le % i
était le premier spécificateur de conv.), et laissez " z " en tant que premier caractère non lu du flux d'entrée.
strtol
La définition de strtol
(et strtoul
) diffère en un point crucial:
La séquence de sujet est définie comme étant la la plus longue sous-séquence initiale du chaîne d'entrée, en commençant par la première caractère non-blanc, qui est de la forme attendue . (7.20.1.4 P4, c'est moi qui souligne)
Ce qui signifie que strtol
doit rechercher la séquence la plus longue valide , dans ce cas le "0". Il doit pointer endptr
sur "x" et renvoyer zéro comme résultat.
Autres conseils
Je ne crois pas que l'analyse soit autorisée à produire des résultats différents. La référence Plaugher indique simplement que l'implémentation strtol ()
pourrait être une version différente, plus efficace, car elle dispose d'un accès complet à la chaîne entière.
Selon la spécification C99, la famille de fonctions scanf ()
analyse les entiers de la même manière que la famille de fonctions strto * ()
. Par exemple, pour le spécificateur de conversion x
, ceci lit:
Correspond à un éventuellement signé nombre entier hexadécimal, dont le format est le même que prévu pour le sujet séquence de la fonction
strtoul
avec la valeur 16 pour l'argumentbase
.
Donc si sscanf ()
et strtoul ()
donnent des résultats différents, l'implémentation de libc n'est pas conforme.
Quels sont les résultats attendus de votre exemple le code devrait être un peu flou, cependant:
strtoul ()
accepte un préfixe facultatif 0x
ou 0X
si base
est 16
, et la spécification lit
La séquence de sujet est définie comme étant la la plus longue sous-séquence initiale du chaîne d'entrée, en commençant par la première caractère non-blanc, c'est-à-dire la forme attendue.
Pour la chaîne "0xz"
, la sous-séquence initiale la plus longue de la forme attendue est "0"
; la valeur doit donc être 0
et l'argument endptr
doivent être définis sur x
.
mingw-gcc 4.4.0 n'est pas d'accord et ne parvient pas à analyser la chaîne avec strtoul ()
et sscanf ()
. Le raisonnement pourrait être que la sous-séquence initiale la plus longue de la forme attendue est "0x"
- ce qui n'est pas un littéral entier valide. Aucun analyse n'est donc effectuée.
Je pense que cette interprétation de la norme est fausse: une sous-séquence de la forme attendue devrait toujours donner une valeur entière valide (si elle est hors limites, les valeurs MIN
/ MAX
sont renvoyés et errno
est défini sur ERANGE
).
cygwin-gcc 3.4.4 (qui utilise newlib autant que je sache) n'analysera pas non plus le littéral si strtoul ()
est utilisé, mais analyse la chaîne en fonction de mon interprétation de la norme avec sscanf ()
.
Attention, mon interprétation de la norme est sujette à votre problème initial, c’est-à-dire que la norme ne garantit que de pouvoir ungetc ()
une fois. Pour décider si le 0x
fait partie du littéral, vous devez lire deux caractères à l'avance: le x
et le caractère suivant. Si ce n'est pas un caractère hexagonal, ils doivent être repoussés. S'il y a plus de jetons à analyser, vous pouvez les tamponner et contourner ce problème, mais s'il s'agit du dernier jeton, vous devez ungetc ()
les deux caractères.
Je ne suis pas vraiment sûr de ce que fscanf ()
devrait faire si ungetc ()
échouait. Peut-être juste définir l'indicateur d'erreur du flux?
Pour résumer ce qui devrait se produire selon la norme lors de l'analyse des nombres:
- si
fscanf ()
réussit, le résultat doit être identique à celui obtenu viastrto * ()
-
contrairement à
strto * ()
,fscanf ()
échoue sila plus longue séquence de caractères d'entrée [...] qui est ou est le préfixe d'une séquence d'entrée correspondante
selon la définition de
fscanf ()
n'est pasla sous-séquence initiale la plus longue [...] de la forme attendue
selon la définition de
strto * ()
C’est un peu moche, mais une conséquence nécessaire de l’exigence selon laquelle fscanf ()
doit être gourmand, mais ne peut pas repousser plus d’un caractère.
Certains implémenteurs de bibliothèques ont opté pour un comportement différent. A mon avis
- laisser
strto * ()
ne pas rendre les résultats cohérents est stupide ( bad mingw ) - repoussant plusieurs caractères pour que
fscanf ()
accepte toutes les valeurs acceptées parstrto * ()
, mais est justifié ( hourra pour newlib s'ils n'ont pas botterstrto * ()
: () - ne pas repousser les caractères qui ne correspondent pas, mais seulement analyser ceux de "forme attendue" semble douteux alors que les caractères disparaissent dans les airs ( bad glibc )
Je ne suis pas sûr de comprendre la question, mais pour une chose, scanf () est supposé gérer EOF. scanf () et strtol () sont différents types de bêtes. Peut-être devriez-vous comparer strtol () et sscanf () à la place?
Je ne suis pas sûr que la mise en œuvre de scanf () puisse être liée à ungetc (). scanf () peut utiliser tous les octets du tampon de flux. ungetc () pousse simplement un octet à la fin du tampon et le décalage est également modifié.
scanf("%d", &x);
ungetc('9', stdin);
scanf("%d", &y);
printf("%d, %d\n", x, y);
Si l'entrée est "100", la sortie est "100, 9". Je ne vois pas comment scanf () et ungetc () pourraient interférer. Désolé si j'ai ajouté un commentaire naïf.
Pour la saisie dans les fonctions scanf () ainsi que pour les fonctions strtol () , en Sec. 7.20.1.4 P7 indique: Si la séquence de sujet est vide ou ne présente pas la forme attendue, aucune conversion n'est effectuée. la valeur de nptr est stockée dans l'objet pointé par endptr, à condition que endptr ne soit pas un pointeur nul . Vous devez également tenir compte des règles d'analyse des jetons définis dans les règles de Sec. 6.4.4 Constantes , règle indiquée en Sec. 7.20.1.4 P5 .
Le reste du comportement, tel que la valeur errno , doit être spécifique à la mise en oeuvre. Par exemple, sur ma machine FreeBSD, les valeurs EINVAL et ERANGE sont utilisées. Sous Linux, il en va de même, où les référents standard se réfèrent uniquement à la ERANGE . .
Réponse obsolète après la réécriture de la question. Quelques liens intéressants dans les commentaires cependant.
En cas de doute, écrivez un test. - proverbe
Après avoir testé toutes les combinaisons de spécificateurs de conversion et de variations d'entrée auxquelles je pouvais penser, je peux dire qu'il est correct que les deux familles de fonctions ne donnent pas des résultats identiques . (Au moins dans la glibc, qui est ce que j'ai disponible pour les tests.)
La différence apparaît lorsque trois circonstances se rencontrent:
- Vous utilisez
"% i"
ou"% x"
(en autorisant la saisie hexadécimale). - L'entrée contient le préfixe hexadécimal
"0x"
(facultatif). - Il n'y a pas de chiffre hexadécimal valide après le préfixe hexadécimal.
Exemple de code:
#include <stdio.h>
#include <stdlib.h>
int main()
{
char * string = "0xz";
unsigned u;
int count;
char c;
char * endptr;
sscanf( string, "%x%n%c", &i, &count, &c );
printf( "Value: %d - Consumed: %d - Next char: %c - (sscanf())\n", u, count, c );
i = strtoul( string, &endptr, 16 );
printf( "Value: %d - Consumed: %td - Next char: %c - (strtoul())\n", u, ( endptr - string ), *endptr );
return 0;
}
Sortie:
Value: 0 - Consumed: 1 - Next char: x - (sscanf())
Value: 0 - Consumed: 0 - Next char: 0 - (strtoul())
Cela me trouble. Évidemment, sscanf ()
ne sera pas sauvegardé au 'x'
, sinon il ne serait pas en mesure d'analyser aucun " Hexadécimaux préfixés par 0x "
". Donc, il a lu le 'z'
et l'a trouvé non correspondant. Mais il décide d’utiliser uniquement le <0>
en tant que valeur. Cela signifierait repousser le 'z'
et le 'x'
. (Oui, je sais que sscanf ()
, que j'ai utilisé ici pour faciliter les tests, ne fonctionne pas sur un flux, mais je suppose fortement qu'ils ont créé tous les ... scanf ()
les fonctions se comportent de manière identique par souci de cohérence.)
Donc ... un caractère ungetc ()
n'est pas vraiment la raison, ici ...?: - /
Oui, les résultats diffèrent . Je ne peux toujours pas l'expliquer correctement, cependant ...: - (