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

Ich habe ein ziemlich einfaches Programm zum Umwandeln einer Zeichenkette (unter der Annahme, dass Zahlen eingegeben wurden) in eine Ganzzahl gemacht.

Nachdem ich fertig war, bemerkte ich einige sehr seltsame “Bugs”, die ich nicht beantworten kann, hauptsächlich aufgrund meiner begrenzten Kenntnisse darüber, wie die functionen scanf() , gets() und fgets() funktionieren. (Ich habe eine Menge Literatur gelesen.)

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

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

Also hier ist das Problem, das ich habe. Erstens, wenn die function gets() funktioniert das Programm perfekt.

Zweitens, wenn Sie fgets() , ist das Ergebnis etwas falsch, weil die function fgets() scheinbar das fgets() newline (ASCII-Wert 10) liest, was das Ergebnis schraubt.

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

Jetzt weiß ich, dass gets() entmutigt ist zu verwenden, also würde ich gerne wissen, ob ich fgets() hier verwenden kann, damit es Newline-Zeichen nicht liest (oder ignoriert). Was ist mit der function scanf() in diesem Programm?

   
  • Benutze niemals gets . Es bietet keinen Schutz vor einer Pufferüberlauf-Schwachstelle (das heißt, Sie können nicht sagen, wie groß der Puffer ist, den Sie übergeben), so dass ein Benutzer nicht in eine Zeile größer als der Puffer und Speicher übergehen kann.

  • Vermeiden Sie die Verwendung von scanf . Wenn es nicht sorgfältig verwendet wird, kann es die gleichen Pufferüberlaufprobleme wie gets . Selbst wenn man das ignoriert, hat es andere Probleme, die es schwierig machen, korrekt zu verwenden .

  • Im Allgemeinen sollten fgets stattdessen fgets verwenden, obwohl es manchmal unbequem ist (Sie müssen die Zeilenumbrüche entfernen, müssen Sie vorher eine Puffergröße bestimmen, und dann müssen Sie herausfinden, was mit zu langen Zeilen zu tun ist – behalten Sie den Teil Sie lesen und vercasting den Überschuss , vercasting das Ganze, bauen den Puffer dynamisch an und versuchen es erneut, usw.). Es gibt einige Nicht-Standard-functionen, die diese dynamische Zuweisung für Sie übernehmen (zB getline auf POSIX-Systemen, Chuck Falconers Public-Domain- ggets function). Beachten Sie, dass ggets ähnliche Semantiken hat, indem es einen abschließenden Zeilenumbruch für Sie entfernt.

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

Eine einfache (wenn auch seltsame) Möglichkeit, die nach dem Lesen mit fgets folgende nachgestellte Zeile aus einem Puffer zu fgets ist: strtok(buffer, "\n"); Dies ist nicht so, wie strtok verwendet werden soll, aber ich habe es öfter benutzt als in der beabsichtigten Art und Weise (was ich im Allgemeinen vermeide).

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

  • Zunächst sollte CharToInt() in das richtige StringToInt() da es in einem String arbeitet, der kein einzelnes Zeichen ist.

  • Die function CharToInt() [sic.] CharToInt() unsicher. Es überprüft nicht, ob der Benutzer versehentlich einen Nullzeiger übergibt.

  • Die Eingabe wird nicht validiert, oder die Eingabe wird nicht richtig übersprungen. Wenn der Benutzer eine Nicht-Ziffer eingibt, enthält das Ergebnis einen falschen Wert. Dh wenn Sie N eingeben, wird der Code *(s+i) & 15 14 ergeben!

  • Als nächstes sollte das unscheinbare CharToInt() in CharToInt() [sic.] Als digit da es das ist, was es wirklich ist.

  • Auch das Kludur- return result / 10; ist genau das – ein schlechter Hack , um eine errorshafte Implementierung zu umgehen.

  • Ebenso wird MAX schlecht benannt, da es mit der Standardnutzung zu kollidieren scheint. dh #define MAX(X,y) ((x)>(y))?(x):(y)

  • Das wortreiche *(s+i) ist nicht so lesbar wie einfach *s . Es besteht keine Notwendigkeit, den Code mit einem weiteren temporären Index zu verwenden und zu überdecken.

bekommt ()

Das ist schlecht, weil es den Eingabezeichenfolgenpuffer überlaufen kann. Wenn die Puffergröße beispielsweise 2 ist und Sie 16 Zeichen eingeben, wird str überströmt.

scanf ()

