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
- Die for-Schleife
- Die Syntax der for-Schleife
- Aufgaben zur for-Schleife
- Lösungen zu den Aufgaben (for-Schleife)
- 1. Aufgabe: Hexadezimalzahlen
- 2. Aufgabe: Teiler berechnen
- 3. Aufgabe: Teiler berechnen und addieren
- 4. Aufgabe: Fibonacci-Zahlen
- Die kopf- und die fußgesteuerte Schleife
- Die Syntax der kopfgesteuerten Schleife (while-Schleife)
- Die Syntax der fußgesteuerten Schleife (do-while-Schleife)
- Aufgaben
- Lösungen zu den Aufgaben
- Einige allgemeine Bemerkungen zu Speicherplatz und Rechenzeit
Einordnung des Artikels
- Einführung in die Informatik
- Einführung in die Programmiersprache C und in C-Zeiger
- Einführung in die Programmiersprache C: HelloWorld und erste Programme
- Einführung in die Programmiersprache C: Schleifen mit for und while
- Einführung in die Programmiersprache C und in C-Zeiger
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:
- Den Schleifenkörper mit den Anweisungen und
- 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:
- die kopfgesteuerte Schleife (mit
while
), - 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:
- Die zusätzliche Ablaufsteuerung von Schleifen mit
break
undcontinue
. - Das Konzept Gültigkeitsbereich von Variablen (scope).
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:
- Deklaration und Initialisierung der Zählvariable i.
- Bedingung unter der die Schleife erneut ausgeführt wird (i < 101); oder negativ formuliert: Abbruchbedingung (wenn i =101 erreicht wird).
- 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.
♦ ♦ ♦
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:
- Die kopfgesteuerte Schleife oder while-Schleife: Die Ausführungs-Bedingung für die Schleife wird immer vor dem Abarbeiten des Schleifenkörpers geprüft.
- 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:
- Konsolen-Ausgaben beanspruchen sehr viel Rechenzeit, da jetzt externe Geräte angesprochen werden müssen und nicht nur Berechnungen im Prozessor stattfinden. Zum Testen von Programmen sind sie meist sehr hilfreich, im tatsächlichen Einsatz des Programms sollte man sie so weit wie möglich vermeiden.
- Bei allen Anweisungen und Berechnungen in einer Schleife sollte man sich überlegen, ob sie wirklich nötig sind oder zumindest schneller ausgeführt werden können.
- Meistens ist es überflüssig, innerhalb einer Schleife eine Variable zu definieren (also deklarieren und initialisieren). Die Deklaration kann immer vor die Schleife verlegt werden. Im Beispiel oben wird die Variable c vor der Schleife deklariert, aber innerhalb der Schleife zum ersten Mal initialisiert. Verlegt man die Deklaration in die Schleife, wird bei jedem Durchlauf Speicherplatz für eine neue Variable angelegt. Hier reicht es die einmal deklarierte Variable c (Zeile 4) immer wieder zu überschreiben.
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?