C++/Tutorial: 3. Kontrollstrukturen
Inhaltsverzeichnis
Kontrollstrukturen
Unsere bisherigen Programme können zwar auf Eingaben reagieren, aber das ganze nur sehr stupide. Wir können Benutzereingaben zwar zu einem gewissen Grad weiterverarbeiten, aber
wirkliche Programmlogik können wir bisher noch nicht realisieren. Dazu müsste das Programm zum Beispiel entscheidungen treffen können. Dazu gibt es die sogenannten "Kontrollstrukturen", sie lenken den Programmfluss.
Auch an dieser Stelle möchte ich zunächst einmal ein Beispielprogramm anbringen, das dann schrittweise verstanden werden soll.
#include <iostream> //macht cout, cin und string verfügbar #include <limits> //macht numeric_limits verfügbar using namespace std; int main() { cout << "Welche Rasse wollen Sie wählen?(Mensch, Troll, Ork)" << endl; //Begrüßungstext string race; //Die Variable, die die Rasse als einfache Zeichenkette enthält cin >> race; //Einlesen der Benutzereingabe cin.clear(); //cin Fehlerflags löschen cin.ignore(numeric_limits<streamsize>::max(), '\n'); //cin Puffer leeren if(race == "Mensch") //Wenn die Rasse "Mensch" ist, dann cout << "Ein hässlicher, stinkender Mensch, also?" << endl; //gib aus else if(race == "Troll") //ansonsten: wenn die Rasse Troll ist, dann cout << "Ein alter dummer Troll, also?" << endl; //gib aus else if(race == "Ork") //ansonsten: wenn die Rasse Ork ist, dann cout << "Ein prächtiger Ork, also? Zweifelsohne die edelste Rasse." << endl; //gib aus else //ansonsten cout << "Keine gültige Rasse" << endl; //gib aus cin.get(); //halte Konsole offen }
Boolesche Ausdrücke
Da das meiste des obigen Beispiels bereits bekannt sein sollte, springen wir gleich zu der ersten if-Klausel, genauer gesagt zu dem, was dort in der Klammer steht.
Es handelt sich dabei um einen sogenannten booleschen Ausdruck. Boolesche Logik spielt eine wichtige Rolle bei Bedingungen und generell in der Programmierung, sie befasst sich mit
Wahrheitswerten. Wie bereits im vorigen Kapitel angemerkt, gibt es in C++ den Datentyp bool, der Wahrheitswerte speichert. Ein boolescher Ausdruck entspricht eben einem solchen Wahrheitswert und so ist es auch möglich den Ausdruck stellvertretend für dessen Resultat zu verwenden und das Resultat einer booleschen Variable zuzuweisen.
Nehmen wir nun das Beispiel
race == "Mensch"
hierbei handelt es sich um einen Vergleich mit dem Vergleichsoperator ==, welcher auch auf Gleichheit überprüft. Wenn race also "Mensch" entspricht, ist der Ausdruck true, wenn er es nicht tut, ist der Ausdruck false.
Das Ergebnis könnten wir nun auch einer booleschen Variable zuweisen
bool is_race_human(race == "Mensch"); //wenn race "Mensch" entspricht, ist is_race_human = true, ansonsten ist is_race_human = false
Vergleichsoperatoren
Als Beispiel hatten wir nun den Vergleichsoperator ==, es gibt aber noch eine Reihe anderer Vergleichsoperatoren.
- a == b überprüft auf Gleichheit von a und b
- a != b ist true, wenn a ungleich b ist
- a < b ist true, wenn a kleiner b ist
- a <= b ist true, wenn a kleiner-gleich b ist
- a >= b ist true, wenn a größer-gleich b ist
- a > b ist true, wenn a größer b ist
wobei a und b für beliebige Ausdrücke steht(nochmal zur Erinnerung: ein Ausdruck ist alles was zu einem Wert ausgewertet werden kann).
Logische Operatoren
Oftmals reichen Vergleichsoperatoren nicht aus, sondern es ist erforderlich mehrere logische Ausdrücke zu verbinden, das geschieht mit den logischen Operatoren.
Logische Operatoren sind
- a && b ist true, wenn sowohl a als auch b true sind
- a || b ist true, wenn mindestens einer der beiden Operanden true ist
- !a ist true, wenn a false ist.
Es wäre nun z.B. möglich zu schreiben:
a == b || a == c
was true wäre, wenn a b ist, oder a c ist. Man könnte dies nun auch verneinen
!(a == b || a == c)
das wiederspräche aber dem logischen Denkvermögen, sinnvoller wäre es zu sagen, dass a nicht b und nicht c sein darf, also
a != b && a != c
was äquivalent zu obigem Code wäre
Operatorenpriorität
Nachdem nun bereits einige Operatoren angesprochen wurden(arithmetische Operatoren, Vergleichsoperatoren, logische Operatoren, <<, >>) wäre es sinnvoll etwas über die Reihenfolge zu wissen, wie die Operatoren ausgewertet werden. Eine Regel ist bereits bekannt: Punkt-vor-Strichrechnung gilt in C++, wie in der Mathematik. Zusätzlich gibt es aber noch ein viel komplexeres Konstrukt der Operatorenpriorität, die festlegt welcher Operator priorität hat, sie lautet wie folgt
- ::
- ++(Postfix), --(Postfix), (), [], ., ->, typeid, const_cast, static_cast, dynamic_cast, reinterpret_cast
- ++(Prefix), --(Prefix), +(Unär - sprich als Vorzeichen), -(Unär), !, ~, C-Style-Cast, *, &, sizeof, new, new[], delete, delete[]
- .*, ->*
- *, /, %
- +,-
- <<,>>
- <, <=, >, >=
- ==, !=
- &
- ^
- |
- &&
- ||
- ?:
- =, +=, -=, *=, /=, %=, |=, ^=, &=, >>=, <<=, throw
- ,
Bedingungen
Nachdem wir nun Ausdrücke formulieren können, die zu true oder false evaluieren, wird es Zeit sich um die Anwendung zu kümmern: eine Bedingung.
Eine Bedingung wäre zum Beispiel die if-Klausel, bei der die folgende Anweisung oder der folgende Block nur ausgeführt wird, wenn der Ausdruck, der in Klammern gesetzt dahinter steht true ist.
so zum Beispiel
if(race == "Mensch") cout << "Mensch!" << endl;
gibt nur "Mensch!" aus, wenn race == "Mensch" wahr ist, also race Mensch entspricht. Dazu gibt es noch die else-Klausel, die nach der bedingten Anweisungen anzutreffen ist und, die ausgeführt, wird, wenn der Ausdruck in if nicht wahr ist.
if(race == "Mensch") cout << "Mensch!" << endl; else cout << "kein Mensch!" << endl;
gibt also "Mensch!" aus, wenn race gleich "Mensch" ist und "kein Mensch!" wenn dem nicht so ist. Das in dem Beispiel verwendete else if ist keine spezielle Anweisung, sondern zusammengesetzt aus else und if, sprich ein anderer Weg es zu formatieren wäre:
if(race == "Mensch") //Wenn die Rasse "Mensch" ist, dann cout << "Ein hässlicher, stinkender Mensch, also?" << endl; //gib aus else if(race == "Troll") //ansonsten: wenn die Rasse Troll ist, dann cout << "Ein alter dummer Troll, also?" << endl; //gib aus else if(race == "Ork") //ansonsten: wenn die Rasse Ork ist, dann cout << "Ein prächtiger Ork, also? Zweifelsohne die edelste Rasse." << endl; //gib aus else //ansonsten cout << "Keine gültige Rasse" << endl; //gib aus
aber das ist eindeutig weniger schön und weniger lesbar.
Switch-Case
Eine Alternative für viele else-ifs mit integralen Typen ist die switch case Anweisung.
switch überprüft einen Wert und springt automatisch zum passendem case. break; verlässt die switchanweisung dann wieder, das ganze könnte z.B. folgendermaßen aussehen
switch(num) { case 0: cout << "null" << endl; break; case 1: cout << "eins" << endl; break; case 2: cout << "zwei" << endl; break; default: cout << "über zwei" << endl; break; }
wenn num also 0 ist, wird "null" ausgegeben, wenn es 1 ist wird "eins" ausgegeben, für 2 "zwei". Für alle anderen Fälle (default) wird "über zwei" ausgegeben.
Wenn wir auf break verzichten würde, würde er im Falle von 0 "null", "eins", "zwei" und "über zwei" ausgeben, da switch nur zum entsprechendem case springt, ab da dann allerdings bis zu einer break Anweisung alle Anweisungen ausgeführt werden.
Blöcke
Nun können wir bisher mit if Anweisungen nur einzelne Anweisungen bedingen, nicht aber mehrere. Meistens ist es jedoch notwendig, dass mehrere Anweisungen ausgeführt werden.
Wie bereits erwähnt führt if die nachfolgende Anweisung oder den nachfolgenden Block aus, wenn die Bedingung erfüllt ist und eben jene Blöcke sind es, die es ermöglichen mehrere Anweisungen zu bedingen.
Ein Block besteht aus einer Reihe Anweisungen, die innerhalb von geschweiften Klammern zu finden sind
if(some_condition) { //Block Anfang cout << "erste Ausgabe" << endl; cout << "zweite Ausgabe!" << endl; } //Block Ende
Ein Block fasst im Prinzip mehrere Anweisungen zusammen
Variablenscope
Eng verbunden mit Blöcken ist das Prinzip des Gültigkeitsbereiches für lokale Variablen.
Alle Variablen, die wir bisher gesehen haben, waren lokale Variablen, da wir uns bisher immer nur innerhalb einer Funktion und eines Blockes bewegt haben, waren die lokalen Variablen
für uns auch überall sichtbar. Tatsächlich sind Variablen aber nur in dem Block gültig, indem sie deklariert wurden.
if(some_condition) { int var(0); var += 1;//korrekt } var += 1; //Fehler: var ist in diesem Block nicht mehr gültig
Im inneren Block sind weiterhin alle Variablen aus dem äußeren Block sichtbar, es ist jedoch möglich sie zu überschreiben:
int i(0); { i += 1; //verändert das äußere i int i(0); //erstellt inneres i i += 1; //verändert das innere i, da es das äußere i verdeckt } i += 1; //verändert das äußere i, da das innere i hier nicht existiert
Schleifen
Die zweite Art von Kontrollstrukturen neben den Bedingungen sind die Schleifen, sie sorgen dafür, dass sich bestimmte Programmabschnitte wiederholen.
Stellt euch ein Spiel vor, hier handelt es sich auch nicht um ein stur durchlaufendes Programm, sondern um ein Programm, dessen Laufzeit variiert und bei dem diverse Aufgaben immer wieder wiederholt werden.
Es gibt derzeit 3 Arten von Schleifen: while-Schleifen, do-while-Schleifen und for-Schleifen
Die While-Schleife
Die While-Schleife ist die einfachste Schleife, sie ist der Bedingung mit if sehr ähnlich und sieht wie folgt aus:
string race(""); while(race != "Mensch" && race != "Troll" && race != "Ork") { cin >> race; cin.clear(); cin.ignore(numeric_limits<streamsize>::max(), '\n'); }
While ist wie eine if-Bedingung aufgebaut, mit dem Unterschied, dass sie die folgende Anweisung oder den folgenden Block solange wiederholt, bis die Bedingung false ist.
Das heißt in dem Fall, werden die 3 Zeilen zur Eingabe solange wiederholt, solange race eine ungültige Eingabe ist. Es wird also immer eine neue Eingabe verlangt, bis race entweder "Mensch", "Troll", oder "Ork" entspricht.
Als Leitsatz kann man sagen, dass die while-Schleife immer dann verwendet werden sollte, wenn die Anzahl der Schleifendurchläufe sich aus dem Schleifenkontext ergibt, das Programm beim eintreten in die Schleife also noch nicht weiß, wie oft der Code nun wiederholt werden soll.
Die Do-While-Schleife
Dennoch wäre es in dem Fall sinnvoller zu einer anderen Schleife zu greifen und zwar der Do-While-Schleife. Die Do-While-Schleife ist zur While-Schleife eigentlich semantisch identisch, mit dem Unterschied, dass die Bedingung erst am Ende des Schleifendurchlaufes überprüft wird. Das hat zur Folge, dass der Code >mindestens< ein mal ausgeführt wird, egal ob die Bedingung true oder false ist.
In dem Fall wollen wir mindestens eine Eingabe, vor der ersten Eingabe kann race auch nicht korrekt sein, deswegen wäre folgendes sinnvoller
string race; do { cin >> race; cin.clear(); cin.ignore(numeric_limits<streamsize>::max(), '\n'); } while(race != "Mensch" && race != "Troll" && race != "Ork");
Hier möchte ich auch nochmal drauf aufmerksam machen, dass die Do-While-Schleife die einzige Kontrollstruktur ist, die ihren Kopf nach dem Körper hat und dadurch auch die einzige, die mit einem Semikolon abgeschlossen werden muss.
Anwendung: Wann immer eine While-Schleife gefordert ist, die mindestens einmal durchlaufen werden muss.
Die For-Schleife
Die For-Schleife ist eine Zählschleife. Ihr Schleifenkopf ist in 3 Teile aufgeteilt:
for(Deklaration;Bedingung;Zähler)
In dem Abschnitt zur Deklaration lassen sich Variablen definieren, die über die gesamte Schleife hinweg gültig sind, üblicherweise ein Zähler.
Der Abschnitt zur Bedingung ist identisch zu dem der While-Schleife
Der Abschnitt des Zählers enthält leglich eine Anweisung, die nach jedem Schleifendurchlauf durchgeführt wird.
Wozu man das braucht erscheint im ersten Moment vielleicht nicht ganz klar, man könnte zum Beispiel statt folgendem
for(int i(0); i < 5; i += 1) // i ist der Schleifenzähler, die Schleife läuft solange i unter 5 ist, nach jedem Schleifendurchlauf wird i um 1 hochgezählt -> die Schleife wird 5 mal ausgeführt cout << "hi" << endl;
auch folgendes schreiben
int i(0); while(i < 5) { cout << "hi" << endl; i += 1; }
In der Tat ist der größte Unterschied auch ein syntaktischer, die for-Schleife ist in solchen Fällen leichter zu lesen und schneller zu schreiben. Der einzige semantische Unterschied besteht im Scope von i, da i bei der for-Schleife nur innerhalb der for Schleife gültig ist(im Gegensatz zu einer in der Schleife deklarierten Variable aber für alle Durchläufe) während die Variable bei der while-Schleife im übergeordnetem Block gültig ist.
Die for-Schleife ist dann sinnvoll, wenn das Programm zum Zeitpunkt des Eintrittes in die Schleife bereits weiß, wie oft die Schleife durchlaufen werden wird.
Inkrement- und Dekrementoperatoren
Für for-Schleifen ist es häufig sinnvoll Integervariablen um 1 zu erhöhen oder zu erniedrigen, das heißt zu inkrementieren oder zu dekrementieren. Für diesen Sonderfall bietet C++ nochmal 2 besondere Operatoren(welche übrigens auch schneller sind, also i += 1), den Inkrement- und Dekrementoperatoren ++ und --.
Man könnte z.B. mit
i++;
eine Variable schnell um 1 erhöhen und mit -- um 1 erniedrigen.
Beide Operatoren gibt es in einer Postfix und einer Prefixvariante, das bedeuted folgendes:
In der Postfixvariante, die oben vorgestellt ist, wird der Operator der Variable nachgestellt (i++), in der Prefixvariante wird der Operator der Variable vorangestellt(++i). Beide Varianten erhöhen die Variable um 1, wo liegt also der Unterschied? Und zwar hat der Ausdruck "i++" oder "++i" auch einen Wert an sich, so wäre es zum Beispiel gültig zu schreiben
int a(i++);
Der Unterschied liegt im Wert des Ausdruckes, während die vorangestellte Prefixvariante als Wert des Ausdrucks den bereits erhöhten Wert liefert, erhöht die nachgestellte Postfixvariante den Wert erst nachdem der Ausdruck ausgewertet wurde.
int i(0); int a(i++); //a ist nun 0, i ist nun 1 int b(++i); //b ist nun 2, i ebenfalls
Das böse Goto
Sei es nur als Warnung möchte ich dieses Konstrukt ebenfalls erwähnen. Oftmals wird einem vieles einfacher erscheinen, wenn man leglich eine Anweisung hätte, mit der man zu
einer bestimmten Stelle im Code springen kann. Derartige Sprunganweisungen gab es auch schon immer, sie weisen allerdings große Probleme auf, denn sie erzeugen sogenannten "Spaghetti-Code", das heißt Code, der nur schwer zurückzuverfolgen und dementsprechend auch nur schwer wartbar oder erweiterbar ist. Aus diesem Grund hat man sich in den 60er Jahren von diesem Konstrukt abgewendet und stattdessen eine Reihe anderer Sprachmittel eingeführt.
In vielen Tutorials wird goto als "böse" bezeichnet und es ist vollkommen korrekt, dass man es nicht verwenden sollte. Allerdings gilt genau so, dass man sich niemals dogmatisch an irgendwelche Regeln halten sollte, weswegen ich das goto Statement hier ebenfalls erläutern werde. Dennoch möchte ich noch einmal explizit darauf hinweisen, dass man alle Probleme auch anders und in 99,9% der Fälle besser lösen kann und man goto nur dann verwenden sollte, wenn man sich absolut sicher ist, dass es der beste Weg ist. Ich persönlich habe in C++ noch nie einen solchen Fall erlebt, von daher bin ich mir auch ziemlich sicher, dass ihr dieses Konstrukt so bald nicht brauchen werdet und es auch nicht verwenden solltet!
Für eine Sprunganweisung wird eine Sprungmarke, ein "Label" benötigt, zudem gesprungen werden kann. Dieses wird folgendermaßen in den Code gesetzt
name_des_labels: cout << "hi" << endl;
es ist nun möglich mit goto zum label zu springen:
goto name_des_labels;
sodass das Programm seinen Fluss vom Punkt des Labels an fortsetzt.