7.2. Die for-Schleife

Die for-Schleife ist der while-Schleife in vieler Hinsicht ähnlich. Beide sind "abweisende" Schleifen, das heißt, die Schleifenbedingung wird noch vor dem ersten Schleifendurchlauf überprüft. Im Prinzip kann jede for-Schleife durch eine while-Schleife ausgedrückt werden und umgekehrt. Die for-Schleife wird eher bei zählergesteuerten Schleifen (mit konstanter Anzahl an Durchläufen) bevorzugt, wohingegen while-Schleifen dann Verwendung finden, wenn die Abbruchbedingung durch das Auftreten eines unzulässigen Werts erfüllt sein soll. In allgemeiner Form sieht eine for-Schleife wie folgt aus:


for( Zähler-Initialisierung; Bedingung; Zählerveränderung )
	 Schleifenkörper;

Die for-Schleife nimmt Ausdrücke in die Schleifenanweisung auf, die bei while gewöhnlich vor der Schleife oder im Schleifenkörper stehen. Das betrifft die Initialisierung des Schleifenzählers und dessen Inkrementierung bzw. Dekrementierung. Daher kann man sagen, eine übliche for-Schleife ist bedeutungsmäßig äquivalent mit folgender while-Schleife:


Zähler-Initialisierung;

while( Bedingung ){
	Anweisungen;
	Zählerveränderung;
	}

Als Anwendungsbeispiel für eine for-Schleife wollen wir ein Programm erstellen, das eine ASCII-Tabelle in zweispaltiger Formatierung ausgibt:


/***************************************************
 * asciitab.c                                      *
 * Gibt ASCII-Tabelle zweispaltig aus, beginnend   *
 * mit dem Wert 33                                 *
 ***************************************************/

#include <stdio.h>			/* wg printf() */

int main(void){

unsigned short usAsciiVal;

 /* Variable usAsciiVal mit 33 initialisieren,
  * weil Zeichen < 33 nicht angezeigt werden
  * sollen
  */
 for ( usAsciiVal = 33; usAsciiVal <= 255; usAsciiVal++ ){
	 if( usAsciiVal % 2 != 0)
	/* ungerade Werte in die linke ... */
		 printf("%c\t%d", usAsciiVal, usAsciiVal);
	 else
	/* ... und gerade in die rechte Spalte setzen */
		printf("\t\t\t%c\t%d\n", usAsciiVal, usAsciiVal);
	}
return 0;
}

Wie schon aus der allgemeinen Form der for-Schleife hervorgeht, übernimmt im allgemeinen der erste Ausdruck der Schleifenanweisung die Initialisierung des Schleifenzählers. Im vorliegenden Beispiel belegen wir usAsciiVal mit dem Anfangswert 33, weil wir auf die Darstellung von Steuer- und "Whitespace"-Zeichen verzichten wollen. Dieser erste Ausdruck wird nur einmal berücksichtigt, und zwar vor der ersten Auswertung der Bedingung (2. Ausdruck). Andernfalls würde der Zählervariablen bei jedem Schleifendurchlauf der Anfangswert zugewiesen, und aus der for-Schleife würde zwangsläufig eine Endlosschleife, da die Schleifenbedingung nie falsch werden kann.

Vor jedem Durchgang wird der zweite Ausdruck, also die Schleifenbedingung ausgewertet: Sobald sie den Wert 0 ergibt, bricht die Ausführung der Schleife ab. Im vorliegenden Beispiel sollen alle ASCII-Werte bis einschließlich 255 dargestellt werden. Daher fungiert dieser Wert als oberes Limit, und die Bedingung wird falsch, sobald usAsciiVal diesen Wert überschreitet.

Der dritte Ausdruck schließlich wird ebenfalls bei jedem Durchgang abgearbeitet. In der Regel wird dort der Schleifenzähler verändert. usAsciiVal — die einzige Variable in unserem Programm — dient zur Schleifenkontrolle und repräsentiert gleichzeitig den ASCII-Wert, der ausgegeben werden soll. Da wir alle Werte zwischen 33 und 255 darstellen wollen, erhöhen wir den Wert dieser Variablen bei jedem Durchlauf um 1.

Üblicherweise weist der erste Ausdruck in der Schleifenanweisung einer Variablen den Anfangswert zu. Bei Bedarf ist die for-Schleife aber flexibel genug, um auch mehrere Variablen zu bedienen. Dazu ist allerdings der Einsatz des Kommaoperators erforderlich.

Aufgabe des Kommaoperators ist es, mehrere Anweisungen zu verknüpfen. Solcherart verbundene Anweisungen werden einfach der Reihe nach — also eine nach der anderen — ausgeführt. Es ist zu beachten, daß von den Anweisungen, die durch den Kommaoperator verbunden sind, diejenige den Wert für den Gesamtausdruck liefert, die am weitesten rechts steht. Das ist nicht von Bedeutung, sobald mehrere Variablen innerhalb einer for-Schleife zu initialisieren sind; falls Sie aber mehrere Vergleiche mit Hilfe des Kommaoperators aneinanderfügen, dann entscheidet der logische Wert der letzten Vergleichsoperation über den Wahrheitswert des ganzen Ausdrucks.


