Verzweigungen

Willemers Informatik-Ecke

Diese Seite basiert auf Inhalten aus meinem Buch "Einstieg in C++", seinerzeit erschienen im Verlag Galileo Computing. Das Buch ist inzwischen vergriffen.


Das Nachfolgebuch heißt C++. Der Einstieg und ist bei Wrox im Verlag Wiley-VCH erschienen.

Die Inhalte dieser Website unterliegen meinem Urheberrecht und dürfen trotz inhaltlicher Überschneidungen mit dem Buch dank der freundlichen Genehmigung des Verlags Wiley-VCH hier erscheinen.

>>Wenn ein Mädchen unter 21 ist, wird sie vom Gesetz geschützt. Wenn sie erst über 60 ist, wird sie von der Natur geschützt. Und alles, was dazwischen ist, ist zur Jagd freigegeben.<<
Cary Grant in >>Unternehmen Petticoat<<.

Es gibt diverse Gründe, warum ein Programm in Abhängigkeit von Variablen bestimmte Operationen nicht ausführen soll. Hier sind ein paar typische Beispiele zusammengestellt:

Das Programm soll in all diesen Fällen in Abhängigkeit von einem Variableninhalt seinen Ablauf ändern. Dazu brauchen wir einen Befehl, der unter einer Bedingung eine Anweisung oder einen Anweisungsblock ausführt. Um die Bedingung zu formulieren brauchen wir Operatoren, mit denen wir Werte vergleichen können.

Nur unter einer Bedingung: if

Wenn ein Mann seine Frau aus dem Fenster wirft, liest man das in der BILD-Zeitung.
Wenn eine Frau ihren Mann aus dem Fenster wirft, liest man das in >>Schöner Wohnen<<.
Zitat unbekannter Herkunft

Mit dem Befehl if ist es möglich, eine Befehlssequenz unter einer Bedingung auszuführen. Das Schlüsselwort if kommt aus dem Englischen und bedeutet >>falls<< oder >>wenn<<. Die Syntax der if-Anweisung ist in Abbildung dargestellt.

Grafik nur im Buch vorhanden

Dem Schlüsselwort if folgt eine Klammer, in der die Bedingung formuliert wird, unter der der nachfolgende Anweisungsblock ausgeführt wird. Ein Anweisungsblock ist entweder eine einzelne Anweisung oder mehrere Anweisungen, die durch geschweifte Klammern zusammengefasst werden.

Grafik nur im Buch vorhanden

Einrücken

Es hat sich bewährt, Anweisungen, die unter Bedingungen oder in Schleifen ausgeführt werden, einzurücken. Einige Programmeditoren führen dies automatisch durch. Für das Einrücken können die Tabulatortaste oder Leerzeichen verwendet werden. Sofern Sie mit unterschiedlichen Editoren oder gar Systemen arbeiten, sollten Sie Leerzeichen verwenden, weil nicht alle Editoren die Tabulatoren gleich interpretieren. Im ersten Beispiel soll der Inhalt zweier Variablen durch einander geteilt werden. Das Programm würde an dieser Stelle abstürzen, wenn der Divisor 0 wäre. Aus diesem Grund wird ein vorsichtiger Programmierer vor der Berechnung eine Prüfung vornehmen, ob der Divisor ungleich 0 ist. Das Zeichen für Ungleichheit ist ein Ausrufezeichen, gefolgt von einem Gleichheitszeichen.
if (Divisor != 0)
    Ergebnis = Dividend / Divisor;
Die Division wird nur durchgeführt, wenn die Bedingung zutrifft, also der Divisor ungleich 0 ist.

Verschachtelt

