Kontrollstrukturen

Video zum Kapitel

Link zu YouTube

Block

Da C so viele moderne Programmiersprachen inspiriert hat, sind die Kontrollstrukturen ebenfalls sehr gängig. Wir kennen sie aus C++, Java oder C#. C bietet hier sehr wenig Überraschungen; kennt man die Kontrollstrukturen aus Java, so kennt man auch die aus C.

Ebenfalls bekannt aus vielen Sprachen ist das Konstrukt des Blocks, also einer Gruppierung von mehreren Statements zu einem gemeinsamen Ganzen.

  • Anweisungen, die zwischen zwei geschweiften Klammern { } stehen, bilden einen Block (compound statement)
  • ein Block kann dort stehen, wo auch ein einzelnes Statement stehen kann (z. B. in Kontrollstrukturen)
int i = 7;

{
  /* Dies ist ein Block */
  i++;
}

Der Block kann dort stehen, wo sonst ein einzelnes Statement stehen kann und er verhält sich dann – im Zusammenhang mit den Kontrollstrukturen – wie ein einzelnes Statement. Außerdem werden Blöcke noch benutzt, um den Rumpf von Funktionen anzugeben.

  • Blocke bildet Sichtbarkeitsbereich (scope)
  • Variablen, die in einem Block deklariert werden,
    • sichtbar nur im Block (und geschachtelten Blöcken)
    • leben nur solange, bis der Block verlassen wird
int a = 3;
{
    int b = 4;
    {
        int c = 5;
        a++; b++; c++;
    }
    a++; b++; /* c ist nicht sichtbar */
}
a++; /* c und b sind nicht sichtbar */

Im Beispiel ist die Variable a in beiden Blöcken sichtbar und lebt bis zum Ende des Beispiels. Die Variable b ist aber nur innerhalb des äußeren und inneren Blocks vorhanden. c sogar nur im innersten Block.

Die genaue Lebensdauer der Variable können wir nicht bestimmen, da wir außerhalb ihres Sichtbarkeitsbereichs keinen Test machen können, ob sie noch vorhanden ist. Es ist dem Compiler freigestellt, wann er die Variable freigibt. Üblicherweise wird er aus Effizienzgründen alle lokalen Variablen einer Funktion am Ende der Funktion freigeben, er kann es aber auch früher tun, wenn die Variable nicht mehr sichtbar ist.

Diese Eigenschaft, dass die Sichtbarkeit von Variablen auf den umgebenden Block beschränkt sind, bezeichnet man auch als Block Scope. Damit grenzen sich die C-artigen Sprachen von anderen Programmiersprachen, wie z. B. Ruby oder Python ab, bei denen die Variablen immer in der ganzen Funktion sichtbar sind.

Auswahl (if)

Die einfachste Kontrollstruktur wird durch die Auswahlanweisung (if) zur Verfügung gestellt. Ist die Bedingung wahr, wird die Anweisung nach der Bedingung ausgeführt.

if-Anweisung bietet eine einseitige Auswahl

  • Syntax: if (BEDINGUNG) { TRUE-ZWEIG }
  • wenn die logische Bedingung erfüllt ist, wird der True-Zweig ausgeführt
if (antwort == 42) {
    printf("Die Antwort auf alle Fragen");
}
Bedingung ist vom Typ int mit 0 → false

Da sich Java bei der Syntax und den Kontrollstrukturen stark von C hat beeinflussen lassen, sollte es nicht verwunden, dass die C-Kontrollstrukturen denen von Java exakt gleichen. (Nur ist es natürlich in Wirklichkeit genau andersherum.)

Ein ganz großer Unterschied zu Java ist der Typ der Bedingung: Während in Java die Bedingung vom Datentyp boolean sein muss, ist sie in C vom Datentyp int. Der Grund ist, dass es in C bis vor kurzem keinen Datentyp für Wahrheitswerte gibt, sondern diese einfach als int dargestellt werden. Hierbei gilt, dass

  • 0 → false
  • != 0 → true

Dieses Manko wurde im Standard adressiert und ein neuer Datentyp _Bool wurde in C99 eingeführt. Er heißt _Bool, weil man sich nicht getraut hat, den Datentyp bool zu nennen: Existierende Programme, die sich selbst einen bool definiert haben, hätten sonst nicht mehr compiliert.

Der C11-Standard hat über die Header-Datei stdbool.h einen „echten“ bool eingeführt. In der Datei findet man dann aber auch nur:

#define bool    _Bool
#define true    1
#define false   0

Erst seit dem C23-Standard gibt es einen echten Datentyp bool und die Schlüsselworte true und false.

Die Kontrollstrukturen fassen aber trotzdem weiterhin jeden Wert != 0 als true und 0 als false auf. Deswegen schreiben C-Programmierer völlig unverkrampft Code wie den folgenden – auch heute noch:

int i = 7;