Beispiel für die Verwendung des Kommaoperators:

ulSum = ( ulCount = 34l, ++ulCount );

ulSum erhält den Wert 35, weil der Kommaoperator dafür sorgt, daß die durch ihn verknüpften Anweisungen der Reihe nach von links nach rechts abgearbeitet werden; der Wert des letzten Ausdrucks gilt als Wert des Gesamtausdrucks. In unserem Beispiel wird zuerst der Variablen ulCount der Wert 34l zugewiesen und dieser anschließend um 1 inkrementiert. Diesen zuletzt vorliegenden Wert erhält dann ulSum. Die Klammern sind notwendig, da der Kommaoperator einen geringeren Vorrang besitzt als der Zuweisungsoperator.

Unter Verwendung des Kommaoperators könnten wir eine neue Version von asciitab.c erstellen:


/***************************************************
 * asciitab.c                                      *
 * Gibt ASCII-Tabelle zweispaltig aus, beginnend   *
 * mit dem Wert 32                                 *
 * Diese Version verwendet den Kommaoperator, um   *
 * zwei Variablen zu kontrollieren                 *
 ***************************************************/
#include <stdio.h>			/* wg printf() */

int main(void){

unsigned short usGerade, usUngerade;

 /* beide Variablen werden innerhalb der Schleifen-
  * anweisung initialisiert und bei jedem Durchlauf
  * um den Wert 2 erhöht
  */
 for ( usGerade = 32, usUngerade = 33;
	usUngerade <= 255;
	usGerade += 2, usUngerade += 2){
	
		printf("%c\t%d\t\t\t%c\t\%d\n", 
			usGerade, usGerade,
			usUngerade, usUngerade);
		}
return 0;
}

Bei diesem Beispiel ist zu beachten, daß die Veränderung der Variablenwerte nicht mehr durch den Inkrementoperator geleistet werden kann, weil die Variablenwerte in diesem Fall ja bei jedem Durchlauf um den Wert 2 erhöht werden müssen. Als kompakte Alternative bietet sich der zusammengesetzte Zuweisungsoperator += an. Es fällt zudem auf, daß die vorliegende Variante mit zwei Laufvariablen die if-Anweisung einschließlich der Modulo-Division überflüssig macht. Die Werte für die linke und rechte Spalte werden innerhalb eines einzigen Schleifendurchlaufs geliefert; demzufolge muß die Version mit zwei Variablen nur halb soviele Schleifendurchläufe absolvieren wie jene mit einer Variablen.

Neben der Erweiterung der Ausdrücke durch den Kommaoperator läßt die for-Schleife weitere Varianten zu. Abweichend vom konventionellen Gebrauch von for, wo der Wert des Schleifenzählers in der Bedingung abgefragt wird, kann jeder beliebige Vergleich als Abbruchbedingung herangezogen werden. Dazu folgendes Beispiel:


/* rate.c
 * Generiert eine Zufallszahl, die (möglichst
 * mit binärer Suchstrategie) erraten werden
 * soll
 */

#include <stdio.h>    /* wg printf(), scanf() */
#include <stdlib.h>   /* wg rand(), srand() */
#include <time.h>     /* wg time() */

int main(void){

int iCount, iZufallsZahl, iEingabe = 0;
/* iEingabe muß mit einem Wert außerhalb des
 * Bereichs 1 —

 100 initialisiert werden
 */

srand( (unsigned)time( NULL ) );
/* Zufallsgenerator mit Hilfe des
 * Timers initialisieren
 */
iZufallsZahl = rand() % 100 + 1;
/* Zufallszahl ermitteln; damit diese einen Wert
 * zwischen 1 und 100 erhält, modulo 100
 * dividieren. Diese Division ergibt aber immer
 * einen ganzahligen Rest zwischen 0 und 99. Des-
 * wegen muß noch 1 hinzuaddiert werden.
 */
printf("Erraten Sie eine Zahl zwischen 1 und 100!\n\a");

 for(iCount = 1; iZufallsZahl != iEingabe; iCount++)
	{
	 printf("\n%d. Versuch: ", iCount);
	 scanf("%d", &iEingabe);

	if( iEingabe != iZufallsZahl)

	/* falls Zahl nicht erraten ...*/
		printf("Die Zahl ist zu %s!\n",
		    iEingabe < iZufallsZahl ? "klein": "groß");
	else
	/* ... andernfalls Erfolgsmeldung ausgeben */
	 	printf("!!! BINGO !!!\n");
	}

 return 0;
 }

Die for-Schleife erweist sich hier als bequem, da sie die Initialisierung und Inkrementierung des Schleifenzählers leistet. In der Schleifenbedingung selbst taucht iCount allerdings nicht auf. Die Anzahl der Schleifendurchläufe hat mithin keinen Einfluß auf den Abbruch der Schleifenausführung: Dieser erfolgt nur dann, sobald die eingegebene Zahl mit der Zufallszahl übereinstimmt. Dieser etwas unkonventionelle Einsatz der for-Schleife zeigt, daß diese auch in den Fällen Anwendung finden kann, in denen die Anzahl der Schleifendurchläufe nicht bereits vorher feststeht.

Zur Ermittlung der Zufallszahl werden in diesem Beispiel die von ANSI definierten Funktionen srand() und rand() benutzt. Den Zufallswert liefert eigentlich rand(), srand() dient dazu, den Zufallsgenerator zu initialisieren; wir nehmen dafür zusätzlich den Timer zu Hilfe. Der Zugriff auf den Timer erfolgt mit der Funktion time() — auch sie ist Teil der von ANSI definierten Standardbibliothek: srand() und rand() sind in stdlib.h deklariert, time() in time.h.

Der "Mißbrauch" einer for-Schleife kann sogar so weit gehen, daß die einzelnen Ausdrücke der Schleifenanweisung nur noch in entferntem Zusammenhang mit ihrer normalen Funktion stehen. Dabei kann man sich dann die Tatsache zunutze machen, daß der erste Ausdruck einmal, die beiden anderen bei jedem Schleifendurchlauf ausgeführt werden:


.
.
for( printf("Start: "); scanf("%[a-z]c", &cStart);
	 printf("Eingabe: "))
	{
	......
	 }
.
.

Die Abbruchbedingung dieser for-Schleife ist dann erreicht, wenn die Funktion scanf() den Wert 0 zurückgibt, also kein Zeichen eingelesen wurde. Ansonsten wird zum Start der Schleife die Meldung "Start:" ausgegeben, und bei jedem Durchlauf erfolgt die Ausgabe eines Prompts in Form von "Eingabe:".

Gebräuchlich sind außerdem for-Schleifen, bei denen nicht alle Ausdrücke der Schleifenanweisung vorhanden sind; im Extremfall fehlen sämtliche Anweisungen. In diesem Fall muß aber berücksichtigt werden, daß die beiden Semikola nicht fehlen dürfen. Beispielsweise würde die folgende Schleife so lange Zeichen einlesen, bis ein Punkt eingegeben wird:


.
.
for(cKey = '\0'; (cKey = getchar()) != '.';)
	{
	......
	 }
.
.

Wird keine Schleifenbedingung angegeben, dann gilt diese stets als wahr. Auf diese Weise läßt sich eine Endlosschleife erzielen:


.
.
for(;;){
	print("Das ist eine Endlosschleife\n");
	}
.
.

In der Praxis besteht kaum Bedarf an Endlosschleifen; solche Konstruktionen ohne Schleifenbedingung arten auch selten in Endlosschleifen aus, weil der Abbruch in diesen Fällen durch Anweisungen innerhalb des Schleifenkörpers vorgenommen wird (näheres dazu unter 7.5).

Eine weitere ungewöhnliche, aber zulässige Konstruktion stellen schließlich Schleifen ohne Körper dar. Derartige Gebilde sind dann sinnvoll, wenn die Ausdrücke der Schleifenanweisung alleine ausreichen, um die gewünschte Aufgabe zu erfüllen. Eine Standardanwendung für derartige Schleifen besteht im Überspringen von Leerzeichen am Beginn eines Eingabestroms. Das bedeutet, daß eingegebene Zeichen so lange zu ignorieren sind, als es sich dabei um Leerzeichen handelt:


for( ;getchar() == ' '; );

Da in dieser for-Schleife der erste und letzte Ausdruck leer bleiben, würde man in so einem Fall wohl eine while-Schleife bevorzugen:


while( getchar() == ' ');

Sowohl die for- als auch die while-Schleife sind so lange aktiv, wie über die Funktion getchar() Leerzeichen eingelesen werden. Sie brechen ab, sobald ein beliebiges anderes Zeichen ankommt. Es ist zu beachten, daß ein weiteres getchar() den verbleibenden Zeilenvorschub aus dem Tastaturpuffer lesen muß, sofern die Zeichen von der Tastatur eingegeben werden, also:


for( ;getchar() == ' '; getchar() );

Schleifen ohne Körper zeichnen sich dadurch aus, daß die Schleifenanweisung mit einem Semikolon abgeschlossen wird. Da gewöhnlich kein Semikolon am Ende von Schleifenanweisungen zu finden ist (weil sonst der Schleifenkörper nicht ausgeführt wird), kann eine solche Schleife irritierend für den Leser eines C-Programms (einige Wochen nach Verfassen des Programms wahrscheinlich auch für den Autor selber) sein. Deshalb sollte eine Schleife ohne Körper durch entsprechende Kommentierung gekennzeichnet werden, damit man nicht bei wiederholter Durchsicht des Programms dahinter einen Programmierfehler vermutet:


while( getchar() == ' ')
	 /* Schleife ohne Körper */ ;

oder


while( getchar() == ' ')
	/* führende Leerzeichen überspringen */ ;