Einführung in die Programmiersprache C: Schleifen mit for und while

Vorgestellt werden die Schleife mit Zählvariable (for-Schleife), die kopfgesteuerte Schleife (while-Schleife) und die fußgesteuerte Schleife (do-while-Schleife). Ihre Behandlung erfolgt aber nur exemplarisch an typischen Anwendungen. Eine detailliertere Behandlung findet sich im entsprechenden Kapitel zu C++.

Einordnung des Artikels

Einführung

Schleifen (oder Iterationen) werden eingesetzt, wenn ein Block von identischen Anweisungen mehrfach ausgeführt werden sollen. Daher muss man zwei Elemente der Schleife unterscheiden:

  1. Den Schleifenkörper mit den Anweisungen und
  2. Die Schleifensteuerung, in der kontrolliert wird, ob die Schleife erneut ausgeführt oder verlassen wird.

Die for-Schleife besitzt die übersichtlichste Schleifensteuerung: eine Zählvariable i durchläuft einen vorgegebenen Bereich, etwa ganze Zahlen von 1 bis 100 und für jeden Wert von i wird der Schleifenkörper ausgeführt.

Bei while-Schleifen gibt es zwei Varianten:

  1. die kopfgesteuerte Schleife (mit while ),
  2. die fußgesteuerte Schleife (mit do while ).

Sie werden eingesetzt, wenn man nicht vorgeben möchte (oder vorgeben kann), wie oft die Schleife durchlaufen werden soll. Durch eine Bedingungsprüfung wird entschieden, ob die Schleife erneut durchlaufen oder verlassen wird. Bei der kopfgesteuerten Schleife findet die Bedingungsprüfung vor dem Schleifenkörper statt, bei der fußgesteuerten Schleife nach dem Schleifenkörper.

Da hier nur eine kurze Einführung in die Programmiersprache C gegeben werden soll, werden nur diejenigen Aspekte von Schleifen besprochen, die man beim ersten Umgang mit ihnen auf jeden Fall kennen muss. Es gibt noch weitere Aspekte, denen man mit Sicherheit im Umgang mit Schleifen irgendwann begegnen wird, die hier aber nicht erklärt werden, insbesondere:

In Elementare Syntax von C++: Schleifen werden diese Konzepte behandelt – allerdings für C++. Die Syntax für Schleifen ist in C und C++ völlig identisch. Der einzige Unterschied zwischen den Sprachen, der zu beachten ist, betrifft die Verwendung der Initialisierungsliste: In C++ können Variablen von elementaren Datentypen mit Hilfe einer Liste der Länge 1 initialisiert werden, eine typische Deklaration mit gleichzeitiger Initialisierung sieht dann aus wie:

int n {17};

Dies ist in C nicht möglich; obiger Befehl muss in C geschrieben werden als:

int n = 17;

Beachtet man diesen kleinen Unterschied, kann man Elementare Syntax von C++: Schleifen als ausführliche Darstellung von Schleifen in C verwenden.

Die for-Schleife

Die for-Schleife wird auch als Schleife mit Zählvariable bezeichnet. Die Zählvariable durchläuft einen vorgegebenen Bereich und bei jedem Durchlauf wird ein Anweisungs-Block ausgeführt.

Dadurch ist das Verhalten der for-Schleife besser kontrollierbar als bei einer while-Schleife; diese wird ausgeführt, solange eine gewisse Bedingung erfüllt ist. Und oft ist es schwer vorherzusagen, ob die while-Schleife einmal, zweimal, und so weiter oder sogar unendlich oft ausgeführt wird.

Die Syntax der for-Schleife

Die Syntax der for-Schleife kann man an einem typischen Beispiel erklären. Im folgenden Listing wird eine Variable sum mit dem Wert 0 initialisiert. Nach dem Durchlaufen der Schleife hat sum den Wert:

1 + 2 + ... + 100 = 100 * 101 / 2 = 5050.

int sum = 0;

for (int i = 1; i < 101; i++){
    sum += i;
}

printf("Summe: %i ", sum);          // 5050

Im Schleifenkörper (innerhalb der geschweiften Klammern) steht die Anweisung, die für die Summation sorgt (Zeile 4): zu sum wird jeweils der aktuelle Wert von i addiert. (Hier könnten auch mehrere Anweisungen stehen.)

Die Konfiguration der Schleife geschieht innerhalb der runden Klammern nach for (Zeile 3); die Konfiguration besteht aus drei Bestandteilen:

  1. Deklaration und Initialisierung der Zählvariable i.
  2. Bedingung unter der die Schleife erneut ausgeführt wird (i < 101); oder negativ formuliert: Abbruchbedingung (wenn i =101 erreicht wird).
  3. Schrittweite (der Inkrement-Operator sorgt hier dafür, dass die Schrittweite gleich eins ist — das muss aber nicht sein).

Diese drei Bestandteile der for-Anweisung sind durch Strichpunkte getrennt (Initialisierung der Zählvariable; Ausführungs-Bedingung; Schrittweite).

Aufgaben zur for-Schleife

1. Aufgabe:

Der Nutzer soll eine ganze Zahl n zwischen 1 und 4 eingeben. Es sollen alle n-stelligen Hexadezimalzahlen ausgegeben werden. Sie sollen aufsteigend sortiert ausgegeben werden, also zum Beispiel für n = 2:

0
1
2
...
FD
FE
FF

Wie realisiert man, dass alle Zahlen mit 4 Ziffern ausgegeben werden – also eventuell mit führenden Nullen; wieder für n = 2:

0000
0001
0002
...
00FD
00FE
00FF

2. Aufgabe:

Der Nutzer soll eine ganze Zahl eingeben. In einer for-Schleife sollen alle Teiler der Zahl berechnet und ausgegeben werden.

3. Aufgabe:

Wie in der zweiten Aufgabe sollen die Teiler einer eingegebenen Zahl n berechnet werden. Zusätzlich soll die Summe der Teiler berechnet und ausgegeben werden; dabei wird n nicht zu den Teilern gerechnet.

Eine Zahl, die in diesem Sinne mt der Summe ihrer Teiler übereinstimmt, heißt perfekte Zahl.

4. Aufgabe:

Setzt man

f0 = 1, f1 = 1 sowie fn+2 = fn+1 + fn für n = 0, 1, 2,...,

so werden die Zahlen fn als die Fibonacci-Zahlen bezeichnet.

Schreiben Sie ein Programm, das die ersten N Fibonacci-Zahlen berechnet und ausgibt. Testen Sie Ihr Programm mit N = 30.

Lösungen zu den Aufgaben (for-Schleife)

1. Aufgabe: Hexadezimalzahlen

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main(void) {
    // Eingabe von n
    int max = pow(16, n);
    
    for (int i = 0; i < max; ++i) {
        // ohne Anpassung der Stellen:
        //printf("%X\n", i);
        
        // mit 4 Stellen (eventuell führende 0):
        printf("%.4X\n", i);    
    }
    return EXIT_SUCCESS;
}

Zur Erklärung:

Zeile 3: Da später zur Berechnung der Potenz die Funktion pow() eingesetzt wird, muss man math.h inkludieren.

Zeile 6: Die Eingabe von n ist nicht dargestellt; dies wurde bei den Programmen nach HelloWorld erklärt.

Zeile 7: Die größte einstellige Hexadezimalzahl ist 15, die größte zweistellige Hexadezimalzahl ist 255. Allgemein wird die größte n-stellige Hexadezimalzahl durch

16n - 1

berechnet. Dies geschieht mit Hilfe der Funktion pow(), die als Eingabewerte die Basis und die Potenz besitzt. Näheres findet man in der Dokumentation zu math.h und dort zu pow().

Zeile 9: Die Zahl max wäre als Hexadezimalzahl schon um eine Stelle zu lang und soll nicht mehr ausgegeben werden; daher wird als Bedingung in der Konfiguration der Schleife i < max gesetzt.

Zeile 11: (Auskommentiert) Die Formatierungsanweisung %X sorgt dafür, dass eine ganze Zahl als Hexadezimalzahl ausgegeben wird, wobei die Ziffern A-F groß geschrieben werden. Für Kleinschreibung muss man %x verwenden.

Zeile 14: Die Formatierungsanweisung %.4X sorgt dafür, dass die Zahl i als Hexadezimalzahl mit genau 4 Stellen ausgegeben wird – also eventuell mit führenden Nullen.

2. Aufgabe: Teiler berechnen

Die folgende Lösung arbeitet nach dem Prinzip brute force: Zu einer gegebenen Zahl n werden alle Zahlen von 1 bis n-1 getestet, ob sie Teiler von n sind. Man könnte diese Suche natürlich geschickter gestalten, aber dann wird der Quelltext komplexer. (Für große Zahlen n sollte man besser die Primfaktorzerlegung bestimmen.)

Ob eine Zahl i Teiler von n ist, kann mit dem Rest feststellen: Ist der Rest der Ganzzahl-Division "n durch i" gleich null, dann ist i ein Teiler von n. Daher wird getestet, ob n % i == 0 (siehe Zeile 9).

Dass n ein Teiler von n ist, wird hier nicht angegeben; man könnte diesen Teiler auch nach der Schleife selbst ergänzen.

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // Eingabe von n
    printf("n = %i \n", n);

    for (int i = 1; i < n; ++i) {
        if ( (n % i) == 0) {
            printf("Teiler: %i \n", i);
        }
    }
}

Beispiel: Ausführung des Programms für n = 24.

n = 24 
Teiler: 1 
Teiler: 2 
Teiler: 3 
Teiler: 4 
Teiler: 6 
Teiler: 8 
Teiler: 12

Zur Erklärung:

Zeile 5: Die Eingabe von n wird wieder nicht gezeigt.

Zeile 6: Die eingegebene Zahl wird sofort ausgegeben.

Zeile 8: Da n selbst hier nicht zu den Teilern gerechnet wird, muss die Zählvariable i (möglicher Teiler) nur von 1 bis n-1 laufen.

Zeile 9 und 10: Falls das aktuelle i ein Teiler ist, wird es ausgegeben.

Zeile 9: Man beachte, dass hier der Vergleichsoperator == eingesetzt werden muss, den man nicht mit dem Zuweisungsoperator = verwechseln darf.

3. Aufgabe: Teiler berechnen und addieren

Zusätzlich zur letzten Aufgabe wird vor der Schleife eine Variable sum mit 0 initialisiert (Zeile 7); zu dieser Variable wird der jeweilige Teiler addiert (Zeile 12). Nach der Schleife wird sum ausgegeben (Zeile 15).

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    // Eingabe von n
    printf("n = %i \n", n);
    int sum = 0;

    for (int i = 1; i < n; ++i) {
        if ( (n % i) == 0) {
            printf("Teiler: %i \n", i);
            sum = sum + i;
        }
    }
    printf("Summe der Teiler = %i \n", sum);
}

Für n = 28 lautet jetzt die Ausgabe:

n = 28 
Teiler: 1 
Teiler: 2 
Teiler: 4 
Teiler: 7 
Teiler: 14 
Summe der Teiler = 28

Aufgabe: Welche Ausgabe erzeugt das Programm, wenn man die printf()-Anweisung aus Zeile 15 in die Schleife verschiebt, und zwar nach Zeile 12? Testen Sie diese Variante!

4. Aufgabe: Fibonacci-Zahlen

Das folgende Listing zeigt nur noch die Anweisungen, die innerhalb von main() stehen müssen:

int a = 1;
int b = 1;
int c;
int max = 31;

for (int i = 2; i < max; ++i) {
    c = a + b;
    // Ausgabe:
    printf("i = %i: %i \n", i, c);

    // Vorbereitung für nächsten Durchlauf:
    a = b;
    b = c;
}

Ausgaben (die ersten beiden Fibonacci-Zahlen wurden nicht ausgegeben):

i = 2: 2 
i = 3: 3 
i = 4: 5 
i = 5: 8 
i = 6: 13 
i = 7: 21 
i = 8: 34 
i = 9: 55 
i = 10: 89 
i = 11: 144 
i = 12: 233 
i = 13: 377 
i = 14: 610 
i = 15: 987 
i = 16: 1597 
i = 17: 2584 
i = 18: 4181 
i = 19: 6765 
i = 20: 10946 
i = 21: 17711 
i = 22: 28657 
i = 23: 46368 
i = 24: 75025 
i = 25: 121393 
i = 26: 196418 
i = 27: 317811 
i = 28: 514229 
i = 29: 832040 
i = 30: 1346269

Zur Erklärung:

Zeile 1 und 2: Die nullte und die erste Fibonacci-Zahl werden gesetzt (aber nicht ausgegeben).

Zeile 3: Die zweite Fibonacci-Zahlen wird als Variable c deklariert, aber noch nicht initialisiert.

Zeile 4: Damit man leicht die Anzahl der Fibonacci-Zahlen verändern kann, wird eine Variable max gesetzt.

Zeile 6: Damit i die Nummer der Fibonacci-Zahl angibt, startet die Schleife mit i = 2 . Die Schleife wird verlassen, wenn i gleich max ist.

Zeile 7 und 9: In der Schleife wird die nächste Fibonacci-Zahl berechnet und ausgegeben.

Zeile 12 und 13: Damit man auch im nächsten Durchlauf der Schleife die folgende Fibonacci-Zahl mit Zeile 7 berechnen kann, müssen a und b neu gesetzt werden.

Die kopf- und die fußgesteuerte Schleife

Bei der for-Schleife muss man bei der Konfiguration der Schleife wissen, wie oft sie durchlaufen werden soll. Es gibt allerdings auch Probleme, bei denen dies nicht bekannt ist – oder mit großem Rechenaufwand verbunden wäre.

Beispiel: Berechnung der Quadratwurzel mit dem Heron-Verfahren

Es gibt eine einfache Rekursionsformel zur Berechnung der Wurzel einer gegebenen Zahl a, nämlich

xn+1 = (xn + a / xn) / 2.

Man startet dazu etwa mit x1 = a und berechnet sukzessive die Werte x2, x3, und so weiter. Es ist klar, dass jede der Zahlen xn eine rationale Zahl ist, eine Wurzel im Allgemeinen aber irrational ist. Wie man leicht zeigen kann, konvergiert die Folge der xn gegen die Wurzel aus a.

Man muss dazu nur in der Rekursionsformel den Ansatz

xn = xn+1

machen und einen Zusammenhang zwischen xn und a herstellen. Wenn man zeigen kann, dass die Folge der xn konvergiert, kann der Grenzwert nur mit der Wurzel aus a übereinstimmen (siehe Abbildung 1).

Bildlich kann man sich die Rekursion wie in Abbildung 1 rechts vorstellen: Dazu ist die rechte Seite der Rekursionsformel als grüne Funktion f(x) eingezeichnet. Man geht vom Startwert x1 zum Funktionswert f(x1), von dort zur Gerade y = x (blau eingezeichnet), wieder zur Funktion und so weiter. Nach wenigen Iterationsschritten kommt man dem Schnittpunkt von Funktion f (x) und der Gerade schon so nahe, dass man die Pfade nicht mehr in das Diagramm eintragen kann. Die Berechnung der ersten Werte x1, x2, ..., x6 zeigt, wie schnell das Verfahren gegen die gesuchte Wurzel konvergiert.

Abbildung 1: Berechnung einer Wurzel mit Hilfe des Heron-Verfahrens.Abbildung 1: Berechnung einer Wurzel mit Hilfe des Heron-Verfahrens.

♦ ♦ ♦

Soll die Berechnung der Wurzel in einer Schleife geschehen, so muss man sich bei einer for-Schleife vorgeben, wie oft sie durchlaufen wird. Es gibt aber auch Schleifen, die man anders konfiguriert: Hier kann man sich etwa vorgeben, wie weit sich xn und xn+1 unterscheiden dürfen; wird die Schranke unterschritten, kann man die Iteration abbrechen. Beim Taschenrechner geschieht genau dies: die Rekursionsformel des Heron-Verfahrens wird so lange in sich eingesetzt, bis sich die Ergebnisse nur noch um einen Betrag unterscheiden, der im Taschenrechner nicht sichtbar ist.

Es gibt zwei Varianten:

  1. Die kopfgesteuerte Schleife oder while-Schleife: Die Ausführungs-Bedingung für die Schleife wird immer vor dem Abarbeiten des Schleifenkörpers geprüft.
  2. Die fußgesteuerte Schleife oder do-while-Schleife: Die Ausführungs-Bedingung für die Schleife wird immer nach dem Abarbeiten des Schleifenkörpers geprüft.

Die Syntax der kopfgesteuerten Schleife (while-Schleife)

Wie oben bei der for-Schleife wird wieder das Beispiel gezeigt, in dem die Zahlen von 1 bis 100 summiert werden:

int sum = 0;
short j = 1;

while (j < 101){
    sum += j;
    j += 1;
}

printf("Summe: %i \n", sum);        // 5050

Die Variable sum wird wieder vor der Schleife mit 0 initialisiert (Zeile 1). Die Zählvariable j wird vor der Schleife initialisiert (Zeile 2).

Innerhalb der runden Klammern nach while steht die Ausführungs-Bedingung (Zeile 4). In den geschweiften Klammern steht der Schleifenkörper (Zeile 5 und 6).

Im Schleifenkörper wird die Summe sum aktualisiert und die Zählvariable j hochgezählt. Sobald j den Wert 101 erreicht, ist die Ausführungs-Bedingung nicht mehr erfüllt und die Schleife wird verlassen.

Die Syntax der fußgesteuerten Schleife (do-while-Schleife)

Die fußgesteuerte Schleife dreht nur die Reihenfolge von Ausführungs-Bedingung und Schleifenkörper um; der Schleifenkörper steht innerhalb der geschweiften Klammern nach do:

int sum = 0;
short i = 1;

do{
    sum += i;
    i += 1;
}
while (i < 101);

printf("Summe: %i \n", sum);

Aufgaben

1. Aufgabe:

Wie groß muss man n in der Summe

1 + 2 + 3 + ... + n

wählen, so dass das Ergebnis gerade noch kleiner als 10000 ist?

Lösen Sie das Problem sowohl mit einer kopf- als auch einer fußgesteuerten Schleife.

2. Aufgabe:

Oben wurde das Heron-Verfahren vorgestellt. Berechnen Sie damit die Wurzel aus 2. Als Abbruchbedingung wird |xn+1 - xn| < eps, mit eps = 10-10,

gefordert.

Entscheiden Sie, ob für die Berechnung eine while-Schleife oder eine do-while-Schleife besser geeignet ist.

Vergleichen Sie Ihren Näherungswert mit dem exakten Wert.

3. Aufgabe:

Berechnen Sie für die Fibonacci-Zahlen das Verhältnis fn+1 / fn solange bis sich die Werte um weniger als 10-10 unterscheiden.

Gegen welchen Grenzwert konvergiert die Folge der Verhältnisse?

Lösungen zu den Aufgaben

1. Aufgabe:

1 + 2 + ... + 140 = 9870.

2. Aufgabe: Heron-Verfahren

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

int main(void) {
    // Eingabe von x (Wert aus dem die Wurzel berechnet werden soll, hier x = 2)
    
    double eps = 10e-10;
    double y = x;               // y: Startwert der Folge x_n
    double z;                   // nächstes Folgenglied
    double diff;                // Abstand benachbarter Folgenglieder

    do {
        z = (y + x / y) / 2;            // z = x_n+1 (neues Folgenglied)
        diff = fabs(y - z);             // Differenz |x_n+1 - x_n|
        printf("in while: z =  %.12f \n", z);
        y = z;                          // y für nächsten Iterationsschritt vorbereiten
    } while (diff > eps);

    printf("nach while: z =  %.12f \n", z);
}

Ausgaben:

in while: z =  1.500000000000 
in while: z =  1.416666666667 
in while: z =  1.414215686275 
in while: z =  1.414213562375 
in while: z =  1.414213562373 
nach while: z =  1.414213562373

Die Ausgabe von z innerhalb des Schleifenkörpers ist eigentlich unnötig; sie soll nur zeigen, wie schnell das Verfahren konvergiert.

3. Aufgabe: Verhältnis aufeinanderfolgender Fibonacci-Zahlen

Das folgende Listing zeigt wieder der Inhalt von main():

// a, b, c: 3 aufeinanderfolgende Fibonacci-Zahlen
int a = 1;
int b = 1;
int c;

// q1, q2: Verhältnisse der aufeinanderfolgenden F-Zahlen
double q1;
double q2 ;

// Schranke für Abbruch:
double eps = 10e-10;

do{
    c = a + b;
    q1 = (double) b / a;
    q2 = (double) c / b;
    printf("Quotient in do: %.12f \n", q2);

    // Vorbereitung für nächsten Durchlauf:
    a = b;
    b = c;
}
while(fabs(q1 - q2) > eps);

printf("Quotient: %.12f \n", q2);
printf("Grenzwert: %.12f \n", (1 + sqrt(5)) / 2);

Das Programm erzeugt folgende Ausgaben:

Quotient in do: 2.000000000000 
Quotient in do: 1.500000000000 
Quotient in do: 1.666666666667 
Quotient in do: 1.600000000000 
Quotient in do: 1.625000000000 
Quotient in do: 1.615384615385 
Quotient in do: 1.619047619048 
Quotient in do: 1.617647058824 
Quotient in do: 1.618181818182 
Quotient in do: 1.617977528090 
Quotient in do: 1.618055555556 
Quotient in do: 1.618025751073 
Quotient in do: 1.618037135279 
Quotient in do: 1.618032786885 
Quotient in do: 1.618034447822 
Quotient in do: 1.618033813400 
Quotient in do: 1.618034055728 
Quotient in do: 1.618033963167 
Quotient in do: 1.618033998522 
Quotient in do: 1.618033985017 
Quotient in do: 1.618033990176 
Quotient in do: 1.618033988205 
Quotient in do: 1.618033988958 
Quotient: 1.618033988958 
Grenzwert: 1.618033988750

Einige allgemeine Bemerkungen zu Speicherplatz und Rechenzeit

Das letzte Beispiel zeigt deutlich einige Punkte, die man bei Schleifen immer beachten sollte – insbesondere dann, wenn sie sehr oft durchlaufen werden oder man nicht weiß, wie oft sie durchlaufen werden:

Aufgabe:

Bei der Berechnung der Verhältnisse der Fibonacci-Zahlen werden innerhalb der Schleife 2 Quotienten berechnet (Zeile 15 und 16); zusätzlich findet eine Typumwandlung statt, da man sonst die Ganzzahl-Division erhält. Wie kann man eine der beiden Divisionen einsparen?