Que puis-je utiliser pour la conversion d'entrée au lieu de scanf?


125

J'ai vu très souvent des gens décourager les autres à utiliser scanfet dire qu'il existe de meilleures alternatives. Cependant, tout ce que je finis par voir, c'est «ne pas utiliser scanf» ou «voici une chaîne de format correcte» , et jamais aucun exemple des «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 de ne pas utiliser scanf.

Puisque scanfc'est mauvais, quelles sont les options ANSI C pour convertir les formats d'entrée qui scanfpeuvent généralement être manipulées (comme les entiers, les nombres à virgule flottante et les chaînes) sans utiliser scanf?

Réponses:


87

Les façons les plus courantes de lire une entrée sont:

  • utiliser fgetsavec une taille fixe, ce qui est généralement suggéré, et

  • en utilisant fgetc, ce qui peut être utile si vous n'en 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 aussi mauvais que de simplement l'utiliser 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, soit à strtok_rpartir de POSIX ou strtok, ce qui n'est pas thread-safe. Vous pouvez également rouler votre propre variante thread-safe en utilisant strcspnet strspn, comme strtok_rcela n'implique pas de prise en charge spéciale du système d'exploitation.

  • Cela peut être exagéré, mais vous pouvez utiliser des lexers et des analyseurs ( flexet bisonétant les exemples les plus courants).

  • Aucune conversion, utilisez simplement la chaîne


