Immer diese Wiederholungen: Schleifen |
Die Schleife beginnt mit dem Schlüsselwort while. In Klammern folgt die Bedingung, unter der die direkt anschließende Anweisung ausgeführt wird. Auch hier können mehrere Anweisungen zu einem Anweisungsblock zusammengefasst werden, wenn sie in geschweiften Klammern stehen. Abbildung {grafwhile} zeigt den Syntaxgraph.
Grafik nur im Buch vorhanden
#include <iostream>
using namespace std;
int main()
{
i = 1;
while (i <= 10) // Schleifenbedingung
{
cout << i << endl; // Aktion
i++; // Ohne diese Erhöhung wird es eine Endlosschleife
}
}
#include <iostream>
using namespace std;
int main()
{
int i = 0;
while (i < 10)
{
i++;
cout << i << endl;
}
}
Grafik Struktogramm der while-Schleife nur im Buch vorhanden
Als weiteres Beispiel wird noch einmal gezählt, diesmal aber rückwärts.
int i = 10;
while (i > 0)
{
i--;
}
Hier wird die Zählvariable i mit 10 vorbesetzt. In der
Schleifenbedingung wird geprüft, ob i größer als 0 ist.
Das ist wahr, also wird die Schleife
betreten und der Schleifenkörper ausgeführt. Darin wird lediglich die Variable
i um 1 vermindert, so dass i nach dem ersten Durchlauf immer noch 9 enthält.
Das ist immer noch größer als 0. Die Schleife wird so oft durchlaufen, bis
i durch das Dekrementieren 0 wird. Bei der nächsten Prüfung am Schleifenkopf
ist die Bedingung nicht mehr gültig, und die Schleife wird sofort verlassen.
Im Listing fehlt die Ausgabe.
Als kleine Übung können Sie darüber nachdenken, was wohl ausgegeben wird,
wenn Sie die Ausgabeanweisung vor oder nach dem Dekrementieren von i in die
Schleife setzen. Wenn Sie sicher sind, was passieren wird, probieren Sie es
aus!
Grafik Syntaxgraph do-while nur im Buch vorhanden
Auch hier werden Sie in den meisten Fällen einen Block aus geschweiften Klammern verwenden, da eine Schleife meist mehrere Anweisungen umfasst.
Das Struktogramm unterscheidet sich von dem der while-Schleife darin, dass die Bedingung nun am Ende steht.
Grafik Struktogramm der do-while-Schleife nur im Buch vorhanden
Im folgenden Beispiel wird rückwärts gezählt. Betrachten Sie das Listing, und überlegen Sie, ob die Zahlen 10, 1 und 0 ausgegeben werden! Prüfen Sie, ob Ihre Vermutungen stimmen!
#include <iostream>
using namespace std;
int main()
{
int i = 10;
do
{
cout << i << endl;
i--;
}
while(i>0);
}
Eine sehr typische Anwendung für Schleifen ist das Zählen; und gerade hier zeigt sich besonders die Übersichtlichkeit der for-Schleife. Das folgende Beispiel zählt von 0 bis 9.
for (i=0; i<10; i++)
{
cout << i << endl;
}
Die Grafik Syntaxgraph für for ist nur im Buch vorhanden.
Die for-Schleife ist der while-Schleife sehr ähnlich. Sie verpackt ihre Fähigkeiten nur eleganter und bewirkt, dass der Programmierer nicht so leicht wichtige Elemente vergisst. Sie könnten das obige Schema leicht auch als while-Schleife darstellen.
Startanweisung;
while ( Bedingung)
{
Schleifenkörper;
Schlussanweisung;
}
Entsprechend unterscheidet sich das Struktogramm nicht von dem der while-Schleife. Es wird lediglich inhaltlich der Kopf der Schleife in den Abfragebereich geschrieben.
Die Grafik Struktogramm der for-Schleife ist nur im Buch vorhanden.
Manche Programmierer setzen beim Struktogramm die Schlussanweisung in das Feld des for-Befehls zu Startanweisung und Bedingung.
#include <iostream>
using namespace std;
int main()
{
int i, a;
for (i=0, a=10; i<5; i++, a--)
{
cout << i << "-" << a << endl;
}
}
for (;;) // läuft bis zum nächsten Stromausfall
{
...
}
fehler=0;
while (a>0)
{
...
c=4;
if (fehler==9)
{
break;
}
a++;
...
}
Eine allzu häufige Verwendung von break zeugt von mangelnder Planung. Anstatt sorgfältig eine Bedingung zu formulieren und durch eine if-Abfrage den Ablauf zu steuern, wird die Schleife spontan abgebrochen. Der folgende Ablauf ist äquivalent zu dem vorigen, hat aber den Vorteil, dass der Ablauf und das Ende der Schleife deutlicher werden.
fehler=0;
while (fehler!=9 && a>0)
{
...
c=4;
if (fehler!=9)
{
a++;
...
}
}
Bevor Sie break verwenden, um eine Schleife zu verlassen, sollten Sie darüber
nachdenken, ob Sie den Ablauf nicht eleganter gestalten können. Wenn Sie
dennoch gute Gründe haben, mit break zu arbeiten, sollten Sie
wenigstens am Schleifenkopf durch einen Kommentar auf diese Abnormität
hinweisen.
while (a>0)
{
...
c=4;
if (fehler==9)
{
continue;
}
a++;
...
}
Die Verwendung von continue lässt sich noch leichter umgehen als
die von break. Der einzige Vorteil, den continue gegenüber
der Verwendung einer if-Abfrage hat, ist die Verringerung der
Einrückungstiefe für die Anweisung a++. Aber genau das ist
gleichzeitig der Nachteil: Man kann nicht an der Einrückung ablesen, dass diese
Befehle nur dann ausgeführt werden, wenn die Variable fehler nicht den
Wert 9 enthält.
while (a>0)
{
...
c=4;
if (fehler!=9)
{
a++;
...
}
}
Die einzig wirklich sinnvolle Anwendung des break-Befehls findet
sich in der switch-Anweisung. Hier liegt es in der Natur der Sache,
dass nach dem Abarbeiten eines Falles die Anweisung per Sprung verlassen wird.
if (a==0)
{
a++;
goto Dazwischen;
}
b+=2;
if (b>6)
{
goto GanzHinten;
Dazwischen:
// hier ist b>6 oder a==1
b=4;
}
else
{
c=2;
}
GanzHinten:
a++;
Der Sprung an die Stelle Dazwischen: zeigt, welcher Unfug mit der
goto-Anweisung möglich ist. Obwohl dieser Sprung vom Compiler
nicht bemängelt wird, fällt es schwer vorherzusagen, was passiert.
Wenn Sie schon einen goto-Befehl verwenden, dann sollten Sie ihn
höchstens zum Verlassen von verschachtelten Schleifen einsetzen und nicht
in andere Blöcke hineinspringen.
In alten (wirklich alten) Programmiersprachen gab es nur den absoluten
Sprung, um eine andere Stelle im Programm zu erreichen. Da diese Programme
sehr unübersichtlich wurden, gab es in den 70er Jahren einen
Kreuzzug gegen die Verwendung des goto. Moderne Sprachen haben
durch Abfragen und Schleifen alles an Bord, um auf den Befehl goto
verzichten zu können. Ein Sprung erschwert die Nachvollziehbarkeit des
Programmablaufs. Darum sollten Sie ihn vermeiden.
Die Zahl 2 wird der Vollständigkeit halber einfach zu Anfang
direkt ohne explizite Berechnung ausgegeben.
Grafik ist nur im Buch vorhanden
Grafik nur im Buch vorhanden
Grafik nur im Buch vorhanden
Beispiele
Schleifen und Abfragen sind ein Grundbestandteil jedes Programms. Sie brauchen
einige Übung, um in den Aufgabenstellungen Abfragen und Schleifen zu erkennen
und zu lernen, wie sie kombiniert werden. Zu diesem Zweck werden in diesem
Abschnitt einige Beispiele betrachtet.
Die Beispiele sind so gewählt, dass sie zeigen, wie
Schleifen und Abfragen ineinander verschachtelt werden.
Die Nassi-Schneidermann-Diagramme werden zur Veranschaulichung, aber auch als
Design-Werkzeuge eingesetzt.
Primzahlen
Primzahlen sind Zahlen, die nur durch sich selbst und 1 teilbar sind. Für die
Berechnung der Primzahlen gibt es keine Formel, sondern man muss für
jede kleinere Zahl prüfen, ob sie sich ohne Rest dividieren lässt.
Äußere Schleife
Das Programm soll alle Primzahlen zwischen 3 und 100 ausgeben.
Es gibt also eine große Zählschleife für alle Kandidaten.
#include <iostream>
using namespace std;
int main()
{
const int MaxPrimzahl=100; // Ende der Berechnung
int Primzahl; // Testkandidat
for (Primzahl=3; Primzahl<=MaxPrimzahl; Primzahl++)
// Durchlaufe alle Kandidaten
{
// Pruefe, ob Primzahl wirklich eine Primzahl ist
// Testausgabe:
cout << Primzahl << " ";
}
cout << endl;
}
In die Schleife wurde erst einmal eine Testausgabe gesetzt. Damit können Sie
sehen, dass die Primzahlenkandidaten tatsächlich von 3 bis 100 laufen. Wenn Sie unsicher
sind, ob Programmteile das tun, was Sie von Ihnen erwarten, sollten Sie
solche Testhilfen ruhig in Ihr Programm einbauen. Sie können Sie ja später
wieder löschen. Sollte Ihnen die Menge der Ausgaben zu groß werden, reduzieren
Sie einfach MaxPrimzahl für die Testphase auf 10!
Innere Schleife
Im nächsten Schritt soll in der Schleife jeder Kandidat geprüft werden. Dazu
wird er durch alle Zahlen zwischen 2 und seinem eigenen Wert minus eins
dividiert. Wenn bei dieser Division kein Rest bleibt, sind die Zahlen
durcheinander teilbar und der Kandidat ist keine Primzahl.
Sie brauchen also eine weitere Schleife innerhalb der Schleife.
In dieser Schleife wird der Divisor hochgezählt. Er startet bei 2 und läuft
bis Primzahl-1.
#include <iostream>
using namespace std;
int main()
{
const int MaxPrimzahl=100;
int Primzahl, Divisor;
for (Primzahl=3; Primzahl<=MaxPrimzahl; Primzahl++)
{
// Pruefe, ob Primzahl wirklich eine Primzahl ist
for (Divisor=2; Divisor<=Primzahl-1; Divisor++)
{
// Testausgabe
cout << Divisor << " ";
}
cout << endl;
}
cout << endl;
}
Primzahltest
Nun muss geprüft werden, ob Primzahl durch
Divisor teilbar ist. Dazu kennen
Sie bereits den Befehl % für die Modulo-Rechnung. Sie liefert 0,
wenn beim Teilen kein Rest bleibt. Wir müssen also eine Abfrage konstruieren,
die prüft, ob beim Teilen von Primzahl durch
Divisor kein Rest bleibt.
#include <iostream>
using namespace std;
int main()
{
const int MaxPrimzahl=100;
int Primzahl, Divisor;
for (Primzahl=3; Primzahl<=MaxPrimzahl; Primzahl++)
{
// Pruefe, ob Primzahl wirklich eine Primzahl ist
for (Divisor=2; Divisor<Primzahl; Divisor++)
{
// Ist das restlos teilbar?
if (0==Primzahl % Divisor)
{
// Zahl ist teilbar, ist also keine Primzahl!
// Testausgabe:
cout << Primzahl << "-" << Divisor << endl;
}
}
cout << endl;
}
cout << endl;
}
Sie sind der Lösung schon recht nahe. Wenn Sie das Programm laufen lassen,
können Sie sehen, wie die Primzahlkandidaten mit ihren Teilern angezeigt
werden. Es werden also die Zahlen angezeigt, die keine Primzahlen sind,
denn Primzahlen haben keine Teiler.
Wenn sich das Programm nun merkt, dass es an diese Stelle gekommen
ist, weiß es, dass dieser Kandidat keine Primzahl ist. Um sich
Zustände zu merken, verwenden Sie am besten eine boolesche Variable.
#include <iostream>
using namespace std;
int main()
{
const int MaxPrimzahl=100;
int Primzahl, Divisor;
bool istEinePrimzahl;
cout << "2";
for (Primzahl=3; Primzahl<=MaxPrimzahl; Primzahl++)
{
istEinePrimzahl = true;
// Pruefe, ob Primzahl wirklich eine Primzahl ist
for (Divisor=2; Divisor<Primzahl; Divisor++)
{
// Ist das restlos teilbar?
if (0==Primzahl % Divisor)
{
// Zahl ist teilbar, ist also keine Primzahl!
istEinePrimzahl = false;
}
}
// Pruefung ist beendet.
// Wenn es eine Primzahl ist, ausgeben!
if (istEinePrimzahl)
{
cout << ", " << Primzahl;
}
}
cout << endl;
}
Status merken
Dieses Programm erfüllt die Anforderungen, die wir gestellt hatten.
Zu Anfang wird eine boolesche Variable namens istEinePrimzahl
definiert.
Für jeden neuen Kandidaten wird diese Variable auf true gesetzt.
Sobald
ein Teiler gefunden wird, wird sie auf false gesetzt.
Nachdem alle Divisoren durchgetestet worden sind, wird der Kandidat ausgegeben,
sofern istEinePrimzahl immer noch true ist.
Optimierung
Allerdings ist diese Lösung nicht optimal. So wird die innere Schleife
auch dann noch bis zum Ende durchlaufen, wenn der Kandidat bereits durch 2
teilbar ist. Das können Sie leicht abstellen, indem die Schleife eine
zusätzliche Bedingung erhält. Es wird wie bisher geprüft, ob alle Divisoren
für den Kandidaten durchlaufen wurden, und ergänzend, ob bisher
noch keine Teilbarkeit festgestellt wurde.
#include <iostream>
using namespace std;
int main()
{
const int MaxPrimzahl=100;
int Primzahl, Divisor;
bool istEinePrimzahl;
cout << "2";
for (Primzahl=3; Primzahl<=MaxPrimzahl; Primzahl++)
{
istEinePrimzahl = true;
// Pruefe, ob Primzahl wirklich eine Primzahl ist
for (Divisor=2; istEinePrimzahl && Divisor<Primzahl;
Divisor++)
{
// Ist das restlos teilbar?
if (0==Primzahl % Divisor)
{
// Zahl ist teilbar, ist also keine Primzahl!
istEinePrimzahl = false;
}
}
// Pruefung ist beendet.
// Wenn es eine Primzahl ist, ausgeben!
if (istEinePrimzahl)
{
cout << ", " << Primzahl;
}
}
cout << endl;
}
Dieses Programm liefert dieselben Ergebnisse, läuft aber wesentlich
effizienter. Vielleicht bemerken Sie das erst, wenn Sie MaxPrimzahl
auf 10.000 setzen.
Zum Abschluss sehen Sie in Abbildung {primzahldiagramm}
das Struktogramm des Primzahlprogramms.
Größter gemeinsamer Teiler
Der größte gemeinsame Teiler (ggT) zweier Zahlen ist die größte der Zahlen,
durch die beide Zahlen restlos teilbar sind.
Die Berechnung des größten gemeinsamen Teilers wird vor allem zum Kürzen
von Brüchen eingesetzt. Werden Zähler und Nenner jeweils durch den größten
gemeinsamen Teiler dividiert, ist der Bruch optimal gekürzt.
Der Algorithmus zur Berechnung des größten gemeinsamen Teilers könnte
auf einer Primzahlenberechnung basieren. Dazu würde man beide Zahlen
in ihre Primfaktoren zerlegen und alle gemeinsamen Primfaktoren
multiplizieren.
Aber es geht noch einfacher. Euklid hat bereits vor über 2000 Jahren ein sehr
leicht umzusetzendes Verfahren beschrieben, wie der ggT zu berechnen ist.
Euklid
Die beiden Zahlen, deren ggT zu suchen ist, nennen wir a und b.
Die Idee beruht auf der Erkenntnis, dass der ggT nicht nur Teiler von a und
b ist, sondern auch von b und (a-b), sofern b größer als a
ist. (Ich nehme mal an, dass Sie nicht an einem Beweis dieser
Erkenntnis interessiert sind.)
Man kann also, statt a und b zu untersuchen, auch b und (a-b) untersuchen.
Damit braucht die größere der beiden Zahlen schon nicht mehr untersucht zu
werden. Mit den beiden neuen Zahlen kann man aber wieder genauso verfahren
wie mit a und b. Wieder wird statt der größeren Zahl die Differenz der
beiden Zahlen weiterverwendet.
Wenn man dieses Verfahren so lange wiederholt, bis die kleinere Zahl 0
wird, muss die andere Zahl der ggT sein.
Zuerst das Struktogramm
In diesem Beispiel soll die Entwicklung des Programms mit dem Struktogramm
beginnen. Zunächst werden die beiden Kandidaten eingelesen. Danach
läuft eine Schleife so lange, wie der kleinere Wert größer als 0 ist.
Innerhalb der Schleife muss geprüft werden, welche die größere der beiden
Zahlen ist. Gegebenenfalls werden die Werte noch getauscht, bevor sie
voneinander abgezogen werden.
Tauschen
Damit ist die allgemeine Beschreibung des Algorithmus formalisiert und
in ein Struktogramm gefügt. Fast alles lässt sich leicht direkt in C++-Code
umsetzen. Lediglich das Tauschen des Variableninhalts muss näher beschrieben
werden, bevor es in Programmcode umgesetzt werden kann. Der spontane Gedanke
wäre, erst a=b und dann
b=a zu schreiben. Das
wird aber nicht funktionieren, weil der Variableninhalt von a in der
ersten
Zuweisung überschrieben wird und damit verloren geht. Es muss also eine
Hilfsvariable verwendet werden, in die zuerst der Inhalt von a
gesichert wird, bevor er überschrieben wird.
hilf = a;
a = b;
b = hilf;
Das Tauschen wird jetzt noch in das Struktogramm eingefügt. Gleichzeitig
werden die Bezeichnungen der Syntax von C++ angenähert.
Übertragen in C++
Nun ist es eine leichte Übung, dieses Struktogramm in ein entsprechendes
C++-Programm zu überführen. Das Struktogramm kann also eine Hilfe sein,
ein Programm zu entwickeln, vor allem, wenn komplexere Abläufe vorliegen.
#include <iostream>
using namespace std;
int main()
{
int a, b, hilf;
cin >> a;
cin >> b;
while (b>0)
{
if (b>a)
{
// Tausche a und b
hilf = a;
a = b;
b = hilf;
}
a = a - b;
}
cout << a << endl;
}
Übungen
Diese Seite basiert auf Inhalten aus dem Buch
Arnold Willemer: Einstieg in C++
Mit freundlicher Genehmigung und Unterstützung des Verlags galileo computing
| Informatik-Ecke Einstieg in C++ |
(C) Copyright 2005 Arnold Willemer
|