127
votes

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

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 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ées.

Par exemple, prenons cet extrait de code:

scanf(" %c", &c);

Cela lit l'espace blanc laissé dans le flux d'entrée après la dernière conversion. La solution habituelle suggérée à cela est d'utiliser:

scanf("%c", &c);

ou de ne pas utiliser scanf .

Puisque scanf est mauvais, quelles sont les options ANSI C pour la conversion des formats d'entrée que scanf peut généralement gérer (comme les entiers, les nombres à virgule flottante et les chaînes) sans utiliser scanf ?


0 commentaires

8 Réponses :


89
votes

Les moyens les plus courants de lire les entrées sont:

  • utiliser des fgets de taille fixe, ce qui est généralement suggéré, et

  • en utilisant fgetc , qui peut être utile si vous lisez un seul char .

Pour convertir l'entrée, vous pouvez utiliser diverses fonctions:

  • strtoll , pour convertir une chaîne en entier

  • strtof / d / ld , pour convertir une chaîne en un nombre à virgule flottante

  • sscanf , ce qui n'est pas aussi mauvais que d'utiliser simplement scanf , bien qu'il présente la plupart des inconvénients mentionnés ci-dessous

  • Il n'y a pas de bons moyens d'analyser une entrée séparée par des délimiteurs dans ANSI C. Utilisez soit strtok_r de POSIX, soit strtok , qui n'est pas thread-safe. Vous pouvez également rouler votre propre variante thread-safe en utilisant strcspn et strspn , car strtok_r n'implique aucune prise en charge particulière du système d'exploitation.

  • C'est peut-être exagéré, mais vous pouvez utiliser des lexers et des analyseurs ( flex et bison étant les exemples les plus courants).

  • Pas de conversion, utilisez simplement la chaîne


Comme je n'ai pas expliqué exactement pourquoi scanf est mauvais dans ma question, je vais élaborer:

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

  • Il y a une certaine confusion quant à l'utilisation de l'opérateur unaire & en se référant aux arguments de scanf (spécifiquement avec des chaînes).

  • Il est très facile d'ignorer la valeur de retour de scanf . Cela pourrait facilement entraîner un comportement non défini lors de la lecture d'une variable non initialisée.

  • Il est très facile d'oublier d'éviter le débordement de la mémoire tampon dans scanf . scanf("%s", str) est tout aussi mauvais, sinon pire, que l' 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.



0 commentaires

20
votes