Die Anweisung, die hinter der if-Bedingung steht, kann selbst wieder eine if-Anweisung sein. Sie wird nur ausgeführt, wenn die erste if-Bedingung eintritt. Eine solche Folge von Abfragen nennt man Verschachtelung. Das folgende Beispiel zeigt zwei verschachtelte if-Anweisungen. Hier werden die Variablen auf Gleichheit geprüft. Das Zeichen für Gleichheit besteht aus zwei Gleichheitszeichen, um es gegen das Zuweisungszeichen abzugrenzen.
c=2;
if (a==0)
    if (b==0)
        c=0;
cout << c << endl;
Die Abfrage, ob die Variable b 0 enthält, wird nur gestellt, wenn die Variable a den Inhalt 0 hat. Nur wenn beide Variablen 0 sind, wird die Variable c auf 0 gesetzt. Ansonsten enthält sie weiterhin 2.

Andernfalls: else

Kehren wir zum Divisionsbeispiel zurück. Es wäre schön, wenn das Programm nicht nur prüft, ob der Divisor ungleich 0 ist, sondern den Anwender darüber informieren würde, dass es die Division gar nicht durchführt. Diese Meldung sollte genau dann erscheinen, wenn die if-Bedingung nicht zutrifft. Das könnten Sie erreichen, indem Sie zwei gegensätzliche if-Konstruktionen hintereinander setzen:

if (Divisor != 0)
    Ergebnis = Dividend / Divisor;
if (Divisor == 0)
    cout << "Divisor ist 0! Keine Berechnung!";

Da solche Konstruktionen immer wieder gebraucht werden, bietet C++ einen eigenen Befehl an, um den gegenteiligen Fall der Bedingung abzudecken. Das Schlüsselwort dazu heißt else.

if (Divisor != 0)
    Ergebnis = Dividend / Divisor;
else
    cout << "Divisor ist 0! Keine Berechnung!";

Abbildung {grafifelse} zeigt den vollständigen Syntaxgraph für if mit else.

Grafik nur im Buch vorhanden

Struktogramm

Programme mit verschachtelten Abfragen und Schleifen werden schnell unübersichtlich. Um den Ablauf von Programmen anschaulich darzustellen, sind verschiedene Diagramme entworfen worden. In diesem Buch wird das Nassi-Schneidermann-Diagramm verwendet. Diese Diagramme sind ein Design-Werkzeug, um wohlstrukturierte Programme zu entwickeln.

Ein Nassi-Schneidermann Diagramm stellt ein Programm oder ein Teilprogramm als großes Rechteck dar. Jeder Programmteil wird oben gestartet und endet immer unten. Seitenausstiege gibt es nicht. Ein Programm, das keine Abfragen oder andere Kontrollstrukturen hat, sieht aus wie ein Stapel Ziegelsteine von der Seite.

Grafik nur im Buch vorhanden

Abfragen teilen den Block

Bei einer Abfrage wird der Block in senkrechter Richtung geteilt. Der linke Block hält die Anweisungen, die ablaufen, wenn die Abfrage zutrifft, und der rechte Block wird im anderen Fall durchlaufen. Die Abfrage schreibt man in ein auf der Spitze stehendes Dreieck oberhalb der geteilten Anweisung. Abbildung {abfragediagramm} zeigt eine verschachtelte Abfrage. Darin wird beschrieben, wie man sich verhält, wenn man mal für kleine Programmierer muss.

Grafik nur im Buch vorhanden

Betrachten wir das Struktogramm im Zusammenhang mit einem Programmbeispiel aus dem Bankbereich. Ein Kunde bekommt 3% Zinsen auf seine Spareinlagen. Legt er Geld drei Jahre oder länger fest, bekommt er 4% und bei Festlegung von 6 Jahren oder mehr sogar 5%.

Grafik nur im Buch vorhanden

Das Diagramm kann direkt in ein Programm umgesetzt werden.

if (AnlageDauer<3)
{
    ZinsSatz=3;
}
else
{
    if (AnlageDauer<6)
    {
        ZinsSatz=4;
    }
    else
    {
        ZinsSatz=5;
    }
}

Dangling else

