Was kann ich für die Eingabeumwandlung anstelle von scanf verwenden?




(6)

Was kann ich zum Parsen von Eingaben anstelle von scanf verwenden?

Betrachten fgets() anstelle von scanf(some_format, ...) fgets() mit sscanf(buffer, some_format_and %n, ...)

Mit " %n" kann der Code einfach erkennen, ob das gesamte Format erfolgreich gescannt wurde und am Ende kein zusätzlicher Junk-Platz mehr vorhanden war.

// 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.
  }

Ich habe sehr oft Leute gesehen, die andere davon scanf , scanf und gesagt haben, dass es bessere Alternativen gibt. Am Ende sehe ich jedoch nur entweder " scanf nicht verwenden" oder "Hier ist eine korrekte scanf " und niemals ein Beispiel für die "besseren Alternativen" .

Nehmen wir zum Beispiel diesen Codeausschnitt:

scanf("%c", &c);

Hiermit wird das Leerzeichen gelesen, das nach der letzten Konvertierung im Eingabestream verblieben ist. Die übliche Lösung hierfür ist:

scanf(" %c", &c);

oder nicht scanf .

scanf ANSI C-Optionen zum Konvertieren von Eingabeformaten (wie Ganzzahlen, Gleitkommazahlen und Zeichenfolgen) können von scanf ohne Verwendung von scanf werden, da scanf ?


Warum ist scanf schlecht?

Das Hauptproblem ist, dass scanf niemals dazu gedacht war, Benutzereingaben zu verarbeiten. Es soll mit "perfekt" formatierten Daten verwendet werden. Ich habe das Wort "perfekt" zitiert, weil es nicht ganz stimmt. Es wurde jedoch nicht zum Analysieren von Daten entwickelt, die so unzuverlässig sind wie Benutzereingaben. Benutzereingaben sind naturgemäß nicht vorhersehbar. Benutzer verstehen Anweisungen falsch, machen Tippfehler, drücken versehentlich die Eingabetaste, bevor sie fertig sind usw. Man könnte vernünftigerweise fragen, warum eine Funktion, die nicht für Benutzereingaben verwendet werden sollte, stdin liest. Wenn Sie ein erfahrener * nix-Benutzer sind, wird die Erklärung nicht überraschen, aber es könnte Windows-Benutzer verwirren. In * nix-Systemen ist es sehr verbreitet, Programme zu erstellen, die über Piping funktionieren. Dies bedeutet, dass Sie die Ausgabe eines Programms an ein anderes senden, indem Sie die stdout des ersten Programms an die stdin des zweiten Programms stdin . Auf diese Weise können Sie sicherstellen, dass die Ausgabe und Eingabe vorhersehbar sind. Unter diesen Umständen scanf tatsächlich gut. Wenn Sie jedoch mit unvorhersehbaren Eingaben arbeiten, riskieren Sie alle möglichen Probleme.

Warum gibt es keine benutzerfreundlichen Standardfunktionen für Benutzereingaben? Man kann hier nur raten, aber ich gehe davon aus, dass alte Hardcore-C-Hacker einfach dachten, dass die vorhandenen Funktionen gut genug waren, obwohl sie sehr klobig sind. Wenn Sie sich typische Terminalanwendungen ansehen, lesen sie sehr selten Benutzereingaben von stdin . Am häufigsten übergeben Sie alle Benutzereingaben als Befehlszeilenargumente. Sicher gibt es Ausnahmen, aber für die meisten Anwendungen ist die Benutzereingabe eine sehr kleine Sache.

Also, was kannst du machen?

Mein Favorit ist fgets in Kombination mit sscanf . Ich habe einmal eine Antwort dazu geschrieben, werde aber den kompletten Code erneut posten. Hier ist ein Beispiel mit anständiger (aber nicht perfekter) Fehlerprüfung und -analyse. Es ist gut genug für das Debuggen.

Hinweis

Ich mag es nicht besonders, den Benutzer zu bitten, zwei verschiedene Dinge in einer einzigen Zeile einzugeben. Das mache ich nur, wenn sie auf natürliche Weise zueinander gehören. Wie zum Beispiel printf("Enter the price in the format <dollars>.<cent>: ") sscanf(buffer "%d.%d", &dollar, &cent) printf("Enter the price in the format <dollars>.<cent>: ") und verwenden Sie dann sscanf(buffer "%d.%d", &dollar, &cent) . Ich würde niemals so etwas wie printf("Enter height and base of the triangle: ") . Der Hauptzweck bei der Verwendung von fgets besteht darin, die Eingaben zu kapseln, um sicherzustellen, dass sich eine Eingabe nicht auf die nächste auswirkt.

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