scanf est génial quand vous savez que votre entrée est toujours bien structurée et bien comportée. Autrement...

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

  • Risque de buffer overflow - si vous ne spécifiez pas de largeur de champ pour les spécificateurs de conversion %s et %[ , vous risquez un buffer overflow (en essayant de lire plus d'entrée qu'un buffer n'est dimensionné 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 faire des manigances de macro.

  • Accepte les entrées qui devraient être rejetées - Si vous lisez une entrée avec le spécificateur de conversion %d et que vous tapez quelque chose comme 12w4 , vous vous attendez à ce que scanf rejette cette entrée, mais ce n'est pas le cas - il convertit et attribue avec succès le 12 , laissant w4 dans le flux d'entrée pour encrasser la lecture suivante.

Alors, que devriez-vous utiliser à la place?

Je recommande généralement de lire toutes les entrées interactives sous forme de texte à l'aide de fgets - cela vous permet de spécifier un nombre maximal de caractères à lire à la fois, de sorte que vous pouvez facilement éviter le débordement de la mémoire tampon:

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;

Une bizarrerie de fgets est qu'il stockera la nouvelle ligne de fin dans la mémoire tampon s'il y a de la place, vous pouvez donc vérifier facilement si quelqu'un a tapé plus d'entrée que ce à quoi vous vous attendiez:

while ( getchar() != '\n' ) 
  ; // empty loop

La façon dont vous gérez cela dépend de vous - vous pouvez soit rejeter toute l'entrée d'emblée, et absorber toute entrée restante avec getchar :

char *newline = strchr( input, '\n' );
if ( !newline )
{
  // input longer than we expected
}

Ou vous pouvez traiter l'entrée que vous avez reçue jusqu'à présent et la 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 peut pas partiellement tokeniser une chaîne, puis commencer à en tokeniser une autre, puis reprendre là où vous vous étiez arrêté dans la chaîne d'origine). Il existe 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 tokenisé votre entrée, si vous devez convertir des chaînes en nombres (par exemple, "1234" => 1234 ), vous avez des options. strtol et strtod convertiront 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 problème 12w4 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 input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
  // error reading from input stream, handle as appropriate
}
else
{
  // process input buffer
}


3 commentaires

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


Il existe un moyen d'obtenir la spécification au moment de l'exécution des largeurs de champ, mais ce n'est pas agréable. Vous snprintf() par devoir construire la chaîne de format dans votre code (peut-être en utilisant snprintf() ) snprintf()


Vous avez commis l'erreur la plus courante avec isspace() là-bas - il accepte les caractères non signés représentés comme int , vous devez donc convertir en caractères unsigned char pour éviter UB sur les plates-formes où char est signé.



58
votes

Pourquoi scanf est-il mauvais?

Le principal problème est que scanf n'a jamais été conçu pour traiter les entrées utilisateur. Il est destiné à être utilisé avec des données formatées «parfaitement». J'ai cité le mot «parfaitement» parce que 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. 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 depuis stdin . Si vous êtes un utilisateur expérimenté de * nix, l'explication ne vous surprendra pas, mais elle risque de dérouter les utilisateurs de Windows. Dans les systèmes * nix, il est très courant de construire des programmes qui fonctionnent via le piping, ce qui signifie que vous envoyez la sortie d'un programme à un autre en redirigeant le stdout du premier programme vers le stdin du second. De cette façon, vous pouvez vous assurer que la sortie et l'entrée sont prévisibles. Dans ces circonstances, scanf fonctionne bien. Mais lorsque vous travaillez avec des entrées imprévisibles, vous risquez toutes sortes de problèmes.

Alors, pourquoi n'y a-t-il pas de fonctions standard faciles à utiliser pour la saisie 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 les applications de terminal typiques, elles lisent très rarement les entrées utilisateur de stdin . Le plus souvent, vous transmettez toutes les entrées utilisateur en tant qu'arguments de ligne de commande. Bien sûr, il y a des exceptions, mais pour la plupart des applications, l'entrée de l'utilisateur est une chose très mineure.

Alors que peux-tu faire?

Tout d'abord, gets n'est PAS une alternative. C'est dangereux et ne doit JAMAIS être utilisé. Lisez ici pourquoi: Pourquoi la fonction gets est-elle si dangereuse qu'elle ne devrait pas être utilisée?

Mon préféré est fgets en combinaison avec sscanf . J'ai déjà écrit une réponse à ce sujet, mais je publierai à nouveau le code complet. Voici un exemple avec une vérification et une analyse d'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 l'un à l'autre de manière naturelle. Comme par exemple printf("Enter the price in the format <dollars>.<cent>: "); fgets(buffer, bsize, stdin); puis utilisez sscanf(buffer "%d.%d", &dollar, &cent) . Je ne ferais jamais quelque chose comme printf("Enter height and base of the triangle: ") . Le point principal de l'utilisation des fgets ci-dessous est d'encapsuler les entrées pour s'assurer qu'une entrée n'affecte pas la suivante.

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;
}

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

#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 comme cela éliminera un problème courant, qui est la nouvelle ligne de fin qui peut perturber l'entrée d'imbrication. Mais cela pose un autre problème, à savoir 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 conseillerais de ne pas attendre de l'utilisateur qu'il saisisse une entrée 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. En outre, cette approche est très naturelle dans un sens. Vous n'obtiendrez jamais l'entrée de stdin tant que vous n'aurez pas stdin 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 la mémoire tampon. Est-ce que je me souviens de mentionner que l'entrée utilisateur est maladroite en C? :)

Pour éviter les 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.

Intensifier le jeu

Si vous envisagez sérieusement de créer des programmes en C avec une entrée utilisateur, je vous recommanderais de jeter un œil à une bibliothèque comme ncurses . Parce que vous souhaiterez probablement également créer des applications avec des graphiques de terminal. Malheureusement, vous perdrez une certaine portabilité si vous faites cela, mais cela vous donne un bien meilleur contrôle des entrées utilisateur. Par exemple, il vous donne la possibilité de lire instantanément une touche au lieu d'attendre que l'utilisateur appuie sur Entrée.