while (i--) {
    printf("%d, ", i);
    /* Ausgabe: 6, 5, 4, 3, 2, 1, 0, */
}
  • Man kann if auch ohne Block verwenden
if (lottogewinn)
    freuen();
  • sehr gefährlich, wenn man weitere Statements hinzufügt
if (lottogewinn)
    freuen();
    job_kuendigen();

Im Beispiel wird die Funktion job_kuendigen() immer ausgeführt, da sich das if nur auf das unmittelbar nächste Statement bzw. den nächsten Block auswirkt. Da in diesem Fall kein Block verwendet wurde und die Einrückung für den Compiler unerheblich ist, wird freuen() nur aufgerufen, wenn der Lottogewinn erfolgt ist, job_kuendigen() aber immer.

Es ist deutlich defensiver, immer einen Block zu verwenden. Bis auf zwei zusätzliche Zeichen und eine Zeile mehr macht dies keine zusätzlichen Aufwände, vermeidet aber diesen typischen Fehler. Deswegen schreiben viele Coding-Standards auch vor, dass man Kontrollstrukturen immer mit Block verwendet.

if (lottogewinn) {
    freuen();
    job_kuendigen();
}

Eine zweiseitige Auswahl, also den Entweder/Oder-Fall kann man mit if / else erreichen.

if-else-Anweisung bietet eine zweiseitige Auswahl

  • Syntax: if (BEDINGUNG) { TRUE-ZWEIG } else { ELSE-ZWEIG }
  • wenn Bedingung erfüllt ist, wird der True-Zweig ausgeführt, andernfalls der Else-Zweig
if (antwort == 42) {
    printf("Die Antwort auf alle Fragen");
} else {
    printf("Keine Antwort");
}

Mehrfachverzweigung mit if-else-if

Wenn man mehrere Fälle hintereinander abprüfen möchte, bietet sich neben dem switch/ case (siehe weiter unten) eine Konstruktion mit if und else if an, man spricht dann von einer Mehrfachverzweigung.

if-else-if-Anweisung bietet eine Mehrfachverzweigung

  • Verschachtlung mehrerer if-else-Anweisungen
  • wird speziell formatiert, um besser lesbar zu sein
  • Beliebig viele else if, nur ein else
Syntax
if (BEDINGUNG1) {
    TRUE-ZWEIG1
} else if (BEDINGUNG2) {
    TRUE-ZWEIG2
} else if (BEDINGUNG3) {
    TRUE-ZWEIG3
} else {
    ELSE-ZWEIG
}

Es gibt Programmiersprachen, die für diese Art der Mehrfachverzweigung ein eigenes Schlüsselwort (z. B. elsif oder elif) spendieren. C bleibt sich hier aber treu und stützt die Funktion einfach auf das vorhandene if / else ab. Die Mehrfachverzweigung ist nämlich einfach eine Kombination aus if und else, die in besonderer Weise formatiert wird, um übersichtlich zu bleiben. Hier macht man sich zu Nutze, dass sowohl das if, als auch das else keinen Block benötigen.

Das folgende Beispiel zeigt eine Mehrfachverzweigung für die Abfrage der Temperatur.

if (temp < 10) {
    printf("Viel zu kalt");
}
else if (temp < 20) {
    printf("Kalt");
}
else if (temp > 50) {
    printf("Viel zu warm");
}
else if (temp > 30) {
    printf("Warm");
}
else {
    printf("Alles super");
}

Man könnte das Beispiel auch anders formatieren, bei exakt gleicher Funktion, die Übersichtlichkeit wäre dann aber dahin.

if-else-if unübersichtlich formatiert
if (temp < 10) {
    printf("Viel zu kalt");
}
else {
    if (temp < 20) {
        printf("Kalt");
    }
    else {
        if (temp > 50) {
            printf("Viel zu warm");
        }
        else {
            if (temp > 30) {
                printf("Warm");
            }
            else {
                printf("Alles super");
            }
        }
    }
}

Die Reihenfolge der Bedingungen ist natürlich entscheidend, da nach dem ersten Treffer die anderen Zweige nicht mehr betrachtet werden. Es wäre also falsch, die ersten beiden Bedingungen zu tauschen, weil dann sowohl bei 9 als auch bei 19 Grad immer die Ausgabe „Kalt“ gemacht würde.

Falsche Reihenfolge
if (temp < 20) {
    printf("Kalt");
}
else if (temp < 10) {
    /* Wird nie erreicht! */
    printf("Viel zu kalt");
}

Mehrfachverzweigung mit switch

In manchen Fällen möchte man eine Variable gegen eine Reihe von Werten testen und dafür eine kompaktere Form verwenden, als sie die Mehrfachauswahl mit if-else bietet. Hierfür bietet sich in C das aus anderen Sprachen ebenfalls bekannte switch / case an.

  • switch-Anweisung bietet übersichtliche Form der Mehrfachverzweigung