Eine besondere Problematik ergibt sich bei zwei verschachtelten Abfragen, wenn es nur ein else gibt. Dann stellt sich die Frage, zu welchem if das else gehört. Diese Problematik nennt man das >>Dangling-Else<<-Problem. Dangling else könnte man mit >>baumelndem else<< übersetzen.
if (a == 0)
    if (b == 0)
        c=5;
else
    cout << "Wohin gehöre ich?";
Sie werden vielleicht spontan denken, dass das else zu der Abfrage auf die Variable a gehört. Das liegt aber nur an der etwas boshaften Einrückung. Da das else auf der gleichen Ebene steht wie die Abfrage nach a, wird suggeriert, dass es auch zu dieser Abfrage gehört. Das ist aber falsch. Die Sprache C++ definiert in diesem Fall, dass das else das letzte if bedient und damit zur inneren Abfrage der Variablen b gehört.

Blockbildung klärt

Dieselbe Situation wird auf den ersten Blick eindeutig, wenn die Anweisungen hinter dem if in geschweifte Klammern geschrieben werden. Die beiden möglichen Varianten sind zur besseren Übersicht nebeneinander gesetzt worden. Die Einrückung ist in beiden Fällen die Gleiche. Im linken Beispiel gehört das else zur Abfrage der Variablen a und im rechten Beispiel zur inneren Abfrage auf Variable b.

if (a == 0)                   if (a == 0)
{                             {
    if (b == 0)                   if (b == 0)
    {                             {
        c=5;                          c=5;
    }                             }
}                                 else
else                                  cout << "Wohin?";
    cout << "Wohin?";         }
Durch das Einfügen der Blockgrenzen wird die Situation sofort klar. Sobald Sie den Einrückungsregeln folgen, wird auch aus der Einrückungstiefe sofort klar, wohin das else gehört.

Sie sehen an diesem Beispiel sehr gut, dass das Verwenden von geschweiften Klammern die Zugehörigkeit der Anweisungen verdeutlicht. Schon aus diesem Grund würde ich Ihnen empfehlen, beim if nicht an den geschweiften Klammern zu sparen.

Fall für Fall: switch case

Die Abfrage mit if ermöglicht die Unterscheidung zweier Fälle. Wenn auf Grund verschiedener Werte unterschiedliche Anweisungen ausgeführt werden sollen, können mehrere verschachtelte if-Anweisungen hintereinander gesetzt werden oder es kann eine spezielle Fallunterscheidung verwendet werden. Ein schönes Beispiel ist ein Fahrstuhl in einem Kaufhaus, in dem eine Vielzahl von Stockwerken zur Auswahl stehen und in jedem Stockwerk andere Waren angeboten werden. Zunächst wird die Aufgabe durch kaskadierende if-Anweisungen gelöst:
if (Stockwerk == 1)
{
     cout << "Süssigkeiten, Bücher" << endl;
} 
else if (Stockwerk == 2)
{
    cout << "Bekleidung" << endl;
}
else if (Stockwerk == 3)
{
    cout << "Bekleidung" << endl;
}
else if (Stockwerk == 4)
{
    cout << "Spielzeug" << endl;
}
else if (Stockwerk == 5)
{
    cout << "Unterhaltungselektronik" << endl;
}
else
{
    cout << "Garage" << endl;
}
Wenn Sie es genau nehmen, habe ich etwas bei der Einrückung geschummelt. Eigentlich hätte der Quelltext nach jedem else und jedem if ein Stück eingerückt werden müssen. Dadurch wäre das Listing aber in diesem besonderen Fall auch nicht übersichtlicher geworden. Sie finden diese abgewandelte Form der Einrückung häufiger in Listings, wenn ein if direkt auf ein else folgt.

Diese Konstruktion lässt sich durch die Fallunterscheidung vereinfachen.