Lecture intéressante

Voici une diatribe sur scanf :http://sekrit.de/webdocs/c/beginners-guide-away-from-scanf.html


17 commentaires

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


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


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


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


@chux Eh bien, intentionnel était un peu exagéré. C'était plutôt comme ça que je m'en fichais. Il était plus facile de jeter tout ce qui se cache derrière les chiffres que de faire des vérifications supplémentaires. :)


La raison pour laquelle il n'y a pas de bonnes fonctions pour les E / S de console est que C89 a délibérément évité les fonctionnalités qui ne seraient pas universellement supportables, et il n'est pas nécessaire que les implémentations C (même hébergées) aient quelque chose qui ressemble même à distance à une console.


@supercat Cela semble très plausible et c'est une chose très intéressante. Je pourrais l'ajouter à la réponse. Avez-vous une source?


@klutt: les compilateurs de Borland et Microsoft incluaient tous les deux getch() qui lit un caractère du clavier sans écho, getche() qui lit et fait écho un caractère, putch() qui génère un caractère à l'écran, et cprintf , qui envoie un chaîne à l'écran; ces fonctions ne sont pas affectées par la redirection d'E / S. Je pense qu'il y avait aussi un cgets() même si je ne l'ai jamais utilisé.


@klutt: Recherchez «C99 Rationale» pour savoir pourquoi beaucoup de choses dans la norme sont telles qu'elles sont (elle décrit à la fois C89 et C99). Une chose clé à comprendre est que les auteurs de C89 étaient habilités à décrire un langage existant, plutôt qu'à inventer des fonctionnalités, donc si différentes implémentations utilisaient différentes façons de faire quelque chose, et que toutes les implémentations ne pouvaient pas le prendre en charge, la norme ignorerait simplement le problème en partant du principe que parce que ceux qui pouvaient l'appuyer de manière significative l'ont fait sans aucune norme, ils n'avaient pas besoin de la norme pour leur dire comment le faire.


@klutt: Même la simple lecture des premières parties de la justification donne une impression du langage qui n'a pas pour but de faire sauter les programmeurs à travers des cerceaux pour accomplir "ce qui doit être fait".


@supercat Oui, je suis d'accord à ce sujet. C'est juste que je préfère donner des sources aux allégations.


@klutt: Le fait que le Standard n'exige pas que les implémentations C prennent en charge tout type de console pourrait être déduit du manque de fonctions à cet effet. Je ne connais aucune source particulière pour le fait que de nombreuses implémentations pré-standard supportaient les E / S de console, autre que la documentation pour elles (que je n'ai pas facilement disponible). Il est dommage que le Comité n'ait pas voulu «inventer» des fonctions de console qui pourraient être facilement supportables sur des implémentations existantes avec des consoles simplement en les ajoutant à la bibliothèque, car aucune fonction préexistante ne conviendrait.


@klutt: Sur un système Unix, il est nécessaire de basculer le flux d'entrée en mode "brut" avant que les caractères ne soient saisis, ce qui rend impossible la prise en charge d'un getch() style Borland / MS, mais Unix permet d'utiliser getchar() pour entrée tamponnée ou non tamponnée, selon le paramètre de mode brut. Avoir une fonction "prepare for getch ()" [qui pourrait être un no-op dans MS-DOS], combinée avec getch() (qui sur certaines plates-formes nécessiterait une "préparation") aurait permis aux programmes de prendre en charge les deux types de console plates-formes.


scanf est destiné à être utilisé avec des données parfaitement formatées Mais même ce n'est pas vrai. Outre le problème de "junk" comme mentionné par @chux, il y a aussi le fait qu'un format comme "%d %d %d" est heureux de lire les entrées d'une, deux ou trois lignes (ou même plus, s'il y a lignes vierges intermédiaires), qu'il n'y a aucun moyen de forcer (disons) une entrée sur deux lignes en faisant quelque chose comme "%d\n%d %d" , etc. scanf peut être approprié pour l'entrée de flux formatée, mais ce n'est pas du tout bon pour tout ce qui est basé sur la ligne.