Dies ist ebenso schlecht, weil es den Eingabezeichenfolgenpuffer überlaufen kann.

Sie erwähnen ” wenn Sie scanf () verwenden, ist das Ergebnis völlig falsch, weil das erste Zeichen anscheinend einen ASCII-Wert von -52 hat.

Dies liegt an einer falschen Verwendung von scanf (). Ich konnte diesen Fehler nicht duplizieren.

fgets ()

Dies ist sicher, da Sie garantieren können, dass Sie den Eingabestring-Puffer niemals überlaufen lassen, indem Sie die Puffergröße (die Platz für den NULL enthält) übergeben.

getline ()

Einige Leute haben den C POSIX-Standard getline() als Ersatz vorgeschlagen. Leider ist dies keine praktische portable Lösung, da Microsoft keine C-Version implementiert; Nur die Standard-C ++ – Zeichenfolgenschablone funktioniert, da diese SO 27755191 Frage beantwortet. Microsofts C ++ getline() war zumindest weit zurück als Visual Studio 6 verfügbar, aber da das OP strikt nach C und nicht nach C ++ fragt, ist dies keine Option.

Verschiedenes

Schließlich ist diese Implementierung errorshaft, da sie keinen Ganzzahlüberlauf erkennt. Wenn der Benutzer eine zu große Zahl eingibt, kann die Zahl negativ werden! dh 9876543210 wird -18815698 ?! Lasst uns das auch reparieren.

Dies ist trivial für einen unsigned int zu beheben. Wenn die vorherige Teilnummer kleiner als die aktuelle Teilnummer ist, sind wir übergelaufen und wir geben die vorherige Teilnummer zurück.

Für einen signed int ist das ein bisschen mehr Arbeit. In der Assembly konnten wir das Carry-Flag überprüfen, aber in C gibt es keine eingebaute Standardmethode, um Überlauf mit vorzeichenbehafteten Int-Mathe zu erkennen. Glücklicherweise, da wir mit einer Konstanten, * 10 , multiplizieren, können wir dies leicht erkennen, wenn wir eine äquivalente Gleichung verwenden:

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

Wenn x * 8 überläuft, wird auch logisch x * 10. Für einen 32-Bit-Int-Überlauf wird passieren, wenn x * 8 = 0x100000000, also müssen wir nur feststellen, wenn x> = 0x20000000. Da wir nicht annehmen wollen, wie viele Bits ein int hat, müssen wir nur testen, ob die obersten 3 msbs (Most Significant Bits) gesetzt sind.

Zusätzlich wird ein zweiter Überlauftest benötigt. Wenn das Msb nach der Ziffernverkettung gesetzt ist (Vorzeichenbit), 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 Überlauf in den unsicheren Versionen zu erkennen. Ich habe auch eine signed und eine unsigned Version über #define SIGNED 1

 #include  #include  // 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; } 

Du hast Recht, dass du niemals gets benutzen solltest. Wenn Sie fgets verwenden fgets , können Sie einfach den Zeilenumbruch ü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 NULLs 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 von getline ist, dass es die Zuweisung und Neuzuweisung für Sie übernimmt. Es verarbeitet mögliche eingebettete NULL-Werte und gibt die Anzahl zurück, so dass Sie keine Zeit mit strlen verschwenden strlen . Beachten Sie, dass Sie ein Array mit getline nicht verwenden getline . Der pointers muss NULL oder frei verfügbar sein.

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

Verwenden Sie niemals gets (), es kann zu nicht mehr überprüfbaren Überläufen führen. Wenn Ihr String-Array eine Größe von 1000 hat und 1001 Zeichen eingeben, kann ich einen Pufferüberlauf in Ihrem Programm durchführen.

Probieren Sie fgets () mit dieser modifizierten Version Ihres CharToInt ():

 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 überprüft im Wesentlichen die eingegebenen Ziffern und ignoriert alles andere. Dies ist sehr roh, also modifizieren und abschmecken.

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

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

Das “&” vor der Variable ist wichtig. Er teilt dem Programm mit, in welcher Variablen der gescannte Wert gespeichert werden soll. der fflush(stdin); löscht den Puffer von der Standardeingabe (Tastatur), so dass es weniger wahrscheinlich ist, dass ein Pufferüberlauf auftritt.

Und der Unterschied zwischen gets / scanf und fgets ist, dass gets(); und scanf(); fgets(); nur bis zum ersten Leerzeichen ' ' während fgets(); scannt die gesamte Eingabe. (aber putze den Puffer danach, damit du später keinen Überlauf bekommst)