Wenn Sie so vorgehen, wird ein häufiges Problem behoben, nämlich der abschließende Zeilenumbruch, der mit der Verschachtelungseingabe in Konflikt geraten kann. Es gibt aber noch ein anderes Problem: Wenn die Leitung länger als bsize . Sie können dies mit if(buffer[strlen(buffer)-1] != '\n') überprüfen. Wenn Sie die neue Zeile entfernen möchten, können Sie dies mit buffer[strcspn(buffer, "\n")] = 0 tun.

Im Allgemeinen würde ich raten, nicht zu erwarten, dass der Benutzer Eingaben in einem seltsamen Format eingibt, das Sie mit verschiedenen Variablen analysieren sollten. Wenn Sie die Variablen height und width zuweisen möchten, fragen Sie nicht gleichzeitig nach beiden. Ermöglichen Sie dem Benutzer, die Eingabetaste zwischen ihnen zu drücken. Auch dieser Ansatz ist in gewissem Sinne sehr natürlich. Sie werden niemals die Eingabe von stdin bis Sie die Eingabetaste stdin Warum also nicht immer die ganze Zeile lesen? Dies kann natürlich immer noch zu Problemen führen, wenn die Zeile länger als der Puffer ist. Habe ich daran gedacht zu erwähnen, dass Benutzereingaben in C klobig sind? :)

Um Probleme mit Zeilen zu vermeiden, die länger als der Puffer sind, können Sie eine Funktion verwenden, die automatisch einen Puffer mit der entsprechenden Größe getline() . Verwenden Sie dazu getline() . Der Nachteil ist, dass Sie das Ergebnis anschließend freigeben müssen.

Das Spiel steigern

Wenn Sie es ernst meinen, Programme in C mit Benutzereingaben zu erstellen, würde ich empfehlen, sich eine Bibliothek wie ncurses anzuschauen. Denn dann möchten Sie wahrscheinlich auch Anwendungen mit einigen Terminalgrafiken erstellen. Leider verlieren Sie etwas an Portabilität, wenn Sie das tun, aber es gibt Ihnen eine weitaus bessere Kontrolle über Benutzereingaben. So können Sie beispielsweise einen Tastendruck sofort ablesen, anstatt darauf zu warten, dass der Benutzer die Eingabetaste drückt.


Die gebräuchlichsten Arten, Eingaben zu lesen, sind:

  • Verwenden von fgets mit einer festen Größe, wie es normalerweise empfohlen wird, und

  • Verwenden von fgetc , was nützlich sein kann, wenn Sie nur ein einzelnes fgetc lesen.

Um die Eingabe zu konvertieren, stehen Ihnen verschiedene Funktionen zur Verfügung:

  • strtoll , um einen String in eine Ganzzahl umzuwandeln

  • strtof / d / ld , um einen String in eine Gleitkommazahl umzuwandeln

  • sscanf , was nicht so schlimm ist wie das einfache Verwenden von scanf , obwohl es die meisten der unten genannten scanf hat

  • Es gibt keine gute Möglichkeit, eine durch Trennzeichen getrennte Eingabe in ANSI C zu analysieren. Verwenden strtok_r entweder strtok_r von POSIX oder strtok_s aus dem nicht weit verbreiteten Anhang K. Sie können auch eine eigene mit strcspn und strspn , da dies nicht der Fall ist spezielle Betriebssystemunterstützung einbeziehen.

  • Es mag übertrieben sein, aber Sie können Lexer und Parser verwenden ( flex und bison sind die häufigsten Beispiele).

  • Keine Konvertierung, einfach nur den String verwenden

Da Sie in Ihrer Frage nicht genau scanf , warum scanf schlecht ist, werde ich scanf erläutern:

  • Mit den Konvertierungsspezifizierern %[...] und %c scanf kein Leerzeichen auf. Dies ist anscheinend nicht allgemein bekannt, wie die vielen Duplikate dieser Frage belegen.

  • Es gibt einige Unklarheiten darüber, wann der unäre Operator & , wenn auf die Argumente von scanf Bezug scanf (insbesondere bei Strings).

  • Es ist sehr einfach, den Rückgabewert von scanf zu ignorieren. Dies kann leicht undefiniertes Verhalten beim Lesen einer nicht initialisierten Variablen verursachen.

  • Es ist sehr leicht zu vergessen, einen Pufferüberlauf in scanf zu verhindern. scanf("%s", str) ist genauso schlimm, wenn nicht schlimmer als es gets .

  • Sie können keinen Überlauf erkennen, wenn Sie Ganzzahlen mit scanf . Tatsächlich verursacht ein Überlauf undefiniertes Verhalten in diesen Funktionen.