@SteveSummit: En général, les données formatées "parfaitement" auront un nombre connu d'éléments par ligne. et scanf sera d'une utilité marginale lors de la gestion de données qui n'ont pas de nombres connus d'éléments par ligne. Sur quelle partie de la déclaration êtes-vous en désaccord?


@supercat Cette boîte de commentaires n'est pas assez grande pour une réponse complète, mais c'est un peu comme la vieille blague, "Si nous avions du jambon, nous pourrions faire un sandwich au jambon ... si nous avions du pain." Votre déclaration sur scanf était en partie vraie, mais elle ne va pas assez loin. Je dirais: " scanf est nul pour les entrées utilisateur, parce que l'entrée utilisateur n'est pas formatée, et scanf est conçu pour les entrées formatées ... mais scanf est nul pour les entrées formatées." (Parce que, entre autres, cela peut être à la fois trop indulgent et ne pas pardonner assez.)


@SteveSummit: Dans les cas où le format de l'entrée est si fortement garanti que le programmeur n'aurait vraiment pas besoin de se soucier de la façon dont le code traiterait autre chose, scanf serait utilisable. Avoir une bibliothèque standard incluant une fonction qui fait environ la moitié de la taille d'un compilateur Pascal entier avec IDE intégré juste dans le but de gérer de tels cas rares semble un peu idiot, mais cela ne signifie pas que la fonction est complètement inutile.



7
votes