Syntax
    switch (VARIABLE) {
        case WERT1:
            ANWEISUNGEN;
            break;
        case WERT2:
            ANWEISUNGEN;
            break;
        ...
        default:
            ANWEISUNGEN;
    }

Besonderheiten

  • VARIABLE: muss eine Ganzzahl (int) sein
  • WERT: ein möglicher Wert der Variable als Literal oder Konstante
  • default: Zweig der ausgeführt wird, wenn kein Wert vorher gepasst hat
switch (monat) {
    case 2:  tage = 28; break;
    case 4:  tage = 30; break;
    case 6:  tage = 30; break;
    case 9:  tage = 30; break;
    case 11: tage = 30; break;
    default: tage = 31;
}

Die Variable, die im switch-Statement verwendet wird (hier monat) muss entweder zuweisungskompatibel mit dem Typ int sein (short, char, int) oder es muss sich um einen Aufzählungstyp handeln (enum). Es ist nicht möglich, im switch-Statement Variablen von anderen Typen zu verwenden, d. h. double, float etc. sind nicht verwendbar.

Die Werte in den case-Ästen müssen Konstanten vom Typ int sein. Sie brauchen keinen expliziten Block zu schreiben, d. h. anders als beim if bezieht sich der case-Ast auf alle Statements bis zum nächsten case oder break. Gleichzeitig entsteht aber kein neuer Scope. Wollen Sie in einem case eine Variable definieren und dort verwenden, müssen Sie einen Block einsetzen.

#include <stdio.h>

int main(int argc, char** argv) {
    int selection = 0;

    switch (selection) {
        case 0: {
            /* Block, damit Deklaration von k möglich wird */
            int k = 1;
            k--;
            printf("%d\n", k);
            break;
        }
        case 1:
            printf("%d\n", 1);
    }
}

Der default-Ast wird ausgeführt, wenn keine der Case-Konstanten gepasst hat.

Bei C fällt bei einem switch-Statement der Kontrollfluss durch alle case-Statements, die auf das case folgen, bei dem die Bedingung zutraf (Durchfall-Logik). Daher muss man immer explizit ein break hinter die case-Statements schreiben, wenn man dieses Verhalten nicht möchte, was fast immer der Fall ist.

Manchmal kann man das Durchfallen durch die case-Äste aber auch sinnvoll nutzen, z. B. für das bereits dargestellte Beispiel zu den Wochentagen.

Ausnutzen der Durchfall-Logik
switch (monat) {
    case 2:
        tage = 28;
        break;
    case 4:
    case 6:
    case 9:
    case 11:
        tage = 30;
        break;
    default:
        tage = 31;
}

Da das switch-Statement in C auf ganzzahlige Typen beschränkt ist, ist es nicht so vielseitig, wie in anderen Programmiersprachen. Ein Vorteil ist aber, dass der Compiler den erzeugten Maschinencode besser optimieren kann, als bei einem if-else, da er mit Sprungtabellen arbeiten kann. Wenn man dem Compiler hier helfen möchte, das switch möglichst effizient zu realisieren, sortiert man die case-Labels in aufsteigender Reihenfolge.

Fragezeichen-Operator

Eine syntaktische Beschränkung der bisher vorgestellten Kontrollstrukturen ist, dass sie keine Ausdrücke sind, d. h. sie liefern keinen Wert zurück. Es gibt Programmiersprachen, z. B. Ruby, bei denen das anders ist. In diesen Sprachen kann man z. B. folgendes schreiben:

Kontrollstruktur als Ausdruck in Ruby
bewertung = if temp < 10
                "Viel zu kalt"
            elsif temp < 20
                "Kalt"
            elsif temp > 50
                "Viel zu warm"
            else
                "Alles super"
            end

Diese Möglichkeit kennt C für die normalen Kontrollstrukturen nicht. Eine Ausnahme bildet hier der Fragezeichen-Operator, der eine Auswahl in Form eines Ausdrucks erlaubt.

  • Bedingungen mit if und case können nicht in Ausdrücken eingesetzt werden
  • für Verwendung in Ausdrücken gibt es einen speziellen Fragezeichen-Operator
  • einziger Operator in C mit drei Operanden (ternärer Operator)
  • Syntax: BEDINGUNG ? TRUE-WERT : FALSE-WERT

Beispiel: Absolutbetrag


Mit if
if (a < 0) {
    b = -a;
}
else {
    b = a;
}
Mit Fragezeichen-Operator
b = a < 0 ? -a : a;

Das Beispiel zeigt, dass es mit dem Operator einfacher ist, direkt aus der Auswahl einen Wert zu erhalten. Der Nachteil ist allerdings, dass es sehr unübersichtlich wird, wenn man mehr als zwei Fälle unterscheiden möchte, wie das folgende Beispiel zeigt:

bewertung =  (temp < 10) ? "Viel zu kalt"
          : ((temp < 20) ? "Kalt"
          : ((temp > 50) ? "Viel zu warm"
          : "Alles super"));

Copyright © 2025 Thomas Smits