Hier ist ein Beispiel für die Verwendung von flex zum Scannen einer einfachen Eingabe. In diesem Fall handelt es sich um eine Datei mit ASCII-Gleitkommazahlen, die entweder im US- n,nnn.dd ( n,nnn.dd ) oder im europäischen n.nnn,dd ( n.nnn,dd ) n.nnn,dd . Dies wurde nur aus einem viel größeren Programm kopiert, daher kann es einige ungelöste Verweise geben:

/* 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]

%%

[[email protected]#%&*/].*\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);
}

Lassen Sie uns die Anforderungen für das Parsen wie folgt angeben:

  • gültige Eingabe muss akzeptiert (und in eine andere Form umgewandelt) werden

  • ungültige Eingaben müssen zurückgewiesen werden

  • Wenn eine Eingabe abgelehnt wird, muss dem Benutzer eine beschreibende Nachricht übermittelt werden, die (in einer klaren Sprache, die "für normale Leute, die keine Programmierer sind") verständlich ist, warum sie abgelehnt wurde (damit die Leute herausfinden können, wie das Problem behoben werden kann) Problem)

Um die Dinge sehr einfach zu halten, betrachten wir das Parsen einer einzelnen einfachen Dezimalzahl (die vom Benutzer eingegeben wurde) und sonst nichts. Mögliche Gründe für die Zurückweisung der Benutzereingabe sind:

  • Die Eingabe enthielt nicht akzeptable Zeichen
  • Die Eingabe stellt eine Zahl dar, die niedriger als das akzeptierte Minimum ist
  • Die Eingabe stellt eine Zahl dar, die höher als das akzeptierte Maximum ist
  • Die Eingabe stellt eine Zahl dar, deren Anteil nicht null ist

Definieren wir auch "Eingaben, die nicht akzeptable Zeichen enthalten" richtig. und sag das:

  • führende und nachfolgende Leerzeichen werden ignoriert (zB "
    5 "wird als" 5 "behandelt)
  • Null oder ein Dezimalpunkt ist zulässig (z. B. "1234" und "1234.000" werden beide wie "1234" behandelt)
  • es muss mindestens eine Ziffer geben (zB "." wird abgelehnt)
  • maximal ein Dezimalpunkt ist zulässig (z. B. "1.2.3" wird abgelehnt)
  • Kommas, die nicht zwischen Ziffern stehen, werden verworfen (zB ", 1234" wird verworfen)
  • Kommas nach dem Komma werden verworfen (zB "1234.000.000" wird verworfen)
  • Kommas, die nach einem anderen Komma stehen, werden abgelehnt (zB "1, 234" wird abgelehnt)
  • alle anderen Kommas werden ignoriert (zB "1,234" wird als "1234" behandelt)
  • Ein Minuszeichen, das nicht das erste Nicht-Leerzeichen ist, wird abgelehnt
  • Ein positives Vorzeichen, das nicht das erste Nicht-Leerzeichen ist, wird abgelehnt

Daraus können wir ermitteln, dass die folgenden Fehlermeldungen benötigt werden:

  • "Unbekanntes Zeichen zu Beginn der Eingabe"
  • "Unbekanntes Zeichen am Ende der Eingabe"
  • "Unbekanntes Zeichen in der Mitte der Eingabe"
  • "Anzahl ist zu niedrig (Minimum ist ....)"
  • "Nummer ist zu hoch (Maximum ist ....)"
  • "Zahl ist keine ganze Zahl"
  • "Zu viele Dezimalstellen"
  • "Keine Dezimalstellen"
  • "Schlechtes Komma am Anfang der Nummer"
  • "Schlechtes Komma am Ende der Zahl"
  • "Schlechtes Komma in der Mitte der Zahl"
  • "Schlechtes Komma nach dem Komma"

An diesem Punkt können wir sehen, dass eine geeignete Funktion zum Umwandeln eines Strings in eine Ganzzahl zwischen sehr unterschiedlichen Fehlertypen unterscheiden müsste. und dass etwas wie " scanf() " oder " atoi() " oder " strtoll() " völlig wertlos ist, weil sie Ihnen keinen Hinweis darauf geben, was mit der Eingabe falsch war (und eine völlig irrelevante und unangemessene Definition verwenden) von dem, was "gültige Eingabe" ist / nicht ist).

Beginnen wir stattdessen damit, etwas zu schreiben, das nicht nutzlos ist:

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