Décrivons les exigences de l'analyse comme:

  • une entrée valide doit être acceptée (et convertie sous une autre forme)

  • l'entrée invalide doit être rejetée

  • lorsqu'une entrée est rejetée, il est nécessaire de fournir à l'utilisateur un message descriptif qui explique (dans un langage clair "facilement compréhensible par les gens normaux qui ne sont pas des programmeurs") pourquoi elle a été rejetée (afin que les gens puissent trouver comment résoudre le problème 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 l'entrée de l'utilisateur sont:

  • l'entrée contenait 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 aussi correctement «l'entrée contenait des caractères inacceptables»; et dites 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 les chiffres seront rejetées (par exemple ", 1234" est rejeté)
  • les virgules après une virgule décimale 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 autre qu'un espace est rejeté
  • un signe positif qui n'est pas le premier caractère non blanc est rejeté

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

  • "Caractère inconnu au début de la saisie"
  • "Caractère inconnu à la fin de la saisie"
  • "Caractère inconnu au milieu de la saisie"
  • "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 points décimaux"
  • "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 la virgule décimale"

À partir de là, nous pouvons voir qu'une fonction appropriée pour convertir une chaîne en un entier aurait besoin de faire la distinction entre des types d'erreurs très différents; et que quelque chose comme " scanf() " ou " atoi() " ou " strtoll() " est totalement et totalement sans valeur car ils ne vous donnent aucune indication de ce qui n'allait pas avec l'entrée (et utilisent une définition totalement non pertinente 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() probablement par contenir plusieurs centaines de lignes de code à elle seule.

Maintenant, il s'agissait simplement "d'analyser 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 ne soit 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, en fonction de vos besoins.


0 commentaires

9
votes

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 RETURN. 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 des fgets pour lire une ligne de texte:

line[strcspn(line, "\n")] = '\0';

Cela lit simplement une ligne de texte et l'imprime. Tel qu'il est écrit, il présente quelques limitations, sur lesquelles nous reviendrons dans une minute. Il a également une très grande fonctionnalité: le nombre 512 que nous avons passé comme deuxième argument à fgets est la taille de la line tableau dans laquelle 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.

Nous savons maintenant comment 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, que se scanf -il 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, le moyen le plus simple (bien qu'imparfait) de le faire est d'appeler atoi() . Pour convertir en un 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:

strtok(line, "\n");

Si vous voulez que l'utilisateur tape un seul caractère (peut-être y ou n comme réponse oui / non), vous pouvez littéralement saisir le premier caractère de la ligne, comme ceci:

char *p = strchr(line, '\n');
if(p != NULL) *p = '\0';

(Cela ignore, bien sûr, la possibilité que l'utilisateur ait tapé une réponse à plusieurs caractères; il ignore tranquillement tous les caractères supplémentaires qui ont été tapés.)

Enfin, si vous vouliez que l'utilisateur tape une chaîne ne contenant définitivement pas d' espace, si vous vouliez traiter la ligne d'entrée

you typed: "Steve
"

comme la chaîne "hello" suivie de quelque chose d'autre (ce que le format scanf %s aurait fait), eh bien, dans ce cas, j'ai un peu menti, ce n'est pas 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 d'abord, je veux revenir sur trois choses que j'ai sautées.

(1) Nous avons appelé

printf("you typed: \"%s\"\n", line);

pour lire dans la line du tableau, et où 512 est la taille de la line du tableau afin que fgets sache ne pas la 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 l'endroit où la line été déclarée. C'est une nuisance, il existe donc deux bien meilleures façons de synchroniser les tailles. Vous pouvez, (a) utiliser le préprocesseur pour créer un nom pour la taille:

printf("type something:\n");
if(fgets(line, 512, stdin) == NULL) {
    printf("Well, never mind, then.\n");
    exit(1);
}

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 vérifié les erreurs. Lorsque vous lisez l'entrée, vous devez toujours vérifier la possibilité d'une 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. Donc nous aurions dû faire des choses comme

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

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 voir cela si vous modifiez légèrement notre exemple précédent:

fgets(line, 512, stdin);

Si je lance ceci et tape "Steve" quand il me le demande, il s'imprime

hello world!

Ce " sur la deuxième ligne est dû au fait que la chaîne lue et réimprimée était en fait "Steve\n" .

Parfois, cette nouvelle ligne supplémentaire n'a pas d'importance (comme lorsque nous atoi ou atof , car ils ignorent tous les deux toute entrée non numérique supplémentaire après le nombre), mais parfois cela compte beaucoup. Très souvent, nous voudrons supprimer cette nouvelle ligne. Il y a plusieurs façons de le faire, j'y reviendrai dans une minute. (Je sais que j'ai souvent dit cela. Mais je reviendrai sur toutes ces choses, je le promets.)

À ce stade, vous pensez peut-être: "Je pensais que vous aviez dit que scanf n'était pas bon, et que cette autre méthode serait bien meilleure. Mais fgets commence à ressembler à une nuisance. Appeler scanf était si facile ! Je ne peux pas continuer En l'utilisant?"

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'une de ses 17 bizarreries et faiblesses, ou entre dans une boucle infinie à cause de l'entrée de votre ne vous attendiez pas, ou lorsque vous ne savez pas comment l'utiliser pour faire quelque chose de plus compliqué. Et jetons un œil aux nuisances réelles de fgets :

  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 une très mauvaise chose.

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

  3. Vous devez retirer \n dos. C'est, je l'avoue, une véritable nuisance. J'aurais aimé qu'il y ait une fonction standard que je pourrais vous indiquer qui n'a pas ce petit problème. (S'il vous plaît, personne ne le gets .) Mais comparé aux 17 nuisances différentes de scanf's , je prendrai cette seule nuisance de fgets n'importe quel jour.

Alors, comment supprimez- vous cette nouvelle ligne? Trois façons:

(a) Manière évidente:

printf("type a character:\n");
fgets(line, 512, stdin);
char c = line[0];
printf("you typed %c\n", c);

(b) Manière délicate et compacte:

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);

Malheureusement, celui-ci ne fonctionne pas toujours.

(c) Une autre manière compacte et légèrement obscure:

char line[512];
printf("type something:\n");
fgets(line, 512, stdin);
printf("you typed: %s", line);

Et maintenant que ce n'est plus le cas, nous pouvons revenir à une autre chose que j'ai sauté: 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 discrètement les entrées non numériques de fin, et ils renvoient discrètement 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 correctement ces fonctions est une histoire en soi, et serait trop distraire de ce qui est déjà en train de se transformer en un récit assez fragmenté, je ne vais donc pas en dire plus à leur sujet maintenant.

Le reste du récit principal concerne les entrées que vous essayez peut-être d'analyser qui sont plus compliquées 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 se compliquaient probablement si vous essayiez de faire des choses avec scanf , et où il y a beaucoup plus d'options maintenant que vous avez lu proprement une ligne de texte à l'aide de fgets , bien que l'histoire complète sur tous ces options pourraient probablement remplir un livre, donc nous ne pourrons qu'effleurer 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". Une fonction standard principale pour ce faire est strtok (qui a également 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 . En tout cas, une fois que vous avez des "mots", vous pouvez continuer à traiter chacun d'eux, peut-être avec les mêmes fonctions atoi / atof / strtol / strtod nous avons déjà examinées.

  2. 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 la passer à 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 vous déplacer dans la ligne un caractère à la fois avec un pointeur char * vérifiant les caractères que vous attendez. Vous pouvez également 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 permettra de démarrer.


10 commentaires

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


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


+1 pour utiliser sscanf comme moteur de conversion mais collecter (et éventuellement masser) l'entrée avec un outil différent. Mais peut-être vaut-il la peine de mentionner getline dans ce contexte.


Quand vous parlez des «nuisances réelles de fscanf », voulez-vous dire des fgets ? Et la nuisance n ° 3 me dérange vraiment, d'autant plus que scanf renvoie un pointeur inutile vers le tampon plutôt que de renvoyer le nombre de caractères saisis (ce qui rendrait le retrait de la nouvelle ligne beaucoup plus propre).


Merci pour l'explication de votre sizeof style. Pour moi, se souvenir du moment où vous avez besoin des parenthèses est facile: je pense à (type) comme étant un casting sans valeur (car nous ne sommes intéressés que par le type). Une autre chose: vous dites que strtok(line, "\n") 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() retourne null? C'est vraiment dommage que fgets() ne renvoie pas une valeur plus utile pour que nous puissions savoir si la nouvelle ligne est là ou non.


Je suppose qu'une façon de savoir si la ligne complète a été lue est de mettre un NUL sentinelle à la fin du tampon, (à la line[(sizeof line) - 2] ). Si ce n'est pas une nouvelle ligne ou NUL après lecture, alors nous avons lu une ligne partielle.


@supercat je l'ai certainement fait. Fixé. Merci. Et le n ° 3 m'ennuie vraiment aussi. (Mais quand vous parlez de "surtout étant donné que scanf retourne", voulez-vous dire ...? :-))


