Elementare Syntax von C++: Bedingung, Alternative und Mehrfachalternative

Bedingung, Alternative und Mehrfachalternative sind die Kontrollstrukturen, die man einsetzen kann, um den Ablauf eines Programmes zu steuern. Erklärt wird ihre Syntax in C++ und es werden einige Spitzfindigkeiten diskutiert, die man bei ihrer Verwendung kennen sollte.

Einordnung des Artikels

Übersicht

Bei der Diskussion der Strukturelemente von Algorithmen und der Formulierung eines Algorithmus in Pseudocode (in Pseudocode und formale Sprachen) wurde bereits auf die zentrale Rolle von Bedingungs-Prüfung, Alternative und Mehrfachalternative hingewiesen. Diese drei Strukturelemente werden natürlich auch in der Sprache C++ unterstützt. Die zugehörige Syntax wird in den folgenden Abschnitten vorgestellt und es werden Beispiele und Aufgaben formuliert, um mit ihrer Anwendung vertraut zu werden.

Die Bedingungs-Prüfung

Im Pseudocode wurde die Bedingungs-Prüfung nicht eigens behandelt, da man sie als einen Spezialfall der Alternative verstehen kann. Da selbst die einfachste Form der Bedingungs-Prüfung zu Missverständnissen führen kann, wird sie hier natürlich besprochen.

Im folgenden Beispiel wird geprüft, ob eine eingegebene Zahl n negativ ist. Falls ja, wird ihr Vorzeichen umgedreht. Anschließend wird die Zahl ausgegeben — sie muss jetzt positiv sein.

int n;
cout << "Bitte eine Zahl eingeben ";
cin >> n;
if (n < 0)
    n = - n;
cout << "n = " << n << endl;            // jetzt muss n >= 0 sein

Allgemein sieht die Bedingungs-Prüfung folgendermaßen aus:

if (Bedingung)
    Anweisung1;
                // Hier ist die Bedingungs-Prüfung zu Ende
                // die folgende Anweisung wird immer ausgeführt
Anweisung2;

Zuerst wird die Bedingung geprüft. Falls sie erfüllt ist, wird die Anweisung1 ausgeführt; falls nicht, wird Anweisung1 nicht ausgeführt. Die darauf folgende Anweisung2 wird immer ausgeführt (also unabhängig davon, ob die Bedingung erfüllt war oder nicht).

Oben wurde gesagt, dass selbst die Bedingungs-Prüfung zu Missverständnissen führen kann, nämlich dann, wenn man durch eine Einrückung des Quelltextes einen bestimmten Ablauf suggerieren möchte. Man muss sich aber klarmachen, dass das Ende der Bedingungs-Prüfung nicht durch die Einrückung bestimmt wird.

Im folgenden Code-Schnipsel werden nach einer Bedingungs-Prüfung zwei Anweisungen ausgeführt:

if (Bedingung)
    Anweisung1;
    Anweisung2;

Welche Anweisung wird unter welcher Bedingung ausgeführt? Die Einrückung suggeriert hier, dass beide Anweisungen ausgeführt werden, wenn die Bedingung erfüllt ist. Diese Interpretation ist aber falsch. Nach Anweisung1 ist die Bedingungs-Prüfung zu Ende und Anweisung2 wird immer ausgeführt.

Soll auch Anweisung2 nur unter der Bedingung ausgeführt werden, müssen Klammern gesetzt werden:

if (Bedingung)
    {
    Anweisung1;
    Anweisung2;
    }
Anweisung3

Jetzt sind die beiden Anweisungen zu einem Block zusammengefasst und der Block wird als Ganzes ausgeführt, wenn die Bedingung erfüllt ist. Erst Anweisung3 wird immer ausgeführt.

Um zum obigen Beispiel zurückzukehren:

Falls n negativ ist, soll eine entsprechende Ausgabe erfolgen (Zeile 6) und das Vorzeichen soll umgedreht werden (Zeile 7). Der Quelltext lautet dann (geschweifte Klammern beachten):

int n;
cout << "Bitte eine Zahl eingeben ";
cin >> n;
if (n < 0)
    {
    cout << "es wurde eine negative Zahl eingegeben" << endl;
    n = - n;
    }
cout << "n = " << n << endl;            // jetzt muss n >= 0 sein

Fehlerquelle: Der Compiler beachtet Einrückungen nicht (er liest sie wie ein Trennungszeichen). Achten Sie darauf, dass Sie den gewünschten Programm-Ablauf durch Klammern erzwingen müssen (und nicht durch Einrückungen).

Die Alternative

Die Syntax der Alternative

Im Pseudocode wurde die Alternative mit den Textbausteinen Falls — dann — sonst formuliert:

Falls (Bedingung)
    dann (Anweisung1);
    sonst (Anweisung2);