switch (Stockwerk)
{
    case 1:
        cout << "Süssigkeiten, Bücher" << endl;
        break;
    case 2:
    case 3:
        cout << "Bekleidung" << endl;
        break;
    case 4:
        cout << "Spielzeug" << endl;
        break;
    case 5:
        cout << "Unterhaltungselektronik" << endl;
        break;
    default:
        cout << "Garage" << endl;
        break;
}

Ganzzahliger Ausdruck

Die Fallunterscheidung beginnt mit dem Schlüsselwort switch. In der darauf folgenden Klammer steht der ganzzahlige Ausdruck, dessen Ergebnis die Verzweigung steuert. Ein Ausdruck kann eine Variable sein, wie im Beispiel die Variable Stockwerk. Hier könnte aber auch eine Berechnung stehen, die zu einem ganzzahligen Ergebnis führt. Es können auch Buchstaben verwendet werden, da Buchstaben aus Sicht von C++ letztlich nichts anderes als getarnte Zahlen sind. Im Beispiel wird die Variable Stockwerk ausgewertet, die offensichtlich eine ganze Zahl aufnehmen kann.

Sprungziele

In dem auf das Schlüsselwort switch folgenden Block gibt es Ansprungpunkte, die mit dem Schlüsselwort case gekennzeichnet sind. Darauf folgt eine Konstante, die für den Wert steht, der hier behandelt wird. Zum Abschluss des Ansprungpunktes steht ein Doppelpunkt. Stimmt der Wert des ganzzahligen Ausdrucks mit der Konstante überein, setzt das Programm seine Ausführung an dieser Stelle fort, bis es auf ein break-Kommando stößt. Dann wird der Block verlassen. Eine weitere case-Anweisung mit einer anderen Konstante stoppt das Programm keineswegs. Erscheint kein break, läuft das Programm unaufhaltsam bis zum Ende des Blocks.

Verzweiflungsfall

Findet sich kein passender case-Zweig, setzt das Programm beim Schlüsselwort default seinen Ablauf fort. Es empfiehlt sich, immer einen default-Zweig einzurichten. Zwingend ist dies aber nicht.

In Abbildung {grafswitch} ist der Syntaxgraph der Fallunterscheidung dargestellt.

Grafik nur im Buch vorhanden

Der Case-Block wird in einem separaten Syntaxgraph in Abbildung {grafcase} dargestellt.

Grafik nur im Buch vorhanden

In einer Hinsicht ist der Syntaxgraph nicht ganz korrekt: Danach wäre es erlaubt, default mehrfach in einem Case-Block zu verwenden. Sinnvollerweise darf aber nur ein default-Zweig in einem Case-Block auftreten. Dies ließe sich durch einen etwas komplexeren Syntaxgraphen noch darstellen. Spätestens die Regel, dass jede Konstante nur einmal verwendet werden darf, ist durch den Syntaxgraph nicht mehr darstellbar. Um der Übersicht willen bleibe ich bei dieser etwas vereinfachten Darstellung. Die Syntaxgraphen haben hier den Zweck, die Syntax zu veranschaulichen und sind nicht dafür gedacht, als Basis für die Entwicklung eines Compilers dienen.

Reihenfolge

Das Stockwerk-Beispiel könnte den Eindruck erwecken, dass alle Fälle lückenlos behandelt werden müssten. Das ist keineswegs erforderlich. Es ist auch nicht notwendig, dass die Fälle in aufsteigender Reihenfolge sortiert sind. Auch muss der default-Zweig nicht zwingend als letzter Fall angeführt werden.

In der Praxis kommt die Fallunterscheidung besonders bei der Reaktion auf Kommandos oder Ereignisse zum Zuge. In der Programmierung grafischer Oberflächen sind solche Fallunterscheidungen häufig zu finden. Grafische Applikationen warten auf ein Ereignis, das durch Tastatur, Maus oder andere Quellen ausgelöst wird, unterscheiden anhand einer Nachricht vom System, was der Benutzer ausgelöst hat, und reagieren entsprechend. Die Nachrichten sind üblicherweise ganzzahlige Werte.