@TobySpeight Je pense avoir vu l'astuce de la line[(sizeof line)-2] suggérée auparavant. (Ce serait "un moyen plus efficace de savoir si la ligne complète a été lue", bien sûr, car nous avons déjà un moyen, bien que moins que parfaitement efficace / pratique.)


@TobySpeight Je n'ai pas mentionné le cas d'échec strtok car je ne m'en souviens pas! Mais il y a un autre habitué ici sur [c] - peut-être chux - qui en a parlé. (Renvoyer null n'est pas un problème, car nous ignorons la valeur de retour.)


@dmckee Meant pour mentionner getline ; Merci pour le rappel.



7
votes

Que puis-je utiliser pour analyser l'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 tout le format a été scanné avec succès et qu'aucun élément indésirable supplémentaire autre qu'un espace blanc ne se trouvait à 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.
  }

0 commentaires

-5
votes

D'autres réponses donnent les bons détails de bas niveau, je me limiterai donc à un niveau supérieur: tout d'abord, analysez à quoi vous vous attendez à ce que chaque ligne d'entrée ressemble. 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 finis qui 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 dans le tampon à travers la machine à états. Dans 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 vous-même» si c'est aussi simple que cela; si vous constatez que vous avez besoin d'une grammaire complète et sans contexte, vous feriez mieux de comprendre comment utiliser les outils d'analyse existants (re: lex et yacc ou leurs variantes).


2 commentaires

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


Pourquoi coderiez-vous votre propre machine à états finis, alors que flex simplifie grandement leur écriture?



5
votes

Voici un exemple d'utilisation de flex pour analyser une entrée simple, dans ce cas un fichier de nombres à virgule flottante ASCII qui pourrait être au n,nnn.dd américain ( n,nnn.dd ) ou européen ( n.nnn,dd ). Ceci est juste copié à partir d'un programme beaucoup plus volumineux, 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);
}


0 commentaires