In C++ entfällt das dann (in manchen Programmiersprachen wird hier then eingesetzt):

if (Bedingung)
    Anweisung1;
else
    Anweisung2;

Möchte man — falls die Bedingung erfüllt ist — anstelle der Anweisung1 mehrere Anweisungen ausführen, muss man wieder Klammern setzen (Zeile 2 und 5):

if (Bedingung)
    {
    Anweisung1;
    Anweisung2;
    }
else
    Anweisung3;

Dagegen führt der folgende Gebrauch von if-else zu einem Compiler-Fehler:

if (Bedingung)
    Anweisung1;
    Anweisung2;
else                        // Compiler-Fehler
    Anweisung3;

Der Fehler entsteht dadurch, dass if in der ersten Zeile hier vom Compiler als Bedingungs-Prüfung interpretiert wird, da nach Anweisung1 kein else folgt. Zu dem else in Zeile 4 gibt es kein zugehöriges if.

Verschachtelung von Alternativen

Mehrere Alternativen können ineinander verschachtelt werden, wobei man als Programmierer aber darauf achten sollte, dass die Quelltexte nachvollziehbar bleiben — achten Sie vor allem darauf, die Einrückungen richtig zu setzen.

Im folgenden Beispiel wird wieder eine Zahl n eingegeben und es werden die drei Fälle untersucht:

int n;
cout << "Bitte eine Zahl eingeben ";
cin >>  n;

if (n < 0)
    cout << "x ist negativ" << endl;
else
    if (n > 5)
        cout << "x > 5" << endl;
    else
        cout << "0 <= n <= 5" << endl;

Es ist nicht die einzige Anforderung an den Quelltext, leicht nachvollziehbar zu sein. Gerade wenn sich derartige Abfragen in Schleifen befinden, die sehr oft durchlaufen werden, beanspruchen sie sehr viel Rechenzeit. Daher soll man den wahrscheinlichsten Fall wenn möglich zuerst abfragen. Ist im Beispiel oben etwa n < 0, so wird die zweite Alternative nicht mehr betreten.

Andererseits sollen Sonderfälle immer zuerst abgefragt werden, da es oft vorkommt, dass die Berechnungen, die im Normalfall durchgeführt werden, sonst nicht durchgeführt werden können (wie eine Division durch null). Meist widersprechen sich die beiden zuletzt genannten Anforderungen.

Das dangling else Problem

Die Verschachtelung von Bedingung oder Alternativen führt oft zur Verwirrung, die das folgende Beispiel demonstriert. Es werden zwei int-Variablen x und y deklariert und eingegeben. Anschließend wird in verschachtelten if-Anweisungen festgestellt, welches Vorzeichen die Variablen haben. Versuchen Sie für die beiden nachfolgenden Quelltexte die Frage "Was weiß man hier über x?" zu beantworten:

// die int-Variablen x und y werden deklariert und über die Konsole eingegeben
if (x > 0)
    cout << " x ist positiv ";
    if (y > 0)
    else
        // Was weiss man hier über x??
// die int-Variablen x und y werden deklariert und über die Konsole eingegeben
if (x > 0)
    cout << " x ist positiv ";
    if (y > 0)
else
    // Was weiss man hier über x??

Die beiden Quelltexte sind identisch — bis auf die unterschiedliche Einrückung bei else.

Wie liest der Compiler diese Anweisung? Klar ist, dass er nicht auf die Einrückung achtet: Leerzeichen werden vom Compiler nur als Trennungszeichen gelesen, mit ihnen können aber niemals Anweisungen erzeugt oder verändert werden.

Die Lösung des Problems ergibt sich sofort, wenn man das Block-Konzept beachtet, das in der folgenden Abbildung für die Bedingung und Alternative nochmal erklärt wird.

Abbildung 1: Zur Erläuterung des dangling else Problems.Abbildung 1: Zur Erläuterung des dangling else Problems.

In Abbildung 1 sind links eine Bedingungs-Prüfung und eine Alternative (beide gelb hinterlegt) zu sehen. Soll in einen dieser beiden Blöcke eine Alternative eingebaut werden (Block rechts mit unterstrichenem if-else), so muss an Stelle einer Anweisung der Block eingefügt werden. Aber egal wie man dies macht, es wird immer das unterstrichene else nahe dem unterstrichenen if stehen. Beachtet man dieses Block-Konzept, kann man niemals das unterstrichene else als zu dem nicht unterstrichenen if gehörig ansehen.

Und daher ist in oben gestelltem Problem klar, wie der Compiler den (doppelt aufgeführten) Quelltext interpretieren muss: Das else gehört zu dem am nächsten gelegenen if und der Quelltext ist so zu lesen wie es die erste Darstellung (Einrückung beachten) suggeriert.

Das Problem des dangling else ist also weniger ein Problem von if else sondern vielmehr ein generelles Problem, wie Blöcke ineinandergeschachtelt werden können. Wie das Beispiel aus Abbildung 1 zeigt, ist es verwandt mit dem Problem, wie man Klammern in einem mathematischen Ausdruck setzen darf. So ist etwa

(5 + [3 + 8] + 2)

ein erlaubter Ausdruck. Dagegen dürfen die Klammern nicht wie folgt gesetzt werden:

(5 + [3 + 8) + 2]

In diesem Beispiel mag es noch erlaubt sein, da man die Klammern sowieso alle weglassen kann. Aber wie soll man folgenden Ausdruck interpretieren:

(5 + 2 * [3 + 8) + 2]

Der bedingte Operator

Die Alternative wird meist in der Form verwendet:

if (Bedingung)
    Anweisung;
else
    Anweisung;

wie in folgendem Beispiel:

if (x > 0)
    cout << " x ist positiv " << endl;
else
    cout << " x ist negativ " << endl;

Dafür gibt es auch eine Kurzschreibweise, nämlich den sogenannten bedingten Operator:

x > 0 ? (cout << " x ist positiv " << endl) : (cout << " x ist negativ " << endl);

Für den Anfänger ist die Konstruktion mit if-else leichter zu lesen. Damit auch die Anweisung mit dem bedingten Operator leicht zu lesen ist, empfiehlt es sich Klammern zu setzen — auch an Stellen, an denen sie eigentlich überflüssig sind. Und schreiben Sie vor und nach jedem Operator ein Leerzeichen — gerade der Doppelpunkt ist sonst schwer zu erkennen, wie etwa in:

x>0 ? (cout<<" x ist positiv "<<endl):(cout<<" x ist negativ "<<endl);  // Nicht zu empfehlen: schwer lesbar aufgrund fehlender Leerzeichen

Vielleicht hilft folgende Lesart:

Und wenn das noch nicht hilft, kann man den bedingten Operator auch als eine über drei Zeilen gehende Anweisung schreiben, die mehr an Falls — dann — sonst erinnert:

x > 0 ?
    (cout << " x ist positiv " << endl) : 
    (cout << " x ist negativ " << endl);

Der Nachteil hier ist, dass man den Doppelpunkt leicht mit einem Strichpunkt verwechseln kann.

Die Mehrfachalternative

Eine Mehrfachalternative kann immer aus verschachtelten Alternativen aufgebaut werden, es gibt aber auch die Möglichkeit der sogenannten switch-Anweisung. Dabei können beliebig viele Fälle untersucht werden. Aber um dies zu realisieren, reicht es nicht mehr wie bei Falls — dann — sonst eine Bedingung anzugeben (also eine Bedingung vom Datentyp bool), die entweder eintritt oder nicht. Für die Mehrfachalternative wird eine Variable von ganzzahligem Datentyp angegeben, und die möglichen Bedingungen werden mit Hilfe der Werte dieser Variable definiert. Erlaubt sind also Zeichen, Zahlen wie short und int, aber keine Gleitkommazahlen.

Das folgende Beispiel untersucht, ob ein gegebenes Zeichen eine der Hexadezimal-Ziffern A, B, ..., F (beziehungsweise a, b, ..., f) ist und gibt es als Dezimalzahl aus.

char c;
cout << "Bitte geben Sie ein Zeichen ein: ";
cin >> c;

switch ( c )
    {
    case 'A':
    case 'a':
        cout << "10" << endl;
        break;                  // Verlassen der switch-Anweisung für c == 'A' oder c == 'a'
    case 'B':
    case 'b':
        cout << "11" << endl;
        break;
    case 'C':
    case 'c':
        cout << "12" << endl;
        break;
    case 'D':
    case 'd':
        cout << "13" << endl;
        break;
    case 'E':
    case 'e':
        cout << "14" << endl;
        break;
    case 'F':
    case 'f':
        cout << "15" << endl;
        break;
    default:                        // wird ausgeführt, wenn keine der Bedingungen (case .. ) eingetreten ist
        cout << "Das Zeichen war keine Hexadezimal-Ziffer zwischen A und F!";
        break;
    }
    cout << "Danke!" << endl;