Im folgenden Beispiel wird davon ausgegangen, dass in der Variablen Zeichen ein Buchstabe steht. % der vielleicht direkt von der Tastatur eingegeben wurde. Die Fallunterscheidung interessiert sich für die Buchstaben '3' bis '6' und '9'. Der einfachste Fall ist der, wenn eine '6' in Zeichen vorliegt. Dann wird die Variable e mit 1 belegt. Auch die Ziffer '5' ist noch recht einfach. Hier wird der Variablen d eine 2 zugewiesen.

switch (Zeichen)
{
case '3':
case '4':
case '9':
   a = 4;
case '5':
   d = 2;
   break;
case '6':
   e = 1;
   break;
}

Befindet sich '3', '4' oder '9' in der Variablen Zeichen, so wird zuerst die Variable a auf 4 gesetzt. Da danach kein break erfolgt, wird auch d mit 2 belegt. Erst dann stoppt ein break den weiteren Ablauf. Da es zulässig ist, break wegzulassen, erhalten Sie auch keine Fehlermeldung, wenn es fehlt. Solche Fehler sind schwer zu finden, da man dazu neigt, den Befehl case auch als Ende des vorigen Falles anzusehen.

An dem Beispiel ist auch zu erkennen, dass ein default nicht zwingend erforderlich ist. Alle nicht explizit genannten Fälle werden dann einfach nicht behandelt.

Kurzabfrage mit dem Fragezeichen

Mit Hilfe des Fragezeichens können if-Abfragen verkürzt werden, die lediglich den Wert einer Berechnung beeinflussen. Wenn Sie also einen Ausdruck benötigen, der sich in Abhängigkeit von einer Bedingung verändert, kann er mit dem Fragezeichen extrem kurz formuliert werden.

Ein typisches Beispiel ist die Berechnung der Betragsfunktion. Die Betragsfunktion liefert immer den positiven Betrag eines Wertes. Ist der Eingabewert negativ, muss er mit --1 multipliziert werden. Mit einer if-Anweisung würde das so formuliert werden:

if (Wert >= 0)
{
    Betrag = Wert;
}
else
{
    Betrag = -Wert;
}
Mit Hilfe eines Fragezeichens können Sie das wesentlich kürzer ausdrücken. Zunächst formulieren Sie die Bedingung (hier: Wert>=0). Dann folgt das Fragezeichen. Als Nächstes wird der Ausdruck genannt, der verwendet werden soll, wenn die Bedingung zutrifft (hier: Betrag=Wert). Durch einen Doppelpunkt abgetrennt, wird der Ausdruck ausgeführt, der verwendet werden soll, wenn die Bedingung nicht zutrifft (hier: Betrag=-Wert).
Betrag = Wert>=0 ? Wert : -Wert;
Grundsätzlich kann der Fragezeichen-Ausdruck an jeder Stelle stehen, die einen Ausdruck, also eine Auswertung, erwartet. Der Fragezeichen-Ausdruck hat folgenden Aufbau:

Grafik nur im Buch vorhanden

Der Fragezeichen-Ausdruck ist für viele Programmierer sehr irritierend, die andere Sprachen gewohnt sind, die sich nicht von C herleiten, weil es dort nichts Vergleichbares gibt.

Minimum

Das folgende Beispiel berechnet das Minimum aus den Variablen a und b und zeigt, wie der Fragezeichen-Operator Programme verkürzen kann.
Minimum = a < b ? a : b;
Vor dem Fragezeichen wird geprüft, ob a kleiner als b ist. Ist das der Fall, dann steht in der Variablen a der kleinere Wert, und deren Wert liefert der Ausdruck zwischen dem Fragezeichen und dem Doppelpunkt. Im anderen Fall muss b den kleineren Wert enthalten, und darum steht b auch hinter dem Doppelpunkt.


Informatik-Ecke Einstieg in C++ (C) Copyright 2005 Arnold Willemer