J'ai très souvent vu des gens décourager les autres d'utiliser scanf
et dire qu'il existe de meilleures alternatives. Cependant, tout ce que je vois est soit "n'utilisez pas scanf
" ou "voici une chaîne de format correcte", et jamais aucun exemple de la "meilleures alternatives" mentionné.
Par exemple, prenons cet extrait de code:
scanf("%c", &c);
Cela lit l'espace blanc qui a été laissé dans le flux d'entrée après la dernière conversion. La solution habituelle suggérée est d'utiliser:
scanf(" %c", &c);
ou pour ne pas utiliser scanf
.
Puisque scanf
est mauvais, quelles sont les options C ANSI pour convertir les formats d'entrée que scanf
peut généralement gérer (tels que les entiers, les nombres à virgule flottante et les chaînes) sans utiliser scanf
?
Les façons les plus courantes de lire une entrée sont les suivantes:
en utilisant fgets
avec une taille fixe, ce qui est généralement suggéré, et
en utilisant fgetc
, ce qui peut être utile si vous ne lisez qu'un seul char
.
Pour convertir l'entrée, il existe une variété de fonctions que vous pouvez utiliser:
strtoll
, pour convertir une chaîne en entier
strtof
/d
/ld
, pour convertir une chaîne en nombre à virgule flottante
sscanf
, ce qui n'est pas comme mauvais en utilisant simplement scanf
, bien qu'il présente la plupart des inconvénients mentionnés ci-dessous
Il n'y a pas de bonne façon d'analyser une entrée séparée par un délimiteur en ANSI C. ordinaire. Utilisez soit strtok_r
De POSIX ou strtok
, qui n'est pas thread-safe. Vous pouvez également lancer la vôtre variante thread-safe en utilisant strcspn
et strspn
, car strtok_r
N'implique aucune prise en charge spéciale du système d'exploitation.
Cela peut être exagéré, mais vous pouvez utiliser des lexers et des analyseurs (flex
et bison
étant les exemples les plus courants).
Aucune conversion, utilisez simplement la chaîne
Puisque je ne suis pas entré exactement dans pourquoiscanf
est mauvais dans ma question, je vais élaborer:
Avec les spécificateurs de conversion %[...]
Et %c
, scanf
ne consomme pas d'espace. Ceci n'est apparemment pas largement connu, comme en témoignent les nombreux doublons de cette question .
Il existe une certaine confusion quant au moment d'utiliser l'opérateur unaire &
Lors de la référence aux arguments de scanf
(en particulier avec les chaînes).
Il est très facile d'ignorer la valeur de retour de scanf
. Cela pourrait facilement provoquer un comportement non défini lors de la lecture d'une variable non initialisée.
Il est très facile d'oublier d'empêcher le débordement de tampon dans scanf
. scanf("%s", str)
est tout aussi mauvais, sinon pire, que gets
.
Vous ne pouvez pas détecter de débordement lors de la conversion d'entiers avec scanf
. En fait, le débordement provoque comportement indéfini dans ces fonctions.
scanf
est-il mauvais?Le principal problème est que scanf
n'a jamais été conçu pour gérer les entrées utilisateur. Il est destiné à être utilisé avec des données formatées "parfaitement". J'ai cité le mot "parfaitement" parce qu'il n'est pas complètement vrai. Mais il n'est pas conçu pour analyser des données aussi peu fiables que les entrées utilisateur. Par nature, la saisie de l'utilisateur n'est pas prévisible. Les utilisateurs comprennent mal les instructions, font des fautes de frappe, appuient accidentellement sur Entrée avant d'avoir terminé, etc. On peut raisonnablement se demander pourquoi une fonction qui ne devrait pas être utilisée pour les entrées utilisateur lit à partir de stdin
. Si vous êtes un utilisateur expérimenté de * nix, l'explication ne vous surprendra pas, mais cela pourrait dérouter les utilisateurs de Windows. Dans les systèmes * nix, il est très courant de créer des programmes qui fonctionnent via la tuyauterie, ce qui signifie que vous envoyez la sortie d'un programme à un autre en canalisant le stdout
du premier programme vers le stdin
de la seconde. De cette façon, vous pouvez vous assurer que la sortie et l'entrée sont prévisibles. Dans ces circonstances, scanf
fonctionne réellement bien. Mais lorsque vous travaillez avec une entrée imprévisible, vous risquez toutes sortes de problèmes.
Alors, pourquoi n'y a-t-il pas de fonctions standard faciles à utiliser pour la saisie par l'utilisateur? On ne peut que deviner ici, mais je suppose que les vieux hackers hardcore C pensaient simplement que les fonctions existantes étaient assez bonnes, même si elles sont très maladroites. En outre, lorsque vous regardez des applications de terminal typiques, elles lisent très rarement les entrées utilisateur de stdin
. Le plus souvent, vous passez toutes les entrées utilisateur comme arguments de ligne de commande. Bien sûr, il existe des exceptions, mais pour la plupart des applications, la saisie des utilisateurs est une chose très mineure.
Mon préféré est fgets
en combinaison avec sscanf
. J'ai écrit une fois une réponse à ce sujet, mais je publierai à nouveau le code complet. Voici un exemple avec une vérification et une analyse des erreurs décentes (mais pas parfaites). C'est assez bon pour le débogage.
Remarque
Je n'aime pas particulièrement demander à l'utilisateur de saisir deux choses différentes sur une seule ligne. Je ne fais cela que lorsqu'ils appartiennent les uns aux autres de manière naturelle. Comme par exemple
printf("Enter the price in the format <dollars>.<cent>: ")
puis utilisezsscanf(buffer "%d.%d", &dollar, ¢)
. Je ne ferais jamais quelque chose commeprintf("Enter height and base of the triangle: ")
. Le point principal de l'utilisation defgets
ci-dessous est d'encapsuler les entrées pour s'assurer qu'une entrée n'affecte pas la suivante.
#define bsize 100
void error_function(const char *buffer, int no_conversions) {
fprintf(stderr, "An error occurred. You entered:\n%s\n", buffer);
fprintf(stderr, "%d successful conversions", no_conversions);
exit(EXIT_FAILURE);
}
char c, buffer[bsize];
int x,y;
float f, g;
int r;
printf("Enter two integers: ");
fflush(stdout); // Make sure that the printf is executed before reading
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);
// Unless the input buffer was to small we can be sure that stdin is empty
// when we come here.
printf("Enter two floats: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) error_function(buffer, r);
// Reading single characters can be especially tricky if the input buffer
// is not emptied before. But since we're using fgets, we're safe.
printf("Enter a char: ");
fflush(stdout);
if(! fgets(buffer, bsize, stdin)) error_function(buffer, 0);
if((r = sscanf(buffer, "%c", &c)) != 1) error_function(buffer, r);
printf("You entered %d %d %f %c\n", x, y, f, c);
Faire cela éliminera un problème commun, qui est la nouvelle ligne de fin qui peut perturber l'entrée du nid. Mais il a un autre problème, qui est si la ligne est plus longue que bsize
. Vous pouvez le vérifier avec if(buffer[strlen(buffer)-1] != '\n')
. Si vous souhaitez supprimer la nouvelle ligne, vous pouvez le faire avec buffer[strcspn(buffer, "\n")] = 0
.
En général, je vous conseille de ne pas vous attendre à ce que l'utilisateur entre des entrées dans un format étrange que vous devriez analyser en fonction de différentes variables. Si vous souhaitez affecter les variables height
et width
, ne demandez pas les deux en même temps. Autorisez l'utilisateur à appuyer sur Entrée entre eux. De plus, cette approche est très naturelle dans un sens. Vous n'obtiendrez jamais l'entrée de stdin
jusqu'à ce que vous appuyiez sur Entrée, alors pourquoi ne pas toujours lire la ligne entière? Bien sûr, cela peut toujours entraîner des problèmes si la ligne est plus longue que le tampon. Ai-je pensé à mentionner que l'entrée utilisateur est maladroite en C? :)
Pour éviter des problèmes avec des lignes plus longues que le tampon, vous pouvez utiliser une fonction qui alloue automatiquement un tampon de taille appropriée, vous pouvez utiliser getline()
. L'inconvénient est que vous devrez free
le résultat par la suite.
Si vous êtes sérieux au sujet de la création de programmes en C avec une entrée utilisateur, je recommanderais de jeter un œil à une bibliothèque comme ncurses
. Parce qu'alors vous voudrez probablement aussi créer des applications avec des graphiques de terminal. Malheureusement, vous perdrez une certaine portabilité si vous le faites, mais cela vous donne un bien meilleur contrôle des entrées utilisateur. Par exemple, il vous donne la possibilité de lire une pression de touche instantanément au lieu d'attendre que l'utilisateur appuie sur Entrée.
scanf
est génial quand vous connaissez votre entrée est toujours bien structurée et bien comportée. Autrement...
OMI, voici les plus gros problèmes avec scanf
:
Risque de dépassement de tampon - si vous ne spécifiez pas de largeur de champ pour le %s
et %[
spécificateurs de conversion, vous risquez un débordement de la mémoire tampon (en essayant de lire plus d'entrées qu'une mémoire tampon est dimensionnée pour contenir). Malheureusement, il n'y a pas de bon moyen de spécifier cela comme argument (comme avec printf
) - vous devez soit le coder en dur dans le cadre du spécificateur de conversion, soit effectuer des manigances de macro.
Accepte les entrées qui devraient être rejetées - Si vous lisez une entrée avec le %d
spécificateur de conversion et vous tapez quelque chose comme 12w4
, vous vous attendriez à ce que scanf
rejette cette entrée, mais ce n'est pas le cas - elle convertit et attribue avec succès le 12
, en quittant w4
dans le flux d'entrée pour salir la lecture suivante.
Alors, que devez-vous utiliser à la place?
Je recommande généralement de lire toutes les entrées interactives sous forme de texte en utilisant fgets
- cela vous permet de spécifier un nombre maximum de caractères à lire à la fois , afin que vous puissiez facilement empêcher le débordement de la mémoire tampon:
char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
// error reading from input stream, handle as appropriate
}
else
{
// process input buffer
}
Une particularité de fgets
est qu'il stockera la nouvelle ligne de fin dans le tampon s'il y a de la place, vous pouvez donc faire une vérification facile pour voir si quelqu'un a tapé plus de données que vous attendiez:
char *newline = strchr( input, '\n' );
if ( !newline )
{
// input longer than we expected
}
La façon dont vous gérez cela dépend de vous - vous pouvez soit rejeter l'entrée entière d'emblée, soit extraire toute entrée restante avec getchar
:
while ( getchar() != '\n' )
; // empty loop
Ou vous pouvez traiter l'entrée que vous avez obtenue jusqu'à présent et relire. Cela dépend du problème que vous essayez de résoudre.
Pour tokeniser l'entrée (la diviser en fonction d'un ou plusieurs délimiteurs), vous pouvez utiliser strtok
, mais attention - strtok
modifie son entrée (il écrase les délimiteurs avec le terminateur de chaîne) et vous ne pouvez pas conserver son état (c'est-à-dire que vous ne pouvez pas tokeniser partiellement une chaîne, puis commencer à tokeniser une autre, puis reprendre là où vous vous étiez arrêté dans la chaîne d'origine). Il y a une variante, strtok_s
, qui préserve l'état du tokenizer, mais AFAIK son implémentation est facultative (vous devrez vérifier que __STDC_LIB_EXT1__
est défini pour voir s'il est disponible).
Une fois que vous avez symbolisé votre entrée, si vous devez convertir des chaînes en nombres (par exemple, "1234"
=> 1234
), vous avez des options. strtol
et strtod
convertira les représentations sous forme de chaîne d'entiers et de nombres réels en leurs types respectifs. Ils vous permettent également d'attraper le 12w4
problème que j'ai mentionné ci-dessus - l'un de leurs arguments est un pointeur sur le premier caractère non converti dans la chaîne:
char *text = "12w4";
char *chk;
long val;
long tmp = strtol( text, &chk, 10 );
if ( !isspace( *chk ) && *chk != 0 )
// input is not a valid integer string, reject the entire input
else
val = tmp;
Dans cette réponse, je vais supposer que vous lisez et interprétez des lignes de texte . Peut-être que vous invitez l'utilisateur, qui tape quelque chose et appuie sur RETOUR. Ou peut-être que vous lisez des lignes de texte structuré à partir d'un fichier de données quelconque.
Puisque vous lisez des lignes de texte, il est logique d'organiser votre code autour d'une fonction de bibliothèque qui lit, eh bien, une ligne de texte. La fonction standard est fgets()
, bien qu'il y en ait d'autres (y compris getline
). Et puis l'étape suivante consiste à interpréter cette ligne de texte d'une manière ou d'une autre.
Voici la recette de base pour appeler fgets
pour lire une ligne de texte:
char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);
Cela lit simplement une ligne de texte et l'imprime. Tel qu'il est écrit, il a quelques limitations, que nous verrons dans une minute. Il a également une très grande fonctionnalité: ce nombre 512 que nous avons passé comme deuxième argument à fgets
est la taille du tableau line
que nous demandons à fgets
de lire. Ce fait - que nous pouvons dire à fgets
combien il est autorisé à lire - signifie que nous pouvons être sûrs que fgets
ne débordera pas le tableau en y lisant trop.
Alors maintenant, nous savons lire une ligne de texte, mais que faire si nous voulions vraiment lire un entier, ou un nombre à virgule flottante, ou un seul caractère, ou un seul mot? (C'est-à-dire, si l'appel scanf
que nous essayons d'améliorer avait utilisé un spécificateur de format comme %d
, %f
, %c
, Ou %s
?)
Il est facile de réinterpréter une ligne de texte - une chaîne - comme n'importe laquelle de ces choses. Pour convertir une chaîne en entier, la façon la plus simple (mais imparfaite) de le faire est d'appeler atoi()
. Pour convertir en nombre à virgule flottante, il y a `atof (). (Et il y a aussi de meilleures façons, comme nous le verrons dans une minute.) Voici un exemple très simple:
printf("type an integer:\n");
fgets(line, 512, stdin);
int i = atoi(line);
printf("type a floating-point number:\n");
fgets(line, 512, stdin);
float f = atof(line);
printf("you typed %d and %f\n", i, f);
Si vous vouliez que l'utilisateur tape un seul caractère (peut-être y
ou n
comme réponse oui/non), vous pouvez littéralement simplement saisir le premier caractère de la ligne, comme ceci:
printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);
(Cela ignore, bien sûr, la possibilité que l'utilisateur tape une réponse à plusieurs caractères; il ignore discrètement tous les caractères supplémentaires qui ont été saisis.)
Enfin, si vous vouliez que l'utilisateur tape définitivement une chaîne ne contenant pas d'espace, si vous vouliez traiter la ligne d'entrée
hello world!
comme la chaîne "hello"
suivie par autre chose (ce que le format scanf
%s
aurait fait), eh bien, dans ce cas, j'ai un peu gratté, ce n'est pas tout à fait si facile de réinterpréter la ligne de cette façon, après tout, donc la réponse à cette partie de la question devra attendre un peu.
Mais je veux d'abord revenir sur trois choses que j'ai ignorées.
(1) Nous avons appelé
fgets(line, 512, stdin);
à lire dans le tableau line
, et où 512 est la taille du tableau line
pour que fgets
sache ne pas le déborder. Mais pour vous assurer que 512 est le bon nombre (en particulier, pour vérifier si quelqu'un a peut-être modifié le programme pour changer la taille), vous devez relire où line
a été déclaré. C'est une nuisance, il existe donc deux façons bien meilleures de synchroniser les tailles. (a) utiliser le préprocesseur pour faire un nom pour la taille:
#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);
Ou, (b) utilisez l'opérateur sizeof
de C:
fgets(line, sizeof(line), stdin);
(2) Le deuxième problème est que nous n'avons pas recherché d'erreur. Lorsque vous lisez une entrée, vous devez toujours vérifier la possibilité d'erreur. Si, pour une raison quelconque, fgets
ne peut pas lire la ligne de texte que vous lui avez demandée, il l'indique en renvoyant un pointeur nul. Nous aurions donc dû faire des choses comme
printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
printf("Well, never mind, then.\n");
exit(1);
}
Enfin, il y a le problème que pour lire une ligne de texte, fgets
lit les caractères et les remplit dans votre tableau jusqu'à ce qu'il trouve le caractère \n
Qui termine la ligne, et il remplit également le caractère \n
dans votre tableau . Vous pouvez le voir si vous modifiez légèrement notre exemple précédent:
printf("you typed: \"%s\"\n", line);
Si je lance ceci et tape "Steve" quand il me le demande, il s'imprime
you typed: "Steve
"
Ce "
Sur la deuxième ligne est dû au fait que la chaîne lue et imprimée était en fait "Steve\n"
.
Parfois, cette nouvelle ligne supplémentaire n'a pas d'importance (comme lorsque nous avons appelé atoi
ou atof
, car ils ignorent tous les deux les entrées non numériques supplémentaires après le numéro), mais parfois cela compte beaucoup. Si souvent, nous voulons supprimer cette nouvelle ligne. Il y a plusieurs façons de le faire, que j'aborderai dans une minute. (Je sais que j'ai souvent dit cela. Mais je reviendrai à toutes ces choses, je le promets.)
À ce stade, vous pensez peut-être: "Je pensais que vous avez dit que scanf
n'était pas bon, et cette autre façon serait tellement mieux. Mais fgets
commence à ressembler à une nuisance. scanf
était si facile ! Je ne peux pas continuer à l'utiliser? "
Bien sûr, vous pouvez continuer à utiliser scanf
, si vous le souhaitez. (Et pour vraiment des choses simples, à certains égards, c'est plus simple.) Mais, s'il vous plaît, ne venez pas me pleurer quand il vous échoue à cause de l'une de ses 17 bizarreries et faiblesses, ou entre dans une boucle infinie à cause d'une entrée à laquelle vous ne vous attendiez pas, ou lorsque vous ne savez pas comment l'utiliser pour faire quelque chose de plus compliqué. Et regardons les nuisances réelles de fgets
:
Vous devez toujours spécifier la taille du tableau. Eh bien, bien sûr, ce n'est pas du tout une nuisance - c'est une fonctionnalité, car le débordement de tampon est vraiment une mauvaise chose.
Vous devez vérifier la valeur de retour. En fait, c'est un lavage, car pour utiliser scanf
correctement, vous devez également vérifier sa valeur de retour.
Vous devez retirer le \n
. C'est, je l'avoue, une véritable nuisance. J'aurais aimé qu'il y ait une fonction standard sur laquelle je pourrais vous indiquer qui n'a pas eu ce petit problème. (S'il vous plaît, personne n'évoque gets
.) Mais par rapport à scanf's
17 nuisances différentes, je prendrai celle-ci de fgets
n'importe quel jour.
Alors, comment supprimez-vous cette nouvelle ligne? Trois façons:
(a) Manière évidente:
char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';
(b) Façon délicate et compacte:
strtok(line, "\n");
Malheureusement, celui-ci ne fonctionne pas toujours.
(c) Une autre manière compacte et légèrement obscure:
line[strcspn(line, "\n")] = '\0';
Et maintenant que c'est terminé, nous pouvons revenir à une autre chose que j'ai ignorée les imperfections de atoi()
et atof()
. Le problème avec ceux-ci est qu'ils ne vous donnent aucune indication utile de réussite ou d'échec: ils ignorent tranquillement l'entrée non numérique de fin et ils retournent tranquillement 0 s'il n'y a pas d'entrée numérique du tout. Les alternatives préférées - qui présentent également certains autres avantages - sont strtol
et strtod
. strtol
vous permet également d'utiliser une base autre que 10, ce qui signifie que vous pouvez obtenir l'effet (entre autres) de %o
ou %x
avec scanf
. Mais montrer comment utiliser ces fonctions correctement est une histoire en soi et serait trop distraire de ce qui se transforme déjà en un récit assez fragmenté, donc je ne vais pas en dire plus à ce sujet maintenant.
Le reste de la narration principale concerne les entrées que vous essayez d'analyser, c'est plus compliqué qu'un simple chiffre ou caractère. Que faire si vous voulez lire une ligne contenant deux nombres, ou plusieurs mots séparés par des espaces, ou une ponctuation de cadrage spécifique? C'est là que les choses deviennent intéressantes, et où les choses se compliquaient probablement si vous essayiez de faire des choses en utilisant scanf
, et où il y a beaucoup plus d'options maintenant que vous avez lu proprement une ligne de texte en utilisant fgets
, bien que l'histoire complète de toutes ces options puisse probablement remplir un livre, nous ne pourrons donc que gratter la surface ici.
Ma technique préférée est de diviser la ligne en "mots" séparés par des espaces, puis de faire quelque chose de plus avec chaque "mot". Une fonction standard principale pour ce faire est strtok
(qui a aussi ses problèmes, et qui évalue également toute une discussion séparée). Ma propre préférence est une fonction dédiée pour construire un tableau de pointeurs vers chaque "mot" séparé, une fonction que je décris dans ces notes de cours . Quoi qu'il en soit, une fois que vous avez des "mots", vous pouvez continuer à les traiter, peut-être avec les mêmes atoi
/atof
/strtol
/strtod
fonctions que nous avons déjà examinées.
Paradoxalement, même si nous avons passé pas mal de temps et d'efforts ici à trouver comment s'éloigner de scanf
, une autre bonne façon de gérer la ligne de texte que nous venons de lire avec fgets
est de le passer à sscanf
. De cette façon, vous vous retrouvez avec la plupart des avantages de scanf
, mais sans la plupart des inconvénients.
Si votre syntaxe d'entrée est particulièrement compliquée, il peut être approprié d'utiliser une bibliothèque "regexp" pour l'analyser.
Enfin, vous pouvez utiliser les solutions d'analyse ad hoc qui vous conviennent. Vous pouvez parcourir la ligne un caractère à la fois avec un pointeur char *
Vérifiant les caractères que vous attendez. Ou vous pouvez rechercher des caractères spécifiques à l'aide de fonctions telles que strchr
ou strrchr
, ou strspn
ou strcspn
, ou strpbrk
. Ou vous pouvez analyser/convertir et ignorer des groupes de caractères numériques à l'aide des fonctions strtol
ou strtod
que nous avons ignorées précédemment.
Il y a évidemment beaucoup plus à dire, mais j'espère que cette introduction vous aidera à démarrer.
Que puis-je utiliser pour analyser une entrée au lieu de scanf?
Au lieu de scanf(some_format, ...)
, considérez fgets()
avec sscanf(buffer, some_format_and %n, ...)
En utilisant " %n"
, le code peut simplement détecter si tous le format a été scanné avec succès et qu'aucune jonque non-espace supplémentaire n'était à la fin.
// scanf("%d %f fred", &some_int, &some_float);
#define EXPECTED_LINE_MAX 100
char buffer[EXPECTED_LINE_MAX * 2]; // Suggest 2x, no real need to be stingy.
if (fgets(buffer, sizeof buffer, stdin)) {
int n = 0;
// add -------------> " %n"
sscanf(buffer, "%d %f fred %n", &some_int, &some_float, &n);
// Did scan complete, and to the end?
if (n > 0 && buffer[n] == '\0') {
// success, use `some_int, some_float`
} else {
; // Report bad input and handle desired.
}
Disons les exigences de l'analyse comme:
une entrée valide doit être acceptée (et convertie sous une autre forme)
une entrée non valide doit être rejetée
lorsqu'une entrée est rejetée, il est nécessaire de fournir à l'utilisateur un message descriptif qui explique (en clair "facilement compréhensible par des personnes normales qui ne sont pas programmeurs") pourquoi il a été rejeté (afin que les gens puissent comprendre comment résoudre le problème)
Pour garder les choses très simples, considérons l'analyse d'un seul entier décimal simple (qui a été tapé par l'utilisateur) et rien d'autre. Les raisons possibles du rejet de la saisie de l'utilisateur sont les suivantes:
Définissons également correctement "l'entrée contient des caractères inacceptables"; et dire que:
À partir de cela, nous pouvons déterminer que les messages d'erreur suivants sont nécessaires:
De ce point, nous pouvons voir qu'une fonction appropriée pour convertir une chaîne en un entier devrait faire la distinction entre des types d'erreurs très différents; et que quelque chose comme "scanf()
" ou "atoi()
" ou "strtoll()
" est complètement et totalement sans valeur parce qu'ils ne vous donnent aucune indication de ce qui n'allait pas avec l'entrée (et utilisez une définition complètement hors de propos et inappropriée de ce qui est/n'est pas une "entrée valide").
Au lieu de cela, commençons à écrire quelque chose qui n'est pas inutile:
char *convertStringToInteger(int *outValue, char *string, int minValue, int maxValue) {
return "Code not implemented yet!";
}
int main(int argc, char *argv[]) {
char *errorString;
int value;
if(argc < 2) {
printf("ERROR: No command line argument.\n");
return EXIT_FAILURE;
}
errorString = convertStringToInteger(&value, argv[1], -10, 2000);
if(errorString != NULL) {
printf("ERROR: %s\n", errorString);
return EXIT_FAILURE;
}
printf("SUCCESS: Your number is %d\n", value);
return EXIT_SUCCESS;
}
Pour répondre aux exigences énoncées, cette fonction convertStringToInteger()
finira probablement par représenter plusieurs centaines de lignes de code à elle seule.
Maintenant, c'était juste "l'analyse d'un seul entier décimal simple". Imaginez si vous vouliez analyser quelque chose de complexe; comme une liste de structures "nom, adresse, numéro de téléphone, adresse e-mail"; ou peut-être comme un langage de programmation. Dans ces cas, vous devrez peut-être écrire des milliers de lignes de code pour créer une analyse qui n'est pas une blague paralysée.
En d'autres termes...
Que puis-je utiliser pour analyser une entrée au lieu de scanf?
Écrivez vous-même (potentiellement des milliers de lignes) de code, selon vos besoins.
Voici un exemple d'utilisation de flex
pour analyser une entrée simple, dans ce cas un fichier de ASCII nombres à virgule flottante qui pourraient se trouver dans l'un ou l'autre des États-Unis (n,nnn.dd
) ou européen (n.nnn,dd
) formats. Ceci est juste copié à partir d'un programme beaucoup plus vaste, il peut donc y avoir des références non résolues:
/* This scanner reads a file of numbers, expecting one number per line. It */
/* allows for the use of European-style comma as decimal point. */
%{
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#ifdef WINDOWS
#include <io.h>
#endif
#include "Point.h"
#define YY_NO_UNPUT
#define YY_DECL int f_Lex (double *val)
double atofEuro (char *);
%}
%option prefix="f_"
%option nounput
%option noinput
EURONUM [-+]?[0-9]*[,]?[0-9]+([eE][+-]?[0-9]+)?
NUMBER [-+]?[0-9]*[\.]?[0-9]+([eE][+-]?[0-9]+)?
WS [ \t\x0d]
%%
[!@#%&*/].*\n
^{WS}*{EURONUM}{WS}* { *val = atofEuro (yytext); return (1); }
^{WS}*{NUMBER}{WS}* { *val = atof (yytext); return (1); }
[\n]
.
%%
/*------------------------------------------------------------------------*/
int scan_f (FILE *in, double *vals, int max)
{
double *val;
int npts, rc;
f_in = in;
val = vals;
npts = 0;
while (npts < max)
{
rc = f_Lex (val);
if (rc == 0)
break;
npts++;
val++;
}
return (npts);
}
/*------------------------------------------------------------------------*/
int f_wrap ()
{
return (1);
}