Kontrollstrukturen
Video zum Kapitel

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");
}
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 einelse
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 (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.
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
switch (VARIABLE) {
case WERT1:
ANWEISUNGEN;
break;
case WERT2:
ANWEISUNGEN;
break;
...
default:
ANWEISUNGEN;
}
Besonderheiten
VARIABLE
: muss eine Ganzzahl (int
) seinWERT
: ein möglicher Wert der Variable als Literal oder Konstantedefault
: 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.
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:
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
undcase
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
if (a < 0) {
b = -a;
}
else {
b = a;
}
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"));