Die angegebenen Anforderungen zu erfüllen; Diese Funktion convertStringToInteger() besteht wahrscheinlich aus mehreren hundert Codezeilen.

Dies war nur "Parsen einer einzelnen einfachen Dezimalzahl". Stellen Sie sich vor, Sie wollten etwas Komplexes analysieren. wie eine Liste von Strukturen "Name, Straße, Telefonnummer, E-Mail-Adresse"; oder vielleicht wie eine Programmiersprache. In diesen Fällen müssen Sie möglicherweise Tausende von Codezeilen schreiben, um eine Analyse zu erstellen, die kein verkrüppelter Witz ist.

Mit anderen Worten...

Was kann ich zum Parsen von Eingaben anstelle von scanf verwenden?

Schreiben Sie selbst (möglicherweise Tausende von Zeilen) Code, um Ihren Anforderungen zu entsprechen.


scanf ist fantastisch, wenn Sie wissen, dass Ihre Eingaben immer gut strukturiert sind und sich gut verhalten. Andernfalls...

IMO, hier sind die größten Probleme mit scanf :

  • Risiko eines Pufferüberlaufs - Wenn Sie für die Konvertierungsspezifizierer %s und %[ keine Feldbreite angeben, riskieren Sie einen Pufferüberlauf (wenn Sie versuchen, mehr Eingaben zu lesen, als ein Puffer aufnehmen kann). Leider gibt es keine gute Möglichkeit, dies als Argument anzugeben (wie bei printf ) - Sie müssen es entweder als Teil des Konvertierungsspezifizierers fest codieren oder einige Makrospielereien ausführen.

  • Akzeptiert Eingaben, die zurückgewiesen werden sollten - Wenn Sie eine Eingabe mit dem Konvertierungsspezifizierer %d lesen und etwas wie 12w4 , wird erwartet , dass scanf diese Eingabe zurückweist, dies jedoch nicht - die 12 wird erfolgreich konvertiert und scanf w4 im Eingabestrom, um den nächsten w4 .

Also, was solltest du stattdessen verwenden?

Normalerweise empfehle ich, alle interaktiven Eingaben mit fgets als Text zu lesen. Hier können Sie die maximale Anzahl von Zeichen angeben, die gleichzeitig gelesen werden sollen, um einen Pufferüberlauf auf einfache Weise zu verhindern:

char input[100];
if ( !fgets( input, sizeof input, stdin ) )
{
  // error reading from input stream, handle as appropriate
}
else
{
  // process input buffer
}

Eine Besonderheit von fgets ist, dass die nachgestellte Zeile im Puffer fgets wird, wenn Platz vorhanden ist. So können Sie leicht überprüfen, ob jemand mehr Eingaben getippt hat, als Sie erwartet hatten:

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

Wie Sie damit umgehen, liegt bei Ihnen. Sie können entweder die gesamte Eingabe getchar ablehnen und alle verbleibenden Eingaben mit getchar :

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

Oder Sie können die bisher eingegebenen Daten verarbeiten und erneut lesen. Das hängt von dem Problem ab, das Sie lösen möchten.

Zum Tokenisieren der Eingabe ( strtok anhand eines oder mehrerer Begrenzungszeichen) können Sie strtok verwenden. strtok , dass strtok die Eingabe ändert (Begrenzungszeichen werden mit dem Zeichenfolgenabschlusszeichen überschrieben), und Sie können den Status nicht beibehalten (z. B. Sie) Ich kann eine Zeichenfolge nicht teilweise mit einem Token versehen. Beginnen Sie dann, eine andere Zeichenfolge mit einem Token zu versehen. Es gibt eine Variante, strtok_s , die den Status des Tokenizers strtok_s , aber die Implementierung von __STDC_LIB_EXT1__ ist optional (Sie müssen überprüfen, __STDC_LIB_EXT1__ definiert ist, um __STDC_LIB_EXT1__ , ob sie verfügbar ist).

Wenn Sie nach dem Tokenisieren Ihrer Eingabe Zeichenfolgen in Zahlen konvertieren müssen (z. B. "1234" => 1234 ), stehen Ihnen Optionen zur Verfügung. strtol und strtod konvertieren Zeichenfolgendarstellungen von ganzen Zahlen und reellen Zahlen in ihre jeweiligen Typen. Mit ihnen können Sie auch das 12w4 erwähnte 12w4 Problem abfangen - eines ihrer Argumente ist ein Zeiger auf das erste Zeichen, das nicht in der Zeichenfolge konvertiert wurde:

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;




scanf