7.1. Die while-Schleife
Ein häufig gebrauchtes Mittel zur Bildung von Schleifen in C ist die while-Anweisung. Die Syntax einer while-Schleife lautet:
while(Bedingung)
Schleifenkörper;
Der Schleifenkörper kann aus einer einzelnen Anweisung, einem Anweisungsblock oder auch aus einer leeren Anweisung bestehen.
Bei der Ausführung einer while-Schleife wird zuerst die Bedingung getestet; liefert diese einen Wert ungleich 0, was logisch "wahr" bedeutet, dann werden die Anweisungen des Schleifenkörpers ausgeführt. Wenn die Bedingung gleich beim ersten Durchlauf "falsch" ist, werden die abhängigen Anweisungen überhaupt nie abgearbeitet. Die while-Schleife gilt deshalb als "abweisende
"
Schleife.Nach der Ausführung des Schleifenkörpers wird die Bedingung der while-Anweisung erneut geprüft. Falls diese wiederum den Wert "wahr" liefert, erfolgt ein weiterer Schleifendurchlauf. Der Vorgang wiederholt sich so lange, bis der Kontrollausdruck den Wert 0 — also den Wahrheitswert "falsch" — liefert. Der Wahrheitswert des Kontrollausdrucks kann sich natürlich nur dann ändern, wenn Anweisungen des Schleifenkörpers einen oder mehrere Operanden des Kontrollausdrucks manipulieren. Diese Manipulation erfolgt durch das Programm selbst (durch Veränderung von Variablenwerten) oder durch Einflüsse von außen (wie Benutzereingaben oder Input aus Dateien/Geräten).
Die bisherige Darstellung der while-Schleife läßt einige Übereinstimmungen mit einer if-Anweisung erkennen. Die folgende Tabelle liefert eine Aufstellung der Gemeinsamkeiten und Unterschiede zwischen while und if:
if | while | |
---|---|---|
Anweisungsblock (geschweifte Klammern) notwendig, wenn Körper aus mehr als einer Anweisung besteht? | ja | ja |
Abhängige Anweisungen werden ausgeführt, wenn Bedingung falsch? | nein | nein |
Abhängige Anweisungen können Kontrollausdruck beeinflussen? | nein | ja |
Anzahl der maximalen Durchläufe | 1 | unbegrenzt |
Anzahl der minimalen Durchläufe | 0 | 0 |
Semikolon nach Bedingung? | nein | ungewöhnlich |
Alternativer Ausführungszweig? | else | nein |
Verschachtelung möglich | ja | ja |
Nach diesen eher allgemeinen Erörterungen wollen wir uns while anhand eines Beispiels ansehen. Damit Programme übersichtliche Bildschirmausgaben tätigen können, löschen sie in der Regel den Bildschirm. Der ANSI-Standard definiert dafür jedoch keine Funktion. Wir können eine derartige, überall ausführbare Funktion selbst definieren. Diese müßte (bei einer Standardanzeige mit 25 Zeilen) eigentlich nur 25 mal einen Zeilenvorschub bewirken. Diese einfache Routine zum Löschen des Bildschirms ließe sich unter Verwendung von while folgendermaßen formulieren:
/* clrscr.c
* Programm schreibt 25x '\n' auf den Bildschirm
* Der Zähler iLineCount wird mit 1 initialisiert
* und bei jedem Schleifendurchlauf um 1
* inkrementiert. Erreicht er den Wert 26, dann
* wird die while-Schleife beendet.
*/
#include <stdio.h>
int main(void){
int iLineCount = 1;
while( iLineCount <= 25 ){
putchar('\n');
iLineCount++;
/* Schleifenzähler bei jedem Durchlauf um 1
* erhöhen, sonst ergibt sich eine Endlosschleife
*/
}
return 0;
}
In diesem Beispiel steht die Anzahl der Schleifendurchläufe von Anfang an fest. Die Variable iLineCount wird noch vor der while-Schleife mit dem Wert 1 initialisiert, und bei jedem Schleifendurchlauf um den Wert 1 inkrementiert. Beim 25. Schleifendurchlauf erreicht sie den Wert 26, was bei der nächsten Überprüfung des Kontrollausdrucks zum logischen Wert "falsch" und zum Abbruch der Schleife führt.
Die Anweisungen des Schleifenkörpers führen neben der Erhöhung des Zählers (iLineCount++) die eigentliche Arbeit dieses Programms durch, nämlich die Ausgabe von Zeilenvorschüben auf dem Bildschirm (putchar('\n')).
Unser ANSI-konformes Löschen des Bildschirmes hat leider den Schönheitsfehler, daß der Cursor in der letzten Zeile stehen bleibt. Die meisten Entwickler greifen daher für diesen Zweck auf proprietäre Routinen der Compiler-Hersteller zurück. Bei Borland heißt sie beispielsweise clrscr(), bei Microsoft _clearscreen().
Versierte C-Programmierer würden clrscr.c kopakter formulieren und das Inkrementieren der Zählervariablen mit in den Kontrollausdruck der while-Anweisung aufnehmen:
.
.
int iLineCount = 1;
while( iLineCount++ <= 25 )
putchar('\n');
.
.
Bekanntlich stehen Sie beim Inkrement- bzw. Dekrementoperator immer vor der Wahl, ob Sie die Präfix- oder Postfixnotation verwenden. Sie sollten sich darüber im klaren sein, zu welchen Ergebnissen die jeweilige Variante führt. Das etwas vorsichtiger formulierte erste Programmbeispiel räumt eine eigene Anweisung für die Erhöhung des Zählers ein: daher spielt es dort keine Rolle, welche Notation des Inkrementoperators eingesetzt wird.
Ganz anders verhält es sich bei der kompakteren zweiten Version: Die Verwendung der Postfixvariante bewirkt, daß die Vergleichsoperation vor der Erhöhung des Variablenwerts ausgeführt wird. Auf diese Weise wird die korrekte Anzahl von 25 Schleifendurchläufen erzielt. Der Einsatz der Präfixvariante würde dagegen bewirken, daß die Variable iLineCount immer zuerst um 1 inkrementiert und erst danach mit dem Wert 25 verglichen würde. Das hieße, daß diese Zählervariable beim ersten Vergleich bereits den Wert 2 hätte und daher insgesamt nur 24 Schleifendurchläufe zu erwarten wären.
Zusätzlich beachtenswert ist die Tatsache, daß bei der ersten Programmversion der Wert von iLineCount erst am Ende des Scheifenkörpers erhöht wird; demgegenüber besitzt diese Variable in der zweiten Version während des ganzen Schleifenablaufs den bereits erhöhten Wert. Wird der Schleifenzähler innerhalb des Schleifenkörpers z.B. für arithmetische Operationen herangezogen, dann könnte dieser Unterschied eine Quelle unerwünschter Nebeneffekte sein.
Im nächsten Beipiel wollen wir einen einfachen Filter erstellen, der die Bildschirmausgabe nach jeder Seite anhält — er soll also dem Dienstprogramm more nachempfunden sein, wie es zum Lieferumfang von DOS, OS/2 oder UNIX gehört. Auch dieses Beispiel geht der Einfachheit halber davon aus, daß die Standardeinstellung mit 25 Zeilen pro Bildschirmseite vorliegt. Da wir zum momentanen Zeitpunkt noch nicht über die Mittel verfügen, um die Eingabe zeilenweise zu lesen, erfolgt diese Zeichen für Zeichen. Dieses Verfahren verursacht zwar kaum zusätzlichen Programmieraufwand, würde aber alleine schon wegen der geringeren Ablaufgeschwindigkeit in der Praxis gemieden.
Vorerst wollen wir unsere Problemstellung in natürlicher Sprache formulieren:
- Das Programm soll so lange Zeichen einlesen, bis das Dateiende-Zeichen auftritt.
- Ist das eingelesene Zeichen ein Zeilenvorschub, soll der Zeilenzähler um 1 erhöht werden.
- Beträgt der Wert des Zeilenzählers 24 oder ein Vielfaches davon, dann soll die Bildschirmausgabe angehalten und in der Zeile 25 ein Text wie "Bitte Taste drücken" ausgegeben werden.
- Das zuletzt eingelesene Zeichen soll auf den Bildschirm ausgegeben werden.
Offensichtlich benötigt man zur Lösung des ersten Teilproblems eine Schleife. Innerhalb der Schleife müssen nacheinander Zeichen eingelesen werden und jedes davon muß in der Schleifenbedingung mit dem Wert EOF (End Of File, eine in stdio.h definierte Konstante) verglichen werden.
#include <stdio.h> /* wg. getchar() und EOF */
int main(void){
unsigned char cCharIn = '\0';
/* Variable mit '\0' initialisieren */
while( cCharIn != EOF ){
cCharIn = getchar();
}
return 0;
}
Der bisherige Code leistet das Einlesen von Zeichen, bis die Dateiende-Marke erreicht wird. Im nächsten Schritt wollen wir einen Zeilenzähler einrichten und alle eingelesenen Zeichen auf dem Bildschirm ausgeben.
#include <stdio.h> /* wg. getchar() und EOF */
int main(void){
int iLineCount = 0;
/* Zeilenzähler mit Anfangswert 0 */
unsigned char ucCharIn = '\0';
/* Variable mit '\0' initialisieren, damit
* (ucCharIn != EOF) nicht beim ersten
* Schleifendurchlauf falsch sein kann
*/
while( ucCharIn != EOF ){
ucCharIn = getchar();
if (ucCharIn == '\n')
/* Wenn ein Zeilenvorschub vorliegt...*/
iLineCount++;
/* ... Zeilenzähler erhöhen */
putchar( ucCharIn );
}
return 0;
}
Nun sind wir zwar über die Anzahl der Zeilenumbrüche auf dem laufenden, allerdings nutzen wir dieses Wissen noch nicht aus, um die Bildschirmausgabe nach jeder Seite anzuhalten. Bevor wir dieses tun können, müssen wir herausfinden, ob bereits 24 Zeilen auf den Bildschirm ausgegeben wurden — ob also der Wert von iLineCount ein Vielfaches von 24 beträgt. Dies läßt sich mittels einer Modulo-Division bewerkstelligen: iLineCount % 24 ergibt immer dann den Wert 0, wenn ein Vielfaches von 24 vorliegt.
Um den Bildschirm anzuhalten, benötigen wir eine Funktion, die auch bei der Umleitung der Eingabe beharrlich auf das Drücken einer Taste wartet. Auch eine solche Funktion ist allerdings nicht durch den ANSI-Standard definiert; getchar() ist zu diesem Zweck nicht geeignet, da es bei einem Aufruf wie more < readme.txt nicht auf eine Benutzereingabe wartet, sondern ausschließlich Zeichen aus der Datei readme.txt einliest. Wir benutzen hier die Funktion getch(), die bei einigen Compilern diese Aufgabe erfüllt. Falls Sie unter DOS arbeiten, müssen Sie dem Handbuch Ihres Compilers entnehmen, welche Funktion für die direkte Abfrage der Tastatur zur Verfügung steht. Bei Turbo C bietet sich dafür der Aufruf bioskey(1) an, bei Microsoft C _bios_keybrd(_KEYBRD_READ); für beide Aufrufe müssen Sie die Anweisung #include <bios.h> einfügen.
#include <stdio.h> /* wg. getchar() und EOF */
#include <conio.h> /* wg. getch() */
int main(void){
int iLineCount = 0;
char cCharIn = '\0';
/* Variable gleich mit '\0' initialisieren */
while( cCharIn != EOF ){
cCharIn = getchar();
if (cCharIn == '\n' && ++iLineCount % 24 == 0){
/* wenn '\n' vorliegt, iLineCount inkrementieren
* und anschließend Modulo-Division durchführen
*/
printf("\nWeiter mit einer Taste ...");
getch();
/* bei Turbo C stattdessen bioskey(1), bei
* Microsoft C _bios_keybrd(_KEYBRD_READ)
* einfügen
*/
}
putchar( cCharIn );
}
return 0;
}
Bemerkenswert ist der Kontrollausdruck der if-Anweisung: Dort findet der Vergleich der Variablen cCharIn mit '\n' statt, zusätzlich leistet die Inkrementierung und Überprüfung der Variablen iLineCount. Die Vorgangsweise scheint auf den ersten Blick kompliziert, als Alternative bietet sich aber nur eine geschachtelte if-Anweisung an.
Aufgrund der Auswertungsreihenfolge von &&-Verknüpfungen (siehe Abschnitt 6.5.1) ist gewährleistet, daß der Teilausdruck ++iLineCount % 24 == 0 nur dann berücksichtigt wird, wenn cCharIn == '\n' wahr ist, also ein Zeilenvorschub vorliegt. In diesem Fall wird wegen der Präfixstellung des Operators ++ zuerst die Zahl der Zeilenvorschübe aktualisiert und diese dann der Modulo-Division unterzogen. Ergibt sie das Resultat 0, dann liegt ein Vielfaches von 24 vor.
Bisher benutzen wir den Aufruf von getch() ausschließlich dazu, um die Bildschirmausgabe anzuhalten. Wir könnten aber bei dieser Gelegenheit auch Benutzereingaben auswerten und zur Steuerung des Programms heranziehen. Beispielsweise stünde es dem Benutzer auf diese Art offen, das Programm mit der Eingabe von q zu beenden. Schließlich wollen wir noch die Gesamtzahl der Zeilen bekanntgeben; dies läßt sich einfach durch einen Aufruf von printf() erledigen. Unsere endgültige Version von more.c stellt sich dann folgendermaßen dar:
/*****************************************************
* more.c *
* Filter zur seitenweisen Bildschirmausgabe *
* geht von Standardanzeige mit 25 Zeilen aus *
* Verwendung: *
* more < meine.dat oder type meine.dat|more *
*****************************************************/
#include <stdio.h> /* wg. getchar() und putchar() */
#include <conio.h> /* wg. getch() */
int main(void){
int iLineCount=0;
char cInChar, cKeyPressed = '\0';
while((cInChar = getchar()) != EOF &&
cKeyPressed != 'q'){
/* Schleife läuft so lange, bis EOF im Eingabestrom
* auftaucht oder Benutzer Taste 'q' drückt
*/
if( cInChar == '\n' && ++iLineCount % 24 ==0){
/* Wenn Zeilenvorschub, dann Zeilenzähler
* erhöhen und modulo 24 dividieren
* Falls Ergebnis 0, Meldung in letzte
* Zeile schreiben und auf Benutzerein-
* gabe warten
*/
printf("\n--- Weiter mit einer Taste,"
"beenden mit \'q\' ---");
cKeyPressed = getch();
/* Statt getch() bei Turbo C bioskey(1),
* bei MS C _bios_keybrd(_KEYBRD_READ)
*/
}
putchar(cInChar);
/* Zeichen ausgeben */
}
printf("\n%d Zeilen gezählt", iLineCount);
/* Anzahl der Zeilen ausgeben */
return 0;
}
Gegenüber der letzten Version finden sich hier noch ein paar zusätzliche Änderungen. Es ist allgemein üblich, Anweisungen zur Schleifensteuerung zusammenzufassen und nach Möglichkeit in die Kontrollbedingung aufzunehmen. In unserem Beispiel findet sich der Aufruf von getchar(), der einen der Vergleichsoperanden liefert, ebenso im Ausdruck nach der while-Anweisung wie die Überprüfung der Variablen cKeyPressed auf Übereinstimmung mit 'q'. Wichtig ist bei diesem Vorgehen, daß die Zuweisung (cInChar = getchar()) in Klammern gesetzt wird; andernfalls wird aufgrund der höheren Priorität von != zuerst der Vergleich getchar() != EOF ausgewertet und dessen Ergebnis (entweder 0 oder 1) der Variablen cInChar zugewiesen. Die Aufnahme von cInChar = getchar() in den Kontrollausdruck hat gleichzeitig den angenehmen Nebeneffekt, daß die Variable cInChar nicht mehr initialisiert werden muß, da ihr schon vor dem ersten Vergleich ein Wert durch die Funktion getchar() zugewiesen wird.