Frage #2

Wie und wieso soll ich die Standardeingabe löschen?

Antwort:

Betrachtet folgende Programme:

#include <stdio.h>
 
int main()
{
  int x, y;
 
  printf("Bitte geben Sie einen Summanden ein: ");
  scanf("%d", &x);
 
  printf("Bitte geben Sie noch einen Summanden ein: ");
  scanf("%d", &y);
 
  printf("%d + %d = %d\n", x, y, x + y);
 
  printf("Bitte druecken Sie Enter um zu beenden: ");
  getchar();
 
  return 0;
}
#include <iostream>
 
int main()
{
  int x, y;
 
  std::cout << "Bitte geben Sie einen Summanden ein: ";
  std::cin  >> x;
 
  std::cout << "Bitte geben Sie noch einen Summanden ein: ";
  std::cin  >> y;
 
  std::cout << x << " + " << y << " = " << x + y << '\n';
 
  std::cout << "Bitte druecken Sie Enter um zu beenden: ";
  std::cin.get();
 
  return 0;
}

Diese zwei Programme, die genau das gleiche tun enthalten einen typischen Anfängerfehler. Der Gedanke hinter den Programmen ist Folgender: „Ich lese eine Zahl ein, dann die Nächste, und dann warte ich bis der User Enter drückt“.
Dieses Programm laufen zu lassen ruft aber zwei Überraschungen hervor: wenn man zwei Zahlen eingibt, dann wartet das Programm nicht bis man Enter drückt, sondern beendet sich sofort.
Wenn man bei der ersten Aufforderung einen Buchstaben statt einer Zahl eingibt, dann wird die zweite Zahl gar nicht mehr abgefragt. Der Wert der beiden Zahlen ist „seltsam“, und manchmal ist nicht mal das Ergebnis der einfachen Addition richtig.
Bei den meisten Anfängern herrscht nun Verwirrung, die Welt der Logik scheint zusammengebrochen zu sein.

Ich kann euch versichern: dem ist nicht so. Der Schuldige ist der Eingabepuffer (auch „Anfängerschreck“ genannt).
Sehen wir uns das Ganze im Detail noch mal an:
scanf(), das übrigens wie auch std::cin zur Gattung „Böse Überraschungen“ gehört, interessiert sich nämlich für diejenigen Zeichenfolgen, die zu den Formatierungsargumenten passen.
Und zwar nur dafür. Wenn ihr die tasten „1“ und „2“ drückt, wenn ihr Zwölf eintippen wollt, wandern diese Zeichen erstmal in den Eingabepuffer. Dort bleiben sie so lange, bis jemand „Enter“ drückt. Das kommt daher, dass die Standardeingabe zeilengepuffert ist. Das ist übrigens auch der Grund wieso ihr „Enter“ drücken müsst, damit es weitergeht.
Nun, wenn ihr dann Enter drückt kommt erst mal ein Newline Zeichen '\n' in den Eingabepuffer hinein. Zu diesem Zeitpunkt sieht der Eingabepuffer so aus: „12\n“.
scanf() und cin» interessieren sich nur für die '1' und die '2', denn das ist für sie schon eine Zahl. Diese Zeilen lesen sie, und entfernen sie aus dem Eingabepuffer, und das wars dann auch schon. Das Newline bleibt aber im Eingabepuffer! Nun gehts weiter: der nächste Aufruf von scanf() und std::cin» ignoriert alle Whitespace Zeichen (Leerzeichen, Tabs, Newlines…), also eben auch das Newline das im Speicher ist. Ihr gebt eine Zahl ein, diese wird eingelesen, und das nächste Newline bleibt auch im Puffer.

Zum Schluss kommt der Aufruf nach getchar()/cin.get(). getchar() liest nur ein Zeichen bis zum nächsten Newline ein und cin.get() ebenso.
Also, ein Newline ist bereits im Puffer. Das wird von getchar()/cin.get() gelesen, und beide sind glücklich und zufrieden, auch ganz ohne den Benutzer.
Das ist der Grund für das erste Verhalten.


Versucht mal beim ersten Prompt ein (oder mehrere) Buchstaben einzugeben. Sehen wir uns an was da passiert. scanf()/cin» interessieren sich wie gesagt nur für die Zeichen, die dem Format entsprechen. Nun werden sie aber vor Zeichen gestellt, die nicht zum Format passen! Die beiden wollen ein int! Bekommen haben sie aber etwas anderes. Beleidigt kehren die beiden zurück, ohne ihre Arbeit verrichtet zu haben. x ist jetzt unverändert. Da wir vorher x aber nicht initialisiert haben ist x undefiniert! Das bedeutet es kann alles sein. Die Zeichen, die der User eingegeben hat bleiben im Puffer. Beim zweiten Aufruf versucht scanf()/cin» wieder die Zeichen zu lesen, schafft es wieder nicht, und das Ergebnis ist das gleiche wie vorher. Und wieso kann es jetzt sein, dass die Addition nicht stimmt? Wir haben gesagt dass der Inhalt der beiden Variablen undefiniert ist. Das bedeutet, es kann auch sein dass die Variablen beide einen sehr hohen (bzw. sehr niedrigen) Wert haben. Wenn man die beiden dann addiert kann es sein, dass die Variable in die das Ergebnis hineingeschrieben, wird den Wert nicht fassen kann (Variablen haben eine begrenzte größe und ein begrenztes Fassungsvermögen). Das nennt man „Overflow“ (bzw. „Underflow“), und ist eigentlich eine ganz andere Geschichte. Zwei uninitialisierte Variablen bedeutet eine Addition von zwei Zufallszahlen - unabhängig von der Größe der Variablen.