Étant donné que je ne suis pas allé dans le détail de la raison pour laquelle scanf ma question est mauvaise, je vais élaborer:

  • Avec les spécificateurs de conversion %[...]et %c, scanfne consomme pas d'espace. Cela n'est apparemment pas largement connu, comme en témoignent les nombreux doublons de cette question .

  • Il y a une certaine confusion quant au moment d'utiliser l' &opérateur unaire lors de la référence aux scanfarguments de (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 la mémoire tampon 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 un comportement indéfini dans ces fonctions.



56

Pourquoi est-ce scanfmauvais?

Le principal problème est qu'il scanfn'a jamais été conçu pour gérer les entrées des utilisateurs. Il est destiné à être utilisé avec des données formatées "parfaitement". J'ai cité le mot "parfaitement" car ce n'est pas tout à fait vrai. Mais il n'est pas conçu pour analyser des données aussi peu fiables que les entrées utilisateur. Par nature, l'entrée de l'utilisateur n'est pas prévisible. Utilisateurs instructions se méprend, fait des fautes de frappe, accidentellement appuyez sur Entrée avant d' être fait , etc. On peut raisonnablement se demander pourquoi une fonction qui ne doit pas être utilisé pour l' entrée utilisateur lit à partir 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,stdoutstdinde la seconde. De cette façon, vous pouvez vous assurer que la sortie et l'entrée sont prévisibles. Dans ces circonstances, scanffonctionne vraiment 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. De plus, lorsque vous regardez des applications de terminal typiques, elles lisent très rarement les entrées des utilisateurs 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 par l'utilisateur est une chose très mineure.

Alors que peux-tu faire?

Mon préféré est fgetsen 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 utilisez sscanf(buffer "%d.%d", &dollar, &cent). Je ne ferais jamais quelque chose comme ça printf("Enter height and base of the triangle: "). Le point principal de l'utilisation fgetsci-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);

Si vous en faites beaucoup, je pourrais recommander de créer un wrapper qui videra toujours:

int printfflush (const char *format, ...)
{
   va_list arg;
   int done;
   va_start (arg, format);
   done = vfprintf (stdout, format, arg);
   fflush(stdout);
   va_end (arg);
   return done;
}```

Faire cela éliminera un problème commun, qui est la nouvelle ligne de fin qui peut perturber l'entrée du nid. Mais il y a un autre problème, qui est si la ligne est plus longue que bsize. Vous pouvez vérifier cela 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 saisisse une entrée dans un format étrange que vous devez analyser en fonction de différentes variables. Si vous souhaitez affecter les variables heightet 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 stdinavant d'avoir appuyé sur Entrée, alors pourquoi ne pas toujours lire toute la ligne? 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 aurez besoin freedu résultat par la suite.

Intensifier le jeu

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 terminaux. Malheureusement, vous perdrez une partie de la 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.


Notez que (r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2ne détecte pas aussi mauvais le texte non numérique de fin.
chux

1
@chux Fixe% f% f. Que voulez-vous dire par le premier?
klutt

Avec fgets()of "1 2 junk", if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {ne signale rien de mal à l'entrée même s'il a des "ordures".
chux

@chux Ah, maintenant je vois. Eh bien, c'était intentionnel.
klutt

1
scanfest destiné à être utilisé avec des données parfaitement formatées Mais même ce n'est pas vrai. Outre le problème avec "junk" comme mentionné par @chux, il y a aussi le fait qu'un format comme "%d %d %d"est heureux de lire l'entrée à partir d'une, deux ou trois lignes (ou même plus, s'il y a des lignes vides intermédiaires), qu'il n'y a pas Un moyen de forcer (par exemple) une entrée à deux lignes en faisant quelque chose comme "%d\n%d %d", etc. scanfpourrait être approprié pour une entrée de flux formatée , mais ce n'est pas du tout bon pour quoi que ce soit basé sur une ligne.
Steve Summit

18

scanfest génial quand vous savez que votre contribution est toujours bien structurée et bien conduite. Autrement...

OMI, voici les plus gros problèmes avec scanf:

  • Risque de dépassement de la mémoire tampon - si vous ne spécifiez pas de largeur de champ pour les spécificateurs de conversion %set %[, vous risquez un dépassement de mémoire tampon (en essayant de lire plus d'entrée qu'une taille de mémoire tampon ne peut 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 faire quelques manigances de macro.

  • Accepte les entrées qui doivent être rejetées - Si vous lisez une entrée avec le %dspécificateur de conversion et que vous tapez quelque chose comme 12w4, vous vous attendez scanf à rejeter cette entrée, mais ce n'est pas le cas - elle convertit et attribue avec succès le 12, laissant w4dans 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é fgetsest qu'il stockera la nouvelle ligne de fin dans le tampon s'il y a de la place, vous pouvez donc vérifier facilement 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 récupérer 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 symboliser l'entrée (la diviser en fonction d'un ou de plusieurs délimiteurs), vous pouvez utiliser strtok, mais attention - strtokmodifiez son entrée (elle écrase les délimiteurs avec le terminateur de chaîne), et vous ne pouvez pas conserver son état (c.-à-d., Vous pouvez ' t 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 existe une variante, strtok_squi préserve l'état du tokenizer, mais AFAIK son implémentation est facultative (vous devrez vérifier qu'elle __STDC_LIB_EXT1__est définie pour voir si elle est disponible).

Une fois que vous avez symbolisé votre entrée, si vous devez convertir des chaînes en nombres (c'est-à-dire "1234"=> 1234), vous avez des options. strtolet strtodconvertira les représentations de chaînes d'entiers et de nombres réels en leurs types respectifs. Ils vous permettent également d'attraper le 12w4problème que j'ai mentionné ci-dessus - l'un de leurs arguments est un pointeur vers 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;

Si vous ne spécifiez pas de largeur de champ ... - ou une suppression de conversion (par exemple %*[%\n], ce qui est utile pour traiter les lignes trop longues plus loin dans la réponse).
Toby Speight

Il existe un moyen d'obtenir des spécifications d'exécution des largeurs de champ, mais ce n'est pas bien. Vous finissez par devoir construire la chaîne de formatage dans votre code (peut-être en utilisant snprintf()),.
Toby Speight

5
Vous avez fait l'erreur la plus courante avec isspace()cela - il accepte les caractères non signés représentés comme int, vous devez donc caster unsigned charpour éviter UB sur les plates-formes où charest signé.
Toby Speight

9

Dans cette réponse, je vais supposer que vous lisez et interprétez des lignes de texte . Vous invitez peut-être 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 fgetspour 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é: le nombre 512 que nous avons passé comme deuxième argument fgetsest la taille du tableau dans linelequel nous demandons fgetsde lire. Ce fait - que nous pouvons dire fgetscombien il est autorisé à lire - signifie que nous pouvons être sûrs que fgetsle tableau ne débordera pas 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? (Autrement dit, si l' scanfappel que nous essayons d'améliorer avait été l' aide d' un spécificateur de format comme %d, %f, %cou %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 existe également 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 y- être ou ncomme 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 silencieusement tous les caractères supplémentaires qui ont été saisis.)

Enfin, si vous vouliez que l'utilisateur tape une chaîne ne contenant certainement 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 scanfformat %saurait fait), eh bien, dans ce cas, j'ai un peu tordu, ce n'est pas si facile de réinterpréter la ligne de cette façon, après tout, donc la réponse à cela une 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 sait linedonc fgetsne 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ù il a lineété déclaré. C'est une nuisance, il existe donc deux bien meilleures façons de synchroniser les tailles. Vous pouvez, (a) utiliser le préprocesseur pour donner un nom à la taille:

#define MAXLINE 512
char line[MAXLINE];
fgets(line, MAXLINE, stdin);

Ou, (b) utilisez l' sizeofopérateur 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, fgetsvous ne pouvez pas lire la ligne de texte à laquelle vous lui avez demandé, cela indique cela 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, fgetslit les caractères et les remplit dans votre tableau jusqu'à ce qu'il trouve le \ncaractère qui termine la ligne, et il remplit également le \ncaractère 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
"

Cela "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é atoiou 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 aviez dit que ce scanf n'était pas bon, et cette autre façon serait tellement mieux. Mais cela fgetscommence à ressembler à une nuisance. Appeler 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 des choses vraiment simples, à certains égards, c'est plus simple.) Mais, s'il vous plaît, ne venez pas me pleurer quand il vous échoue en raison de l'un de ses 17 caprices et faiblesses, ou entre dans une boucle infinie à cause de l'entrée de votre ne vous attendiez pas, ou quand vous ne pouvez pas comprendre comment l'utiliser pour faire quelque chose de plus compliqué. Et regardons fgetsles nuisances réelles de:

  1. 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.

  2. Vous devez vérifier la valeur de retour. En fait, c'est un lavage, car pour l'utiliser scanfcorrectement, vous devez également vérifier sa valeur de retour.

  3. Vous devez retirer le \ndos. 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's17 nuisances différentes, je prendrai celle-là de fgetsn'importe quel jour.

Alors, comment voulez - vous supprimer 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 succès 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 strtolet strtod. strtolvous permet également d'utiliser une base autre que 10, ce qui signifie que vous pouvez obtenir l'effet (entre autres) %oou %xavecscanf. Mais montrer comment utiliser correctement ces fonctions 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 une entrée que vous essayez d'analyser, c'est plus compliqué qu'un simple chiffre ou caractère. Que faire si vous souhaitez 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 devenaient probablement compliquées 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 pourrait probablement remplir un livre, donc nous allons seulement pouvoir gratter la surface ici.

  1. 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». L'une des principales fonctions standard pour ce faire est strtok(qui a également ses problèmes, et qui évalue également toute une discussion séparée). Ma 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 poursuivre le traitement de chacun, peut-être avec les mêmes fonctions atoi/ atof/ strtol/ que strtod nous avons déjà examinées.

  2. Paradoxalement, même si nous avons passé scanfpas mal de temps et d'efforts ici à trouver un moyen de nous éloigner , une autre bonne façon de gérer la ligne de texte que nous venons de lire fgetsest de la transmettre sscanf. De cette façon, vous vous retrouvez avec la plupart des avantages de scanf, mais sans la plupart des inconvénients.

  3. Si votre syntaxe d'entrée est particulièrement compliquée, il peut être approprié d'utiliser une bibliothèque "regexp" pour l'analyser.

  4. Enfin, vous pouvez utiliser toutes les solutions d'analyse ad hoc qui vous conviennent. Vous pouvez parcourir la ligne un caractère à la fois avec un char *pointeur vérifiant les caractères que vous attendez. Ou vous pouvez rechercher des caractères spécifiques en utilisant des fonctions comme strchrou strrchr, ou strspnou strcspn, ou strpbrk. Ou vous pouvez analyser / convertir et ignorer des groupes de caractères numériques à l'aide des fonctions strtolou strtodque nous avons ignorées plus tôt.

Il y a évidemment beaucoup plus à dire, mais j'espère que cette introduction vous aidera à démarrer.


Y a-t-il une bonne raison d'écrire sizeof (line)plutôt que simplement sizeof line? Le premier donne l'impression que linec'est un nom de type!
Toby Speight

@TobySpeight Une bonne raison? Non, j'en doute. Les parenthèses sont mon habitude, car je ne peux pas me soucier de savoir si ce sont des objets ou des noms de type pour lesquels ils sont requis, mais de nombreux programmeurs les laissent quand ils le peuvent. (Pour moi, c'est une question de préférence et de style personnels, et assez mineure à ce sujet.)
Steve Summit

+1 pour utiliser sscanfcomme moteur de conversion mais collecter (et éventuellement masser) l'entrée avec un outil différent. Mais cela vaut peut-être la peine d'être mentionné getlinedans ce contexte.
dmckee --- chaton ex-modérateur

Quand vous parlez des " fscanfnuisances réelles", voulez-vous dire fgets? Et la nuisance # 3 me dérange vraiment, surtout étant donné que scanfrenvoie un pointeur inutile vers le tampon plutôt que de renvoyer le nombre de caractères entrés (ce qui rendrait la suppression de la nouvelle ligne beaucoup plus propre).
supercat

1
Merci pour l'explication de votre sizeofstyle. Pour moi, il est facile de se souvenir de quand on veut les parens: je pense (type)que c'est comme un casting sans valeur (parce que nous ne sommes intéressés que par le type). Une autre chose: vous dites que strtok(line, "\n")cela ne fonctionne pas toujours, mais ce n'est pas évident quand ce n'est pas le cas. Je suppose que vous pensez au cas où la ligne était plus longue que le tampon, donc nous n'avons pas de nouvelle ligne et strtok()renvoie null? C'est vraiment dommage de fgets()ne pas retourner une valeur plus utile afin que nous puissions savoir si la nouvelle ligne est là ou non.
Toby Speight

7

Que puis-je utiliser pour analyser l'entrée au lieu de scanf?

Au lieu de scanf(some_format, ...), considérez fgets()avecsscanf(buffer, some_format_and %n, ...)

En utilisant " %n", le code peut simplement détecter si tout 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.
  }

6

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 les gens normaux qui ne sont pas programmeurs") pourquoi il a été rejeté (afin que les gens puissent comprendre comment corriger 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:

  • l'entrée contient des caractères inacceptables
  • l'entrée représente un nombre inférieur au minimum accepté
  • l'entrée représente un nombre supérieur au maximum accepté
  • l'entrée représente un nombre qui a une partie fractionnaire non nulle

Définissons également correctement "l'entrée contient des caractères inacceptables"; et dire que:

  • les espaces de début et de fin seront ignorés (par exemple, "
    5" sera traité comme "5")
  • zéro ou un point décimal est autorisé (par exemple, "1234" et "1234 000" sont tous deux traités de la même manière que "1234")
  • il doit y avoir au moins un chiffre (par exemple "." est rejeté)
  • pas plus d'un point décimal n'est autorisé (par exemple, "1.2.3" est rejeté)
  • les virgules qui ne sont pas entre des chiffres seront rejetées (par exemple ", 1234" est rejeté)
  • les virgules qui se trouvent après un point décimal seront rejetées (par exemple, "1234.000.000" est rejeté)
  • les virgules qui se trouvent après une autre virgule sont rejetées (par exemple, "1,, 234" est rejeté)
  • toutes les autres virgules seront ignorées (par exemple, "1 234" sera traité comme "1234")
  • un signe moins qui n'est pas le premier caractère non blanc est rejeté
  • un signe positif qui n'est pas le premier caractère non blanc est rejeté

À partir de cela, nous pouvons déterminer que les messages d'erreur suivants sont nécessaires:

  • "Caractère inconnu au début de l'entrée"
  • "Caractère inconnu à la fin de l'entrée"
  • "Caractère inconnu au milieu de l'entrée"
  • "Le nombre est trop bas (le minimum est ....)"
  • "Le nombre est trop élevé (le maximum est ....)"
  • "Le nombre n'est pas un entier"
  • "Trop de décimales"
  • "Pas de chiffres décimaux"
  • "Mauvaise virgule au début du numéro"
  • "Mauvaise virgule à la fin du numéro"
  • "Mauvaise virgule au milieu du nombre"
  • "Mauvaise virgule après le point décimal"

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 utilisent une définition complètement hors de propos et inappropriée de ce qui est / n'est pas "valide" contribution").

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 convertStringToInteger()fonction est susceptible de constituer à elle seule plusieurs centaines de lignes de code.

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 l'entrée au lieu de scanf?

Écrivez (potentiellement des milliers de lignes) de code vous-même, selon vos besoins.


5

Voici un exemple d'utilisation flexpour numériser une entrée simple, dans ce cas un fichier de nombres à virgule flottante ASCII qui pourrait être au format US ( n,nnn.dd) ou européen ( n.nnn,dd). 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);
}

-5

D'autres réponses donnent les bons détails de bas niveau, donc je me limiterai à un niveau supérieur: d'abord, analysez à quoi vous attendez chaque ligne d'entrée. Essayez de décrire l'entrée avec une syntaxe formelle - avec de la chance, vous constaterez qu'elle peut être décrite en utilisant une grammaire régulière , ou au moins une grammaire sans contexte . Si une grammaire régulière suffit, vous pouvez coder une machine à états finisqui reconnaît et interprète chaque ligne de commande un caractère à la fois. Votre code lira alors une ligne (comme expliqué dans d'autres réponses), puis analysera les caractères du tampon via la machine à états. À certains états, vous vous arrêtez et convertissez la sous-chaîne analysée jusqu'à présent en un nombre ou autre. Vous pouvez probablement «rouler le vôtre» si c'est aussi simple que cela; si vous trouvez que vous avez besoin d'une grammaire complète et sans contexte, vous feriez mieux de trouver comment utiliser les outils d'analyse existants (re: lexet / yaccou leurs variantes).


Une machine à états finis peut être exagérée; des moyens plus simples de détecter le débordement dans les conversions (comme vérifier si errno == EOVERFLOWaprès utilisation strtoll) sont possibles.
SS Anne

1
Pourquoi voudriez-vous coder votre propre machine à états finis, quand flex rend leur écriture trivialement simple?
jamesqf
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.