Eigenschaften der Mehrfachalternative:

  1. Die Mehrfachalternative wird mit dem Schlüsselwort switch eingeleitet. In den runden Klammern steht ein Ausdruck von ganzzahligem Datentyp: Man nennt ihn den Kontroll-Ausdruck. (Ein Gegenbeispiel mit Gleitkommazahlen folgt unten). Üblicherweise ist der Kontroll-Ausdruck eine Variable, deren Wert in den folgenden Anweisungen abgefragt wird.
  2. Es kann beliebig viele Fälle (case) geben (Zeile 7, 8, 11, ...).
  3. Es kann aber höchstens eine default-Anweisung geben (Zeile 31). Mit default-Anweisung ist gemeint, dass dieser Fall ausgeführt wird, wenn keine andere Bedingung zutreffend war. Die default-Anweisung muss nicht am Ende stehen, entsprechend ihrer inhaltlichen Bedeutung ist dies aber empfehlenswert.
  4. Die break-Anweisung sorgt dafür, dass die switch-Anweisung verlassen wird (Sprung zu Zeile 35). Wird break nicht eingesetzt, wird die nächste Möglichkeit (nächstes case) abgefragt. Da die Mehrfachalternative üblicherweise im Sinne eines entweder oder eingesetzt wird, ist dies nicht sinnvoll und beansprucht unnötige Rechenzeit.
  5. Nach case folgt der konstante Ausdruck (keine Variable), der den entsprechende Fall spezifiziert. Dieser konstante Ausdruck muss von ganzzahligem Datentyp sein. Bei der Abarbeitung der Mehrfachalternative wird dieser konstante Ausdruck mit dem Kontroll-Ausdruck (nach switch) verglichen. (Ein Gegenbeispiel, das eine Variable anstelle eines konstanten Ausdrucks einsetzt, folgt unten.)
  6. Nach case <Ausdruck> folgt ein Doppelpunkt und dann die entsprechende Anweisung, die in diesem Fall ausgeführt werden soll. Es kann auch sofort das nächste case folgen (siehe Zeile 8), wodurch man die beiden Fälle mit dem logischen ODER vereinigt hat.
  7. Sollen bei einer Bedingung mehrere Anweisungen ausgeführt werden, ist es nicht nötig sie in {} einzuschließen; abgearbeitet werden alle Anweisungen bis zum nächsten case.
  8. Die default-Anweisung sollte immer eingesetzt werden: damit hat man die Möglichkeit, unerwartete oder unsinnige Fälle abzufangen.

Das folgende Beispiel zeigt, dass man die switch-Anweisung nicht mit Gleitkommazahlen realisieren kann (Zeile 3 erzeugt den ersten Compiler-Fehler, obwohl die möglichen Fälle mit ganzen Zahlen definiert werden):

float x {17};

switch ( x )        // "error: quantity is not an integer"
case 17:
    cout << x << endl;
case 0:
    cout << "0" << endl;
default:
    cout << "weder 17 noch 0" << endl;

Später wird noch gezeigt, dass neben ganzzahligen Ausdrücken in switch auch Enumerationen erlaubt sind, um Mehrfachalternativen zu bilden.

Oben wurde gesagt, dass die möglichen Bedingungen der Mehrfachalternative mit Hilfe von konstanten Ausdrücken gebildet werden müssen. Dazu das Gegenbeispiel, das zu einem Compiler-Fehler führt (Zeile 6):

short i {17};
short k {17};

switch (i)
{
case k:         // "error: the value of 'k' is not usable in a constant expression"
                // obwohl i == k
    cout << "i = k" << endl;
case 0:
    cout << "0" << endl;
default:
    cout << "weder 17 noch 0" << endl;
}

Aufgaben

1. Sortieren von Zahlen:

Der Nutzer soll drei Zahlen zwischen 0 und 1000 eingeben.

Falls eine Zahl (oder mehrere Zahlen) dies nicht erfüllt, sollen die falschen Zahlen ausgegeben werden; der Nutzer wird aufgefordert, die entsprechenden Zahlen erneut einzugeben.

Falls jetzt die drei Zahlen immer noch nicht zwischen 0 und 1000 liegen, bricht das Programm ab.

Andernfalls sollen die drei Zahlen der Größe nach sortiert ausgegeben werden.

2. Näherungsweise Berechnung der Sinus- und Kosinusfunktion:

Der Nutzer soll gefragt werden, ob er den Winkel im Bogen- oder im Winkelmaß eingeben möchte. Anschließend soll er den Winkel eingeben.

Das Programm soll Sinus- und Kosinusfunktion des Winkels durch folgende Näherung berechnen (die Berechnungen werden im Bogenmaß durchgeführt):

sin x ≈ x - x3 / 3 + x5 / 5

cos x ≈ 1 - x2 / 2 + x4 / 4

Ausgegeben werden sollen die Näherungen für sin x und cos x sowie die exakten Werte (berechnet mit den Funktionen aus der Standard-Bibliothek).

3. Zwei Gleichungen mit zwei Unbekannten:

Gegeben ist ein Gleichungssystem der Form:

a1x + b1y = c1

a2x + b2y = c2

mit gegebenen Koeffizienten a1, a2, b1, b2, c1, c2 und gesuchten x, y.

Schreiben Sie ein C++-Programm, dem die Koeffizienten eingegeben werden können und das anschließend die Berechnungen durchführt und die Ergebnisse ausgibt.