Tja, für getchar()/cin.get() gilt das gleiche wie schon oben. Es bedient sich einfach aus der reichhaltigen Auswahl im Eingabepuffer.

Jetzt wisst ihr, wieso scanf() und std::cin Artefakte sind, die von bösen Mächten erschaffen wurden. Aber wie löst man das Problem? Verwendet einfach cin» und scanf() nicht! Von der Standardeingabe zu lesen wird anscheinend in Übungsprogrammen für Anfänger gerne gemacht. Die korrekte Behandlung von Fehlern ist aber ganz und gar nicht einfach und erfordert einiges an Erfahrung.

Ihr wollt noch immer wissen, wie ihr richtig von der Standardeingabe lesen wollt? Na gut, da habt ihrs:
Erstens einmal müssen wir Fehler erkennen lernen. Dazu sehen wir uns scanf() in einer Referenz an, zum Beispiel hier. Beim Rückgabewert steht folgendes:

On success, the function returns the number of items succesfully read. This count can match the expected number of readings or fewer, even zero, if a matching failure happens.
In the case of an input failure before any data could be successfully read, EOF is returned.


Für uns bedeutet das, wir müssen nur überprüfen, ob der Rückgabewert auch tatsächlich der Anzahl der gewünschten Objekte entspricht:

...
int num_read;
...
 
printf("Bitte geben einen Summanden ein: ");
num_read = scanf("%d", &x);
if (num_read != 1)
{
    printf("Sie mussten eine Zahl eingeben!\n");
    return 1;
}

Damit stellen wir schon mal sicher, dass der Wert eingelesen wurde. Wir können auch so lange fragen, bis der User das eingibt, was wir wollten. Da bei einem Fehler die Zeichen aber im Puffer bleiben würden, würde das zu einer Endlosschleife führen. Wir brauchen eine Funktion, die den Eingabepuffer löscht. Der Code sieht so aus:

...
int num_read;
...
 
do {
    clear_stdin();
    printf("Bitte geben einen Summanden ein: ");
    num_read = scanf("%d", &x);
} while(num_read != 1);

clear_stdin() ist eine kleine selbstgeschriebene Funktion. Sie sieht so aus:

void clear_stdin()
{
    int ch; /* Wichtig! Muss int, und nicht char sein. */   
    while( (ch = getchar()) != '\n' && ch != EOF )
        /* Nichts tun */;
}

Diese Funktion liest alle Zeichen bis zum nächsten Newline (oder EOF) und verwirft sie wieder. Diese Funktion sollte vor jedem scanf() aufgerufen werden.


Sehen wir uns die Referenz von cin an: http://www.cplusplus.com/reference/iostream/istream/ Nach einigem Lesen stellen wir fest, dass man den Status der Standardeingabe über die Methode good() abfragen kann. Den Eingabepuffer kann man mit folgendem Code löschen:

cin.ignore(9999, '\n')

Dieser Code bedeutet: lies und ignoriere so lange alle Zeichen, bis '\n' vorkommt, aber höchstens 9999. Somit kann man alle überflüssigen Zeichen im Puffer entfernen. 9999 ist eine von mir zur Vereinfachung gewählte Zahl. Was ist wenn mehr als 9999 Zeichen drinnen sind? Ganz korrekt wäre folgendes:

#include <limits>
 
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');


So, die abgesicherte Version sieht so aus:

do {
    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    std::cout << "Bitte geben einen Summanden ein: ";
    std::cin  << x;
} while(!std::cin.good());

Wie ihr seht ist aus dem vermeintlich einfachen Programm eine Nachschlageübung geworden. Ich hoffe dieser kleine Exkurs hat euch genug frustriert, um ab jetzt cin>> und scanf() zu meiden (aber nicht genug, um C/C++ zu meiden).

Verbesserungsvorschläge

Hat dir diese Antwort geholfen? Wenn nicht oder wenn du Verbesserungs- bzw. Erweiterungsvorschläge hast, dann schreib das bitte einfach auf die Diskussionsseite.

Navigation

Zurück zur FAQ-Übersicht