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

125

Ich habe sehr oft Leute gesehen, die andere davon abgehalten haben, zu benutzen scanfund zu sagen, dass es bessere Alternativen gibt. Am Ende sehe ich jedoch entweder "Nicht verwenden scanf" oder "Hier ist eine korrekte Formatzeichenfolge" und niemals Beispiele für die genannten "besseren Alternativen" .

Nehmen wir zum Beispiel diesen Codeausschnitt:

scanf("%c", &c);

Dies liest das Leerzeichen, das nach der letzten Konvertierung im Eingabestream verbleibt. Die übliche vorgeschlagene Lösung hierfür ist die Verwendung von:

scanf(" %c", &c);

oder nicht zu verwenden scanf.

scanfWas sind einige ANSI C-Optionen zum Konvertieren von Eingabeformaten, scanfdie normalerweise ohne Verwendung verarbeitet werden können (z. B. Ganzzahlen, Gleitkommazahlen und Zeichenfolgen), da dies schlecht ist scanf?

SS Anne
quelle

Antworten:

87

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

  • Verwenden fgetsmit einer festen Größe, was normalerweise empfohlen wird, und

  • using fgetc, was nützlich sein kann, wenn Sie nur eine einzige lesen char.

Zum Konvertieren der Eingabe stehen verschiedene Funktionen zur Verfügung:

  • strtoll, um eine Zeichenfolge in eine Ganzzahl umzuwandeln

  • strtof/ d/ ld, Eine Zeichenkette in eine Gleitpunktzahl umgewandelt

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

  • Es gibt keine guten Möglichkeiten, eine durch Trennzeichen getrennte Eingabe in einfachem ANSI C zu analysieren. Entweder strtok_rüber POSIX oder strtoknicht threadsicher. Sie können auch Ihre eigene thread-sichere Variante mit strcspnund rollenstrspn , da strtok_rkeine spezielle Betriebssystemunterstützung erforderlich ist.

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

  • Keine Konvertierung, verwenden Sie einfach die Zeichenfolge


Da ich nicht genau darauf eingegangen bin, warumscanf meine Frage schlecht ist, werde ich näher darauf eingehen:

  • Mit den Konvertierungsspezifizierer %[...]und %c, scanfnicht essen 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 verwendet werden soll, wenn auf die scanfArgumente von 'verwiesen wird (insbesondere mit Zeichenfolgen).

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

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

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


SS Anne
quelle
56

Warum ist scanfschlecht?

Das Hauptproblem ist, dass scanfes nie beabsichtigt war, Benutzereingaben zu verarbeiten. Es soll mit "perfekt" formatierten Daten verwendet werden. Ich habe das Wort "perfekt" zitiert, weil es nicht ganz wahr ist. Es ist jedoch nicht dafür ausgelegt, Daten zu analysieren, die so unzuverlässig sind wie Benutzereingaben. Benutzereingaben sind von Natur aus nicht vorhersehbar. Benutzer verstehen Anweisungen falsch, machen Tippfehler, drücken versehentlich die Eingabetaste, bevor sie fertig sind usw. Man könnte sich vernünftigerweise fragen, warum eine Funktion, die nicht für Benutzereingaben verwendet werden sollte, von liest stdin. Wenn Sie ein erfahrener * nix-Benutzer sind, ist die Erklärung keine Überraschung, kann jedoch Windows-Benutzer verwirren. In * nix-Systemen ist es sehr üblich, Programme zu erstellen, die über Piping funktionieren.stdoutstdindes zweiten. Auf diese Weise können Sie sicherstellen, dass Ausgabe und Eingabe vorhersehbar sind. Unter diesen Umständen scanffunktioniert 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 nur sehr selten Benutzereingaben von stdin. Meistens übergeben Sie alle Benutzereingaben als Befehlszeilenargumente. Sicher, es gibt Ausnahmen, aber für die meisten Anwendungen ist die Benutzereingabe eine sehr untergeordnete Sache.

Also was kannst du tun?

Mein Favorit ist fgetsin Kombination mit sscanf. Ich habe einmal eine Antwort darauf geschrieben, aber ich werde den vollständigen Code erneut veröffentlichen. Hier ist ein Beispiel mit anständiger (aber nicht perfekter) Fehlerprüfung und -analyse. Es ist gut genug für Debugging-Zwecke.

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>: ")und dann verwenden sscanf(buffer "%d.%d", &dollar, &cent). Ich würde niemals so etwas tun printf("Enter height and base of the triangle: "). Der Hauptzweck der folgenden Verwendung fgetsbesteht darin, die Eingaben zu kapseln, um sicherzustellen, dass eine Eingabe die nächste nicht beeinflusst.

#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 viele davon ausführen, kann ich empfehlen, einen Wrapper zu erstellen, der immer leert:

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

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

Im Allgemeinen würde ich raten, nicht zu erwarten, dass der Benutzer Eingaben in einem seltsamen Format eingibt, das Sie in verschiedenen Variablen analysieren sollten. Wenn Sie die Variablen heightund zuweisen möchten, widthfragen Sie nicht gleichzeitig nach beiden. Ermöglichen Sie dem Benutzer, zwischen ihnen die Eingabetaste zu drücken. Auch dieser Ansatz ist in gewisser Hinsicht sehr natürlich. Sie werden die Eingabe stdinerst erhalten, wenn Sie die Eingabetaste drücken. Warum also nicht immer die gesamte 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 zuweist getline(). Der Nachteil ist, dass Sie freedas Ergebnis anschließend benötigen .

Das Spiel steigern

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

klutt
quelle
Beachten Sie, dass (r = sscanf("1 2 junk", "%d%d", &x, &y)) != 2der nachfolgende nicht numerische Text nicht als schlecht erkannt wird.
chux
1
@chux% f% f behoben. Was meinst du mit dem ersten?
Klutt
Mit fgets()dem "1 2 junk", if((r = sscanf(buffer, "%d%d", &x, &y)) != 2) {etwas falsch nicht mit Eingang berichten , obwohl es „Junk“ hat.
chux
@chux Ah, jetzt verstehe ich. Nun, das war beabsichtigt.
Klutt
1
scanfsoll mit perfekt formatierten Daten verwendet werden Aber auch das stimmt nicht. Neben dem von @chux erwähnten Problem mit "Junk" gibt es auch die Tatsache, dass ein Format wie "%d %d %d"gerne Eingaben aus einer, zwei oder drei Zeilen liest (oder sogar mehr, wenn dazwischen Leerzeilen liegen), dass es keine gibt Die Möglichkeit, eine zweizeilige Eingabe zu erzwingen (sagen wir), indem Sie etwas Ähnliches "%d\n%d %d"usw. tun , ist scanfmöglicherweise für formatierte Stream- Eingaben geeignet , aber für nichts zeilenbasiertes überhaupt nicht gut.
Steve Summit
18

scanfist großartig, wenn Sie wissen, dass Ihre Eingabe immer gut strukturiert ist und sich gut benimmt. Andernfalls...

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

  • Risiko eines Pufferüberlaufs - Wenn Sie keine Feldbreite für die %sund die %[Konvertierungsspezifizierer angeben, besteht das Risiko eines Pufferüberlaufs (Sie versuchen, mehr Eingaben zu lesen, als ein Puffer für die Größe hat). 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 Makroshenanigans ausführen.

  • Akzeptiert Eingaben, sollte abgelehnt werden - Wenn Sie einen Eingang mit der gerade lesen %dKonvertierungsspezifizierer und Sie geben so etwas wie 12w4, man würde erwarten scanf , dass die Eingabe zu verwerfen, aber es funktioniert nicht - es erfolgreich konvertiert und Abtretungsempfänger des 12, so dass w4in dem Eingangsstrom die nächste Lesung zu beschmutzen.

Also, was solltest du stattdessen verwenden?

Normalerweise empfehle ich, alle interaktiven Eingaben als Text fgetszu lesen. Mit dieser Option können Sie eine maximale Anzahl von Zeichen festlegen, die gleichzeitig gelesen werden sollen, damit Sie einen Pufferüberlauf auf einfache Weise verhindern können:

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

Eine Besonderheit fgetsist, dass die nachfolgende neue Zeile im Puffer gespeichert wird, wenn Platz vorhanden ist. Sie können also auf einfache Weise überprüfen, ob jemand mehr Eingaben eingegeben 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 sofort ablehnen und alle verbleibenden Eingaben schlürfen mit getchar:

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

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

Um die Eingabe zu tokenisieren (auf der Grundlage eines oder mehrerer Trennzeichen aufzuteilen), können Sie sie verwenden strtok, aber Vorsicht - strtokändert ihre Eingabe (sie überschreibt Trennzeichen mit dem String-Terminator), und Sie können ihren Status nicht beibehalten (dh Sie können ' t Eine Zeichenfolge teilweise tokenisieren, dann eine andere Zeichenfolge tokenisieren und dort weitermachen, wo Sie in der ursprünglichen Zeichenfolge aufgehört haben. Es gibt eine Variante, strtok_sdie den Status des Tokenizers beibehält, aber die Implementierung von AFAIK ist optional (Sie müssen überprüfen, __STDC_LIB_EXT1__ob diese definiert ist, um festzustellen, ob sie verfügbar ist).

Sobald Sie Ihre Eingabe getoken haben und Zeichenfolgen in Zahlen konvertieren müssen (dh "1234"=> 1234), haben Sie Optionen. strtolund strtodkonvertiert Zeichenfolgendarstellungen von Ganzzahlen und reellen Zahlen in ihre jeweiligen Typen. Sie ermöglichen es Ihnen auch, das 12w4oben erwähnte Problem zu lösen - eines ihrer Argumente ist ein Zeiger auf das erste Zeichen, das nicht in die 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;
John Bode
quelle
Wenn Sie keine Feldbreite angeben ... - oder eine Konvertierungsunterdrückung (z. B. %*[%\n]nützlich, um später in der Antwort mit überlangen Linien umzugehen).
Toby Speight
Es gibt eine Möglichkeit, die Laufzeitspezifikation der Feldbreiten abzurufen, aber es ist nicht schön. Am Ende müssen Sie die Formatzeichenfolge in Ihrem Code erstellen (möglicherweise mit snprintf()) ,.
Toby Speight
5
Sie haben dort den häufigsten Fehler gemacht isspace()- es werden nicht signierte Zeichen akzeptiert , die als dargestellt intwerden. Sie müssen also umwandeln unsigned char, um UB auf Plattformen zu vermeiden, auf denen charsigniert ist.
Toby Speight
9

In dieser Antwort gehe ich davon aus, dass Sie Textzeilen lesen und interpretieren . Vielleicht fordern Sie den Benutzer auf, der etwas eingibt und RETURN drückt. Oder Sie lesen Zeilen mit strukturiertem Text aus einer Datendatei.

Da Sie Textzeilen lesen, ist es sinnvoll, Ihren Code um eine Bibliotheksfunktion herum zu organisieren, die eine Textzeile liest. Die Standardfunktion ist fgets(), obwohl es andere gibt (einschließlich getline). Und dann ist der nächste Schritt, diese Textzeile irgendwie zu interpretieren.

Hier ist das Grundrezept für den Aufruf fgetszum Lesen einer Textzeile:

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

Dies liest einfach eine Textzeile ein und druckt sie wieder aus. Wie geschrieben hat es ein paar Einschränkungen, die wir in einer Minute erreichen werden. Es hat auch eine sehr gute Funktion: Diese Zahl 512, an die wir als zweites Argument übergeben haben, fgetsist die Größe des Arrays, in das linewir fgetseinlesen möchten. Diese Tatsache - dass wir erkennen können, fgetswie viel es lesen darf - bedeutet, dass wir sicher sein können, dass fgetsdas Array nicht überläuft , indem wir zu viel hineinlesen.

Jetzt wissen wir also, wie man eine Textzeile liest, aber was ist, wenn wir wirklich eine Ganzzahl oder eine Gleitkommazahl oder ein einzelnes Zeichen oder ein einzelnes Wort lesen möchten? (Das heißt, was ist, wenn der scanfAnruf wir versuchen , auf zu verbessern war wie ein Formatbezeichner mit %d, %f, %c, oder %s?)

Es ist einfach, eine Textzeile - eine Zeichenfolge - als eines dieser Dinge neu zu interpretieren. Um eine Zeichenfolge in eine Ganzzahl umzuwandeln, ist der einfachste (wenn auch unvollständige) Weg, dies zu tun, der Aufruf atoi(). Um in eine Gleitkommazahl umzuwandeln, gibt es atof(). (Und es gibt auch bessere Möglichkeiten, wie wir gleich sehen werden.) Hier ein sehr einfaches Beispiel:

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

Wenn Sie möchten, dass der Benutzer ein einzelnes Zeichen eingibt (möglicherweise yoder nals Ja / Nein-Antwort), können Sie buchstäblich nur das erste Zeichen der Zeile wie folgt abrufen:

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

(Dies ignoriert natürlich die Möglichkeit, dass der Benutzer eine Antwort mit mehreren Zeichen eingegeben hat. Alle zusätzlichen Zeichen, die eingegeben wurden, werden stillschweigend ignoriert.)

Wenn Sie möchten, dass der Benutzer eine Zeichenfolge eingibt, die definitiv kein Leerzeichen enthält, wenn Sie die Eingabezeile behandeln möchten

hello world!

Da die Zeichenfolge "hello"von etwas anderem gefolgt wird (was das scanfFormat %sgetan hätte), ist es in diesem Fall nicht ganz so einfach, die Zeile auf diese Weise neu zu interpretieren, also die Antwort darauf Ein Teil der Frage muss etwas warten.

Aber zuerst möchte ich auf drei Dinge zurückkommen, die ich übersprungen habe.

(1) Wir haben angerufen

fgets(line, 512, stdin);

in das Array einzulesen line, und wo 512 die Größe des Arrays ist line, fgetsweiß also, dass es nicht überlaufen soll. Um sicherzustellen, dass 512 die richtige Nummer ist (insbesondere um zu überprüfen, ob möglicherweise jemand das Programm optimiert hat, um die Größe zu ändern), müssen Sie zurücklesen, wo lineimmer dies deklariert wurde. Das ist ein Ärgernis, daher gibt es zwei viel bessere Möglichkeiten, um die Größen synchron zu halten. Sie können (a) den Präprozessor verwenden, um einen Namen für die Größe zu erstellen:

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

Oder (b) verwenden Sie den sizeofOperator von C :

fgets(line, sizeof(line), stdin);

(2) Das zweite Problem ist, dass wir nicht nach Fehlern gesucht haben. Wenn Sie Eingaben lesen, sollten Sie immer nach Fehlern suchen. Wenn fgetsdie von Ihnen angeforderte Textzeile aus irgendeinem Grund nicht gelesen werden kann, wird dies durch die Rückgabe eines Nullzeigers angezeigt. Also hätten wir Dinge wie tun sollen

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

Schließlich gibt es das Problem, dass zum Lesen einer Textzeile fgetsZeichen gelesen und in Ihr Array eingefügt werden, bis das \nZeichen gefunden wird, das die Zeile beendet, und das \nZeichen auch in Ihr Array eingefügt wird . Sie können dies sehen, wenn Sie unser früheres Beispiel geringfügig ändern:

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

Wenn ich dies ausführe und "Steve" eingebe, wenn es mich dazu auffordert, wird es ausgedruckt

you typed: "Steve
"

Das "in der zweiten Zeile ist, weil die Zeichenfolge, die es gelesen und wieder ausgedruckt hat, tatsächlich war "Steve\n".

Manchmal spielt diese zusätzliche Zeile keine Rolle (wie bei unserem Anruf atoioder atof, da beide zusätzliche nicht numerische Eingaben nach der Nummer ignorieren), aber manchmal ist sie sehr wichtig. So oft wollen wir diese neue Zeile entfernen. Es gibt verschiedene Möglichkeiten, die ich in einer Minute erreichen werde. (Ich weiß, dass ich das viel gesagt habe. Aber ich werde auf all diese Dinge zurückkommen, das verspreche ich.)

An diesem Punkt denken Sie vielleicht: "Ich dachte, Sie sagten, es scanf sei nicht gut, und dieser andere Weg wäre so viel besser. Aber fgetses fängt an, wie ein Ärgernis auszusehen. Das Anrufen scanfwar so einfach ! Kann ich es nicht weiter benutzen?" ""

Sicher, Sie können weiter verwenden scanf, wenn Sie möchten. (Und für wirklich einfache Dinge ist es in gewisser Weise einfacher.) Aber bitte kommen Sie nicht zu mir, wenn es Ihnen aufgrund einer seiner 17 Macken und Schwächen versagt oder aufgrund Ihrer Eingabe in eine Endlosschleife gerät nicht erwartet, oder wenn Sie nicht herausfinden können, wie man es benutzt, um etwas komplizierteres zu tun. Und werfen wir einen Blick auf fgetsdie tatsächlichen Belästigungen:

  1. Sie müssen immer die Arraygröße angeben. Nun, das ist natürlich überhaupt kein Ärgernis - das ist eine Funktion, denn Pufferüberlauf ist eine wirklich schlechte Sache.

  2. Sie müssen den Rückgabewert überprüfen. Eigentlich ist das eine Wäsche, denn um scanfrichtig zu verwenden , muss man auch den Rückgabewert überprüfen.

  3. Sie müssen den \nRücken abstreifen . Ich gebe zu, das ist ein echtes Ärgernis. Ich wünschte, es gäbe eine Standardfunktion, auf die ich Sie hinweisen könnte, die dieses kleine Problem nicht hatte. (Bitte niemand ansprechen gets.) Aber im Vergleich zu scanf's17 verschiedenen Belästigungen werde ich diese eine Belästigung eines fgetsjeden Tages nehmen.

So , wie Sie Ihnen die Newline - Streifen? Drei Wege:

(a) Offensichtlicher Weg:

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

(b) Kniffliger und kompakter Weg:

strtok(line, "\n");

Leider funktioniert dieser nicht immer.

(c) Ein anderer kompakter und leicht dunkler Weg:

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

Und jetzt, da das nicht im Weg ist, können wir zu einer anderen Sache zurückkehren, die ich übersprungen habe: den Unvollkommenheiten von atoi()und atof(). Das Problem bei diesen ist, dass sie Ihnen keinen nützlichen Hinweis auf Erfolg oder Misserfolg geben: Sie ignorieren nachfolgende nicht numerische Eingaben stillschweigend und geben stillschweigend 0 zurück, wenn überhaupt keine numerische Eingabe vorhanden ist. Die bevorzugten Alternativen - die auch bestimmte andere Vorteile haben - sind strtolund strtod. strtolSie können auch eine andere Basis als 10 verwenden, was bedeutet, dass Sie den Effekt (unter anderem) %ooder %xmit erzielen könnenscanf. Aber zu zeigen, wie man diese Funktionen richtig einsetzt, ist eine Geschichte für sich und würde zu sehr von dem ablenken, was sich bereits in eine ziemlich fragmentierte Erzählung verwandelt. Deshalb werde ich jetzt nichts mehr darüber sagen.

Der Rest der Haupterzählung betrifft Eingaben, die Sie möglicherweise analysieren möchten und die komplizierter sind als nur eine einzelne Zahl oder ein einzelnes Zeichen. Was ist, wenn Sie eine Zeile lesen möchten, die zwei Zahlen oder mehrere durch Leerzeichen getrennte Wörter oder eine bestimmte Interpunktion enthält? Hier werden die Dinge interessant, und dort wurden die Dinge wahrscheinlich kompliziert, wenn Sie versuchten, Dinge mit zu tun scanf, und wo es jetzt, da Sie eine Textzeile sauber gelesen haben, weitaus mehr Optionen gibt fgets, obwohl die ganze Geschichte über all diese Optionen könnte wahrscheinlich ein Buch füllen, also werden wir hier nur die Oberfläche kratzen können.

  1. Meine Lieblingstechnik besteht darin, die Zeile in durch Leerzeichen getrennte "Wörter" aufzuteilen und dann mit jedem "Wort" etwas weiter zu machen. Eine Hauptstandardfunktion hierfür ist strtok(die auch ihre Probleme hat und die auch eine ganze separate Diskussion bewertet). Meine eigene Präferenz ist eine dedizierte Funktion zum Erstellen eines Arrays von Zeigern auf jedes auseinandergebrochene "Wort", eine Funktion, die ich in diesen Kursnotizen beschreibe . Auf jeden Fall , wenn Sie „Wörter“ haben, können Sie weiter jeden verarbeiten, vielleicht mit den gleichen atoi/ atof/ strtol/ strtod Funktionen haben wir bereits betrachtet.

  2. Paradoxerweise besteht eine scanfandere gute Möglichkeit, mit der Textzeile umzugehen, mit der wir gerade gelesen haben, fgetsdarin, sie weiterzugeben , obwohl wir hier ziemlich viel Zeit und Mühe aufgewendet haben, um herauszufinden, wie wir uns entfernen können sscanf. Auf diese Weise erhalten Sie die meisten Vorteile scanf, jedoch ohne die meisten Nachteile.

  3. Wenn Ihre Eingabesyntax besonders kompliziert ist, kann es angebracht sein, eine "Regexp" -Bibliothek zu verwenden, um sie zu analysieren.

  4. Schließlich können Sie die für Sie geeigneten Ad-hoc- Parsing-Lösungen verwenden. Sie können Zeichen char *für Zeichen durch die Zeile bewegen, indem Sie mit einem Zeiger nach den erwarteten Zeichen suchen. Oder Sie können mit Funktionen wie strchroder strrchr, oder strspnoder strcspnoder nach bestimmten Zeichen suchen strpbrk. Oder Sie können / convert analysieren und überspringen Gruppen von Ziffernzeichen , die mit strtoloder strtodFunktionen , die wir über früher übersprungen.

Es gibt natürlich noch viel mehr zu sagen, aber hoffentlich bringt Ihnen diese Einführung den Einstieg.

Steve Summit
quelle
Gibt es einen guten Grund zum Schreiben sizeof (line)und nicht nur zum Schreiben sizeof line? Ersteres lässt es so aussehen, als wäre linees ein Typname!
Toby Speight
@TobySpeight Ein guter Grund? Nein, ich bezweifle es. Die Klammern sind meine Gewohnheit, weil ich mich nicht daran erinnern kann, ob es sich um Objekte oder Typnamen handelt, für die sie benötigt werden, aber viele Programmierer lassen sie weg, wenn sie können. (Für mich ist es eine Frage der persönlichen Vorlieben und des Stils, und eine ziemlich kleine.)
Steve Summit
+1 für die Verwendung sscanfals Konvertierungs-Engine, aber das Sammeln (und möglicherweise Massieren) der Eingabe mit einem anderen Werkzeug. Aber vielleicht im Zusammenhang erwähnenswert getline.
dmckee --- Ex-Moderator Kätzchen
Wenn Sie über " fscanfdie tatsächlichen Belästigungen" sprechen , meinen Sie das fgets? Und das Ärgernis Nr. 3 ärgert mich wirklich, insbesondere angesichts der Tatsache, dass scanfein nutzloser Zeiger auf den Puffer zurückgegeben wird, anstatt die Anzahl der eingegebenen Zeichen zurückzugeben (was das Entfernen der neuen Zeile viel sauberer machen würde).
Supercat
1
Vielen Dank für die Erklärung Ihres sizeofStils. Für mich ist es einfach, sich daran zu erinnern, wann Sie die Eltern brauchen: Ich denke, (type)es ist wie eine Besetzung ohne Wert (weil wir nur an dem Typ interessiert sind). Eine andere Sache: Sie sagen, dass strtok(line, "\n")das nicht immer funktioniert, aber es ist nicht offensichtlich, wann es nicht funktioniert. Ich vermute, Sie denken an den Fall, in dem die Zeile länger als der Puffer war, also haben wir keine neue Zeile und geben strtok()null zurück? Es ist wirklich schade, fgets()dass kein nützlicherer Wert zurückgegeben wird, sodass wir wissen können, ob die neue Zeile vorhanden ist oder nicht.
Toby Speight
7

Was kann ich verwenden, um Eingaben anstelle von scanf zu analysieren?

Statt scanf(some_format, ...), sollten fgets()mitsscanf(buffer, some_format_and %n, ...)

Mithilfe von " %n"kann der Code einfach erkennen, ob das gesamte Format erfolgreich gescannt wurde und am Ende kein zusätzlicher Nicht-Leerraum-Junk 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.
  }
chux - Monica wieder einsetzen
quelle
6

Geben wir die Anforderungen für das Parsen wie folgt an:

  • Eine gültige Eingabe muss akzeptiert (und in eine andere Form konvertiert) werden.

  • Eine ungültige Eingabe muss zurückgewiesen werden

  • Wenn eine Eingabe abgelehnt wird, muss dem Benutzer eine beschreibende Nachricht zur Verfügung gestellt werden, die erklärt (in klarer Sprache "leicht verständlich für normale Personen, die keine Programmierer sind"), warum sie abgelehnt wurde (damit die Benutzer herausfinden können, wie das Problem behoben werden kann Problem)

Um die Dinge sehr einfach zu halten, sollten Sie eine einzelne einfache Dezimalzahl (die vom Benutzer eingegeben wurde) und nichts anderes analysieren. Mögliche Gründe für die Ablehnung 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, die einen Bruchteil ungleich Null hat

Definieren wir auch "Eingabe enthielt nicht akzeptable Zeichen" richtig. und sag das:

  • führende Leerzeichen und nachfolgende Leerzeichen werden ignoriert (z. B. "
    5" wird als "5" behandelt)
  • Null oder ein Dezimalpunkt sind zulässig (z. B. "1234" und "1234.000" werden beide wie "1234" behandelt).
  • es muss mindestens eine Ziffer vorhanden sein (zB "." wird abgelehnt)
  • Es ist nicht mehr als ein Dezimalpunkt zulässig (z. B. "1.2.3" wird abgelehnt).
  • Kommas, die nicht zwischen Ziffern stehen, werden abgelehnt (z. B. ", 1234" wird abgelehnt)
  • Kommas nach einem Dezimalpunkt werden abgelehnt (z. B. "1234.000.000" wird abgelehnt)
  • Kommas nach einem anderen Komma werden abgelehnt (z. B. "1 ,, 234" wird abgelehnt)
  • Alle anderen Kommas werden ignoriert (z. B. "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 feststellen, 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 ....)"
  • "Anzahl 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 Nummer"
  • "Schlechtes Komma in der Mitte der Zahl"
  • "Schlechtes Komma nach dem Komma"

An diesem Punkt können wir sehen, dass eine geeignete Funktion zum Konvertieren einer Zeichenfolge in eine Ganzzahl zwischen sehr unterschiedlichen Fehlertypen unterscheiden muss. und dass so 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 dessen verwenden, was gültig ist / nicht " Eingang").

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

Um die angegebenen Anforderungen zu erfüllen; Diese convertStringToInteger()Funktion besteht wahrscheinlich aus mehreren hundert Codezeilen für sich.

Dies war nur "Parsen einer einzelnen einfachen Dezimalzahl". Stellen Sie sich vor, Sie möchten etwas Komplexes analysieren. wie eine Liste von "Name, Straße, Telefonnummer, E-Mail-Adresse" -Strukturen; 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 verwenden, um Eingaben anstelle von scanf zu analysieren?

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

Brendan
quelle
5

Hier ist ein Beispiel für die Verwendung flexzum Scannen einer einfachen Eingabe, in diesem Fall einer Datei mit ASCII-Gleitkommazahlen, die entweder im US- ( n,nnn.dd) oder im europäischen ( n.nnn,dd) Format vorliegen können . Dies wird nur aus einem viel größeren Programm kopiert, daher gibt es möglicherweise einige ungelöste Verweise:

/* 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);
}
jamesqf
quelle
-5

Andere Antworten enthalten die richtigen Details auf niedriger Ebene, daher beschränke ich mich auf eine höhere Ebene: Analysieren Sie zunächst, wie jede Eingabezeile aussehen soll. Versuchen Sie, die Eingabe mit einer formalen Syntax zu beschreiben - mit etwas Glück können Sie feststellen, dass sie mit einer regulären Grammatik oder zumindest einer kontextfreien Grammatik beschrieben werden kann . Wenn eine reguläre Grammatik ausreicht, können Sie eine Finite-State-Maschine codierenHiermit wird jede Befehlszeile zeichenweise erkannt und interpretiert. Ihr Code liest dann eine Zeile (wie in anderen Antworten erläutert) und scannt dann die Zeichen im Puffer durch die Zustandsmaschine. In bestimmten Zuständen halten Sie an und konvertieren den bisher gescannten Teilstring in eine Zahl oder was auch immer. Sie können wahrscheinlich "Ihre eigenen rollen", wenn es so einfach ist; Wenn Sie feststellen, dass Sie eine vollständige kontextfreie Grammatik benötigen, sollten Sie besser herausfinden, wie vorhandene Parsing-Tools (re: lexund / yaccoder deren Varianten) verwendet werden.

PMar
quelle
Eine endliche Zustandsmaschine kann übertrieben sein; Es sind einfachere Möglichkeiten zum Erkennen eines Überlaufs bei Conversions (z. B. Überprüfen, ob errno == EOVERFLOWnach der Verwendung strtoll) möglich.
SS Anne
1
Warum sollten Sie Ihre eigene Finite-State-Maschine codieren, wenn Flex das Schreiben trivial einfach macht?
Jamesqf