C – scanf() vs. gets() vs. fgets()

Lesezeit: 12 Minuten

Markos Benutzeravatar
Markus

Ich habe ein ziemlich einfaches Programm zum Konvertieren einer Zeichenkette (vorausgesetzt, Zahlen werden eingegeben) in eine ganze Zahl erstellt.

Nachdem ich fertig war, bemerkte ich einige sehr merkwürdige “Bugs”, die ich nicht beantworten kann, hauptsächlich wegen meines begrenzten Wissens darüber, wie das geht scanf(), gets() und fgets() Funktionen funktionieren. (Ich habe allerdings viel Literatur gelesen.)

Also, ohne zu viel Text zu schreiben, hier ist der Code des Programms:

#include <stdio.h>

#define MAX 100

int CharToInt(const char *);

int main()
{
    char str[MAX];

    printf(" Enter some numbers (no spaces): ");
    gets(str);
//  fgets(str, sizeof(str), stdin);
//  scanf("%s", str);

    printf(" Entered number is: %d\n", CharToInt(str));

    return 0;
}

int CharToInt(const char *s)
{
    int i, result, temp;

    result = 0;
    i = 0;

    while(*(s+i) != '\0')
    {
        temp = *(s+i) & 15;
        result = (temp + result) * 10;
        i++;
    }

    return result / 10;
}

Hier ist also das Problem, das ich hatte. Erstens bei der Verwendung gets() Funktion, das Programm funktioniert einwandfrei.

Zweitens bei der Verwendung fgets()das Ergebnis ist etwas falsch, weil anscheinend fgets() Funktion liest Newline (ASCII-Wert 10) zuletzt, was das Ergebnis vermasselt.

Drittens bei der Verwendung scanf() -Funktion ist das Ergebnis völlig falsch, da das erste Zeichen anscheinend einen ASCII-Wert von -52 hat. Dafür habe ich keine Erklärung.

Jetzt weiß ich das gets() Es wird davon abgeraten, es zu verwenden, daher würde ich gerne wissen, ob ich es verwenden kann fgets() hier, damit es das Zeilenumbruchzeichen nicht liest (oder ignoriert). Außerdem, was hat es mit dem auf sich scanf() Funktion in diesem Programm?

  • Möglicherweise möchten Sie Ihre ersetzen CharToInt() Funktion mit einem Aufruf an atoi() (sie tun dasselbe). Auch die char Datentyp ist implizit signedwas den “-52 ASCII-Wert” erklären könnte, den Sie gesehen haben. cplusplus.com/reference/clibrary/cstdlib/atoi

    – Zeichen

    21. Juli 2010 um 18:07 Uhr


  • Ja, ich könnte atoi() verwenden, aber der eigentliche Sinn dieses Programms bestand darin, bitweise Operatoren zu verwenden. Vielen Dank auch, dass Sie mich an den signierten Wert von char erinnert haben. Die Verwendung von unsigned char löste das Problem, obwohl ich mir immer noch nicht sicher bin, wie und warum.

    – Markus

    21. Juli 2010 um 18:18 Uhr

  • @sigint: In C kann char nach Ermessen des Compilers ein signiertes char oder ein unsigned char sein.

    – ninjalj

    21. Juli 2010 um 18:26 Uhr

  • Ich dachte, Sie müssten wahrscheinlich selbst schreiben. Warum unsigned char(s) Ihr Problem lösen; Ein normales (signed) char hat einen Wertebereich von –128 bis 127, während an unsigned char hat einen Bereich von 0 bis 255. Das Bit-Twiddling hat wahrscheinlich seltsame Dinge mit den negativen Werten gemacht.

    – Zeichen

    21. Juli 2010 um 18:28 Uhr


  • Übrigens, *(s+i) wird normalerweise in C als geschrieben s[i] (es hat genau die gleiche Semantik).

    – Café

    22. Juli 2010 um 0:12 Uhr

Benutzeravatar von jamesdlin
jamesdlin

  • Niemals verwenden gets. Es bietet keinen Schutz vor einer Pufferüberlauf-Schwachstelle (das heißt, Sie können ihm nicht sagen, wie groß der Puffer ist, den Sie ihm übergeben, sodass es einen Benutzer nicht daran hindern kann, eine Zeile einzugeben, die größer als der Puffer ist, und den Speicher zu beschädigen).

  • Vermeide das Benutzen scanf. Wenn es nicht sorgfältig verwendet wird, kann es zu den gleichen Pufferüberlaufproblemen kommen wie gets. Selbst wenn man das ignoriert, es hat andere Probleme, die es schwierig machen, es richtig zu verwenden.

  • Im Allgemeinen sollten Sie verwenden fgets stattdessen, obwohl es manchmal unpraktisch ist (Sie müssen den Zeilenumbruch entfernen, Sie müssen im Voraus eine Puffergröße bestimmen, und dann müssen Sie herausfinden, was mit zu langen Zeilen zu tun ist – behalten Sie den gelesenen Teil und verwerfen Sie den Überschuss, verwerfen Sie das Ganze, vergrößern Sie den Puffer dynamisch und versuchen Sie es erneut usw.). Es sind einige nicht standardmäßige Funktionen verfügbar, die diese dynamische Zuordnung für Sie erledigen (z getline auf POSIX-Systemen, Gemeinfrei von Chuck Falconer ggets Funktion). Beachten Sie, dass ggets hat gets-ähnliche Semantik, da es einen abschließenden Zeilenumbruch für Sie entfernt.

  • Wie ich in meiner Antwort sagte, getline ist jetzt Standard.

    – Matthäus Flaschen

    21. Juli 2010 um 19:04 Uhr

  • @Matthew Flaschen: Welcher Standard? Wenn ich “Nicht-Standard” sage, meine ich “Nicht-Standard-C”, nicht Nicht-POSIX.

    – jamesdlin

    21. Juli 2010 um 19:14 Uhr


Ja, Sie wollen vermeiden gets. fgets liest immer die neue Zeile, wenn der Puffer groß genug war, um sie aufzunehmen (was Sie wissen lässt, wenn der Puffer zu klein war und mehr von der Zeile darauf wartet, gelesen zu werden). Wenn Sie so etwas wollen fgets die die neue Zeile nicht lesen (wobei die Angabe eines zu kleinen Puffers verloren geht), die Sie verwenden können fscanf mit einer Scan-Set-Konvertierung wie: "%N[^\n]"wobei das ‘N’ durch die Puffergröße – 1 ersetzt wird.

Eine einfache (wenn auch seltsame) Möglichkeit, die abschließende neue Zeile nach dem Lesen mit aus einem Puffer zu entfernen fgets ist: strtok(buffer, "\n"); So geht das nicht strtok soll verwendet werden, aber ich habe es öfter auf diese Weise als auf die beabsichtigte Weise verwendet (was ich im Allgemeinen vermeide).

Benutzeravatar von Michaelangel007
Michelangel007

Es gibt zahlreich Probleme mit diesem Code. Wir werden die schlecht benannten Variablen und Funktionen reparieren und die Probleme untersuchen:

  • Zuerst, CharToInt() sollte in das richtige umbenannt werden StringToInt() da es auf einem arbeitet Schnur kein einziges Zeichen.

  • Die Funktion CharToInt() [sic.] ist unsicher. Es wird nicht überprüft, ob der Benutzer versehentlich einen NULL-Zeiger übergibt.

  • Eingaben werden nicht validiert oder, genauer gesagt, ungültige Eingaben übersprungen. Wenn der Benutzer eine Nichtziffer eingibt, enthält das Ergebnis einen falschen Wert. dh wenn Sie eintreten N der Code *(s+i) & 15 wird 14 produzieren!?

  • Als nächstes das Unscheinbare temp in CharToInt() [sic.] aufgerufen werden soll digit denn das ist es wirklich.

  • Auch der Kludge return result / 10; ist genau das – ein schlechtes hacken um eine fehlerhafte Implementierung zu umgehen.

  • Ebenfalls MAX ist schlecht benannt, da es scheinbar im Widerspruch zur Standardverwendung steht. dh #define MAX(X,y) ((x)>(y))?(x):(y)

  • Die ausführliche *(s+i) ist nicht so einfach lesbar *s. Es besteht keine Notwendigkeit, den Code mit einem weiteren temporären Index zu verwenden und zu überladen i.

bekommt ()

Dies ist schlecht, da es den Eingabe-String-Puffer überlaufen lassen kann. Wenn die Puffergröße beispielsweise 2 beträgt und Sie 16 Zeichen eingeben, kommt es zu einem Überlauf str.

scanf()

Dies ist ebenso schlecht, da es den Eingabe-String-Puffer zum Überlaufen bringen kann.

Du erwähnst “Bei Verwendung der Funktion scanf() ist das Ergebnis völlig falsch, da das erste Zeichen anscheinend einen ASCII-Wert von -52 hat.

Das liegt an einer falschen Verwendung von scanf(). Ich war nicht in der Lage, diesen Fehler zu duplizieren.

fgets()

Dies ist sicher, da Sie garantieren können, dass der Eingabe-String-Puffer niemals überläuft, indem Sie die Puffergröße übergeben (die Platz für NULL enthält).

getline()

Ein paar Leute haben C vorgeschlagen POSIX-Standard getline() als Ersatz. Leider ist dies keine praktische portable Lösung, da Microsoft keine C-Version implementiert; nur das Standard-C++ String-Template-Funktion wie diese SO #27755191 Frage beantwortet. Microsofts C++ getline() war zumindest weit zurück als verfügbar Visual Studio 6 aber da das OP streng nach C und nicht nach C++ fragt, ist dies keine Option.

Sonstiges

Schließlich ist diese Implementierung dahingehend fehlerhaft, dass sie keinen Integer-Überlauf erkennt. Wenn der Benutzer eine zu große Zahl eingibt, kann die Zahl negativ werden! dh 9876543210 wird werden -18815698?! Lassen Sie uns das auch beheben.

Dies ist für an trivial zu beheben unsigned int. Wenn die vorherige Teilnummer kleiner als die aktuelle Teilnummer ist, dann haben wir einen Überlauf und geben die vorherige Teilnummer zurück.

Für ein signed int das ist etwas mehr arbeit. In Assembler könnten wir das Carry-Flag untersuchen, aber in C gibt es keine eingebaute Standardmethode, um einen Überlauf mit signed int math zu erkennen. Da wir glücklicherweise mit einer Konstanten multiplizieren, * 10können wir dies leicht erkennen, wenn wir eine äquivalente Gleichung verwenden:

n = x*10 = x*8 + x*2

Wenn x*8 überläuft, wird logischerweise auch x*10 überlaufen. Für einen 32-Bit-Int-Überlauf tritt auf, wenn x*8 = 0x100000000 ist, also müssen wir nur erkennen, wenn x >= 0x20000000 ist. Da wir nicht annehmen wollen, wie viele Bits an int müssen wir nur testen, ob die obersten 3 msb’s (Most Significant Bits) gesetzt sind.

Zusätzlich ist ein zweiter Überlauftest erforderlich. Wenn nach der Ziffernverkettung das msb gesetzt ist (Vorzeichenbit), dann wissen wir auch, dass die Zahl übergelaufen ist.

Code

Hier ist eine feste sichere Version zusammen mit Code, mit dem Sie spielen können, um einen Überlauf in den unsicheren Versionen zu erkennen. Ich habe auch sowohl a signed und unsigned Versionen über #define SIGNED 1

#include <stdio.h>
#include <ctype.h> // isdigit()

// 1 fgets
// 2 gets
// 3 scanf
#define INPUT 1

#define SIGNED 1

// re-implementation of atoi()
// Test Case: 2147483647 -- valid    32-bit
// Test Case: 2147483648 -- overflow 32-bit
int StringToInt( const char * s )
{
    int result = 0, prev, msb = (sizeof(int)*8)-1, overflow;

    if( !s )
        return result;

    while( *s )
    {
        if( isdigit( *s ) ) // Alt.: if ((*s >= '0') && (*s <= '9'))
        {
            prev     = result;
            overflow = result >> (msb-2); // test if top 3 MSBs will overflow on x*8
            result  *= 10;
            result  += *s++ & 0xF;// OPTIMIZATION: *s - '0'

            if( (result < prev) || overflow ) // check if would overflow
                return prev;
        }
        else
            break; // you decide SKIP or BREAK on invalid digits
    }

    return result;
}

// Test case: 4294967295 -- valid    32-bit
// Test case: 4294967296 -- overflow 32-bit
unsigned int StringToUnsignedInt( const char * s )
{
    unsigned int result = 0, prev;

    if( !s )
        return result;

    while( *s )
    {
        if( isdigit( *s ) ) // Alt.: if (*s >= '0' && *s <= '9')
        {
            prev    = result;
            result *= 10;
            result += *s++ & 0xF; // OPTIMIZATION: += (*s - '0')

            if( result < prev ) // check if would overflow
                return prev;
        }
        else
            break; // you decide SKIP or BREAK on invalid digits
    }

    return result;
}

int main()
{
    int  detect_buffer_overrun = 0;

    #define   BUFFER_SIZE 2    // set to small size to easily test overflow
    char str[ BUFFER_SIZE+1 ]; // C idiom is to reserve space for the NULL terminator

    printf(" Enter some numbers (no spaces): ");

#if   INPUT == 1
    fgets(str, sizeof(str), stdin);
#elif INPUT == 2
    gets(str); // can overflows
#elif INPUT == 3
    scanf("%s", str); // can also overflow
#endif

#if SIGNED
    printf(" Entered number is: %d\n", StringToInt(str));
#else
    printf(" Entered number is: %u\n", StringToUnsignedInt(str) );
#endif
    if( detect_buffer_overrun )
        printf( "Input buffer overflow!\n" );

    return 0;
}

  • Das strlen() Die Funktion prüft nicht, ob Sie einen Nullzeiger übergeben haben. Die Standard-C-Bibliotheksspezifikation sagt ausdrücklich (§7.1.4 Verwendung von Bibliotheksfunktionen): Wenn ein Argument für eine Funktion einen ungültigen Wert hat (z. B. einen Wert außerhalb der Domäne der Funktion oder einen Zeiger außerhalb des Adressraums des Programms oder einen Nullzeiger oder einen Zeiger auf einen nicht änderbaren Speicher, wenn der entsprechende Parameter ist nicht konstant qualifiziert) oder ein Typ (nach der Heraufstufung), der von einer Funktion mit variabler Anzahl von Argumenten nicht erwartet wird, ist das Verhalten undefiniert. Es ist vernünftig, einen Nicht-Null-Zeiger zu verlangen.

    – Jonathan Leffler

    10. Juli 2015 um 3:33 Uhr

  • Es ist besser, einen einzeiligen Sicherheitscheck hinzuzufügen und Flüchtigkeitsfehler abzufangen, als anzunehmen, dass ein Anrufer sie nicht machen wird, aber danke für den Kapitelvers der Spezifikation!

    – Michaelangel007

    11. Juli 2015 um 1:14 Uhr

Benutzeravatar von Matthew Flaschen
Matthäus Flaschen

Sie haben Recht, dass Sie niemals verwenden sollten gets. Wenn Sie verwenden möchten fgetskönnen Sie den Zeilenumbruch einfach überschreiben.

char *result = fgets(str, sizeof(str), stdin);
char len = strlen(str);
if(result != NULL && str[len - 1] == '\n')
{
  str[len - 1] = '\0';
}
else
{
  // handle error
}

Dies setzt voraus, dass keine eingebetteten NULL-Werte vorhanden sind. Eine weitere Option ist POSIX getline:

char *line = NULL;
size_t len = 0;
ssize_t count = getline(&line, &len, stdin);
if(count >= 1 && line[count - 1] == '\n')
{
  line[count - 1] = '\0';
}
else
{
  // Handle error
}

Der Vorteil zu getline Es übernimmt die Zuweisung und Neuzuweisung für Sie, behandelt mögliche eingebettete NULL-Werte und gibt die Anzahl zurück, damit Sie keine Zeit damit verschwenden müssen strlen. Beachten Sie, dass Sie kein Array mit verwenden können getline. Der Zeiger muss sein NULL oder frei-fähig.

Ich bin mir nicht sicher, welches Problem Sie haben scanf.

Verwenden Sie niemals gets(), da dies zu unvorhersehbaren Überläufen führen kann. Wenn Ihr String-Array die Größe 1000 hat und ich 1001 Zeichen eingebe, kann ich Ihr Programm überlaufen lassen.

  • Vielen Dank für Ihre Antworten. Sie waren sehr hilfreich. Aber ich würde auch gerne wissen, warum scanf() in diesem Programm nicht funktioniert? Vielen Dank.

    – Markus

    21. Juli 2010 um 18:13 Uhr

Versuchen Sie, fgets() mit dieser modifizierten Version Ihres CharToInt() zu verwenden:

int CharToInt(const char *s)
{
    int i, result, temp;

    result = 0;
    i = 0;

    while(*(s+i) != '\0')
    {
        if (isdigit(*(s+i)))
        {
            temp = *(s+i) & 15;
            result = (temp + result) * 10;
        }
        i++;
    }

    return result / 10;
}

Es validiert im Wesentlichen die Eingabeziffern und ignoriert alles andere. Dies ist sehr roh, also modifizieren Sie es und salzen Sie nach Geschmack.

  • Vielen Dank für Ihre Antworten. Sie waren sehr hilfreich. Aber ich würde auch gerne wissen, warum scanf() in diesem Programm nicht funktioniert? Vielen Dank.

    – Markus

    21. Juli 2010 um 18:13 Uhr

Benutzeravatar von fhdrsdg
fhdrsdg

Ich bin also kein großer Programmierer, aber lassen Sie mich versuchen, Ihre Frage zu beantworten scanf();. Ich denke, das Scanf ist ziemlich gut und verwende es für fast alles, ohne Probleme zu haben. Aber Sie haben einen nicht ganz korrekten Aufbau genommen. Es sollte sein:

char str[MAX];
printf("Enter some text: ");
scanf("%s", &str);
fflush(stdin);

Wichtig ist das “&” vor der Variable. Es teilt dem Programm mit, wo (in welcher Variable) der gescannte Wert gespeichert werden soll. das fflush(stdin); löscht den Puffer aus der Standardeingabe (Tastatur), sodass Sie weniger wahrscheinlich einen Pufferüberlauf bekommen.

Und der Unterschied zwischen gets/scanf und fgets ist das gets(); und scanf(); nur bis zum ersten Leerzeichen scannen ' ' während fgets(); scannt die gesamte Eingabe. (Aber reinigen Sie den Puffer danach unbedingt, damit Sie später keinen Überlauf bekommen)

  • Das Weglassen des & vor str ist völlig in Ordnung, da in C Arrays per Zeiger übergeben werden. Das ist, scanf( "%s", str ); ist genau gleichbedeutend mit scanf( "%s", &str[0] );

    – Michaelangel007

    8. Juli 2015 um 20:10 Uhr


  • Diese Antwort ist in mehrfacher Hinsicht falsch und gefährlich.

    – Herr

    8. Juli 2015 um 21:15 Uhr

  • Um genau zu sein: (1) die & vor dem str ist nicht erforderlich und kann Warnungen von einem gebildeten Compiler generieren; (2) Du solltest was testen scanf() Rücksendungen, um sicherzustellen, dass Sie die erwarteten Daten erhalten; (3) verwenden fflush(stdin) wird von Standard C nicht unterstützt — es funktioniert nur auf einigen Plattformen, insbesondere Microsoft; (4) gets() liest bis zum Zeilenende (ohne Schutz vor Überläufen); (5) fgets() scannt nicht die gesamte Eingabe – es liest bis zum Ende der Zeile oder bis kein Platz mehr im Puffer ist; (6) scanf() kann den Puffer überlaufen lassen — use scanf("%99s", str) wenn MAX==100.

    – Jonathan Leffler

    10. Juli 2015 um 3:12 Uhr


1406870cookie-checkC – scanf() vs. gets() vs. fgets()

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy