C++/Tutorial: 3. Kontrollstrukturen

Aus Scientia
Version vom 6. April 2011, 07:29 Uhr von Alexis Hiemis (Diskussion | Beiträge)

(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Wechseln zu: Navigation, Suche

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.