Resource icon

Die Programmiersprache C - Switches und Loops

Im letzten Teil bin ich auf die verschiedenen Arten von if, else if, und else eingegangen.

Wer schon ein bisschen damit herumgespielt hat, wird sich in manchen Beispielen über die Verbosität (viel zu schreiben) geärgert haben.
Manchmal möchte man einfach nur eine einfache Unterscheidung machen, oder, falls man ein einfaches Übersetzen haben will, direkt Zeichen ersetzen.
Nehmen wir an, wir seien ganz die kuhlen Kids und wollen einen Leetspeak-Automatisierer bauen.
Dann wäre eine erste Annäherung mit if-Verzweigungen folgendes:
C:
if(zeichen == 'e' || zeichen == 'E') zeichen = '3';
if(zeichen == 'o' || zeichen == 'O') zeichen = '0';
if(zeichen == 'i' || zeichen == 'I' || zeichen == 't' || zeichen == 'T') zeichen = '1';
(Ob das jetzt der komplette bzw. korrekte Leetspeak ist oder nicht, weiss ich auch nicht. Ich bin offenbar zu unkuhl dafür.)
Nun stellt man aber fest, dass diese Vergleiche sich gegenseitig ausschliessen (mutual exclusion): Falls das zeichen ein 'e' ist, dann kann es kein 'o' sein. Also nähern wir an und nutzen else-if.
C:
if(zeichen == 'e' || zeichen == 'E') zeichen = '3';
else if(zeichen == 'o' || zeichen == 'O') zeichen = '0';
else if(zeichen == 'i' || zeichen == 'I' || zeichen == 't' || zeichen == 'T') zeichen = '1';
Semantisch ist dieser Code wohl korrekt. Aber man sieht die Redundanzen: Das Wort "zeichen" kommt sehr oft vor. Zudem haben wir viele logische Oder, die alles ein bisschen unübersichtlich machen.

Die Struktur, die uns hier vieles erleichtert, ist "switch" (Schalter, Weiche).
C:
switch(zeichen)
{
case 'e':
case 'E':
    zeichen = '3';
    break;
case 'o':
case 'O':
    zeichen = '0';
    break;
case 'i':
case 'I':
case 't':
case 'T':
    zeichen = '1';
    break;
default:
}
Sieht auf den ersten Blick sehr verwirrend aus, oder?
Tut es tatsächlich. Allerdings ist switch sehr viel logischer als if-Äquivalente.
Wir sagen zuerst, welche Variable wir betrachten:
C:
switch(zeichen)
Hierbei ist wichtig zu beachten, dass das ein Integraler Typ sein muss, Strings kann man hier nicht einsetzen. Oder einfacher gesagt: Es muss eine Zahl sein oder als eine dargestellt werden können (z.B. ist char auch erlaubt, da char ein int-Typ ist).

Dann öffnen wir den switch-body mit '{'.

Danach gibt es drei spezielle Schlüsselwörter (keywords), die man in den meisten Fällen braucht:
"case" definiert die Stelle, an die das Programm springt, falls der Wert der switch-Variable gleich der case-Angabe ist.
Bsp.: Falls (zeichen == 'o') hier true ist, dann springt das Programm zum Label "case 'o':".

"default" ist das else-Äquivalent: Ist kein case erfüllt, dann springt das Programm dort hin.

"break" (hier: "breche aus") ist ein vereinfachtes "goto" und lässt das Programm direkt hinter das switch-Statement springen.
Falls Fälle auftreten, die sich ausschliessen, muss ein break benutzt werden (ähnlich wie das erklärte goto in der if-Einführung).

Und das war es eigentlich auch schon. Ein bisschen Übung gehört natürlich auch dazu, hier ein paar Hinweise:

1) Was in if ver-oder-t werden muss, kann hier mit aufeinanderfolgenden case-Statements erreicht werden.
2) Was in if ver-und-et werden muss, kann hier nicht ausgedrückt werden.
Es ist aber möglich, in einem Case selbst eine if-Verzweigung einzubauen. Das wird aber sehr schnell unübersichtlich.
3) In 90% der Fälle wird man ein break; nach jedem Case haben wollen. In diesem Beispiel ist das nicht der Fall, aber meistens will man nicht, dass der Code durch alle Fälle durchläuft.
4) Anders als beim if-else if ist die Reihenfolge immer wichtig, wenn keine breaks verwendet werden.

Man kann sich das Programm wie eine weisse Wand vorstellen, der man oben Farbe überkippt. Die Farbe läuft runter bis zum switch(Variable). Der korrekte Case wird gefunden und dahin gesprungen, die Strecke zwischen switch() und dem entsprechenden Case bleibt aber weiss, erst nach dem case färbt sie weiter. Und erst beim nächsten break springt die Farbe hinter das switch()-Konstrukt.
Alles, was farbig wurde, wurde ausgeführt, der Rest nicht.


Loops

Nun hatten wir konditionale Ausführung (conditional execution) mit if und switch schon behandelt.
Manchmal möchte man etwas aber auch mehrmals machen, und zwar nicht einfach fix 2x, sondern variablenabhängig viele Male.
Ein Beispiel: Multiplikation, mit Addition nachgestellt.
(Dieses Beispiel macht rein von der Performance her keinen Sinn, für normale Programme also bitte ganz gewohnt '*' verwenden).
C:
int multiply(unsigned int fac1, int fac2)
{
    //Was soll hier hin?
}
Wir sollten also den Faktor 2 fac1 mal addieren.
In C gibt es - anders als in einigen anderen Sprachen - kein "wiederhole n mal". Stattdessen ist die Logik: "Wiederhole, solange wie".
Wir können mit unserem Wissen diese Art von Schleife schon bauen:
C:
int n = 10;
redo:
if(n != 0)
{
    n = n - 1;
    goto redo;
}
n ist zuerst 10. Dann wird überprüft, ob es nicht 0 ist (ist es, da 10 != 0), worauf hin n um 1 verkleinert wird und das Programm zum Vergleich zurückspringt.
Irgendwann ist n == 0, das Programm tritt nicht in das if ein, und geht weiter.
Wir können damit also unsere Multiplikationsfunktion schreiben:
C:
int multiply(unsigned int fac1, int fac2)
{
    int ret = 0;
redo:
    if(fac1 != 0)
    {
        ret = ret + fac2;
        goto redo;
    }

    return ret;
}
Und wir sind fertig. Nun sieht man aber schon: So viele Zeilen für diese einfache Aufgabe? Und dann noch ein Label, das immer eindeutig sein muss? Geht das nicht besser?
Doch, sicher geht das: Wir haben while(). Das ist exakt dasselbe, sieht aber schöner aus und ist sicherer, weil weniger Fehler passieren können (einfach, weil es weniger zu schreiben gibt):
C:
int multiply(unsigned int fac1, int fac2)
{
    int ret = 0;
    while(fac1 != 0)
    {
        ret = ret + fac2;
    }

    return ret;
}
Nun wollen wir den Nutzer fragen, mit welchen positiven Werten er einen Array füllen will.

C:
int data[64]
int i = 0;
while(i < 64)
{
    int antwort = getUserInput(); //diese funktion gibt es nicht
    data[i] = antwort;
    i++;
}
Eine Eingabe von 0 soll das Programm abbrechen, eine negative Zahl ist ungültig und soll eine erneute Eingabe erfordern.
C:
int data[64]
int i = 0;
retry:
while(i < 64)
{
    int antwort = getUserInput(); //diese funktion gibt es nicht
    if(antwort == 0) break; //Schon bekannt
    if(antwort < 0) goto retry;
    data[i] = antwort;
    i++;
}
Nun ist das wieder aufwändig und wir haben deshalb "continue" exakt dafür.
C:
int data[64]
int i = 0;
while(i < 64)
{
    int antwort = getUserInput(); //diese funktion gibt es nicht
    if(antwort == 0) break; //Schon bekannt
    if(antwort < 0) continue;
    data[i] = antwort;
    i++;
}
Ok, das war's dann eigentlich auch schon. Nun gibt es gewisse Variationen davon.
do...while: Dasselbe wie while, aber der Loop-Body wird mindestens 1x ausgeführt.
C:
do
{
    Statement
}
while(Expression);
ist somit dasselbe wie:
C:
redo:
Statement
if(Expression) goto redo;
Nochmals zum Vergleich, was while tut:

C:
redo:
if(Expression)
{
    Statement
    goto redo;
}
Und dann, meist viel öfter gebraucht als while und do-while zusammen:
for.
For heisst bekanntermassen "für", was im Kontext der Schleife wenig Sinn macht. Allerdings ist for in erster Linie für Arrays und ähnliches gedacht: Dort heisst es nämlich: "Für die Element-Indizes i, wobei der Index diese Eigenschaft erfüllt und der nächste Index so aussieht, tue den Loop-Body".

Ein Beispiel: Summe der Werte in einem Array mit while:
C:
int data[64];
int i = 0;
int sum = 0;
while(i < 64)
{
    sum += data[i];
    i++;
}
Und nun dasselbe im for:
C:
int data[64];
int i = 0;
int sum = 0;
for(i = 0; i < 64; i++) //bei moderneren Compilern (C99 und höher) kann man direkt int i = 0 schreiben.
{
    sum += data[i];
}
Bei for gilt:
for(A;B;C) D ist dasselbe wie:
C:
A
while(B)
{
    D;
    C;
}
Mit einem grossen Unterschied:
continue springt nun nicht direkt zurück, sondern führt erst noch C aus.
C:
int i = 0;
while(i != 1)
{
    continue;
    i++;
}
beendet somit nie (i++ wird nie erreicht, daher wird i nie == 1 sein), aber
C:
for(int i = 0; i != 1; i++)
{
retry:
    continue; //Hier dasselbe wie i++; goto retry;
}
beendet nach genau einer Iteration. Das macht meist mehr Sinn, denn da for für mehrere Elemente gedacht ist, macht es keinen Sinn, dasselbe Objekt mehrmals zu betrachten, ist aber dennoch ein wichtiger Unterschied.


Einige caveat:
1) Was in einem Loop deklariert wird, ist nur in einer Loopiteration (und nicht während des gesamten Loops) gültig, Summen müssen also ausserhalb deklariert werden.
2) In diesen Beispielen wurde int verwendet. Für 99% der Fälle sollte man heutzutage size_t verwenden, int wurde hier nur als akzeptable Vereinfachung benutzt.
3) continues stehen implizit immer am Ende einer Schleife, ein continue dort ist also überflüssig.
4) breaks brechen immer nur aus dem innersten Loop aus, man kann also nicht gleichzeitig aus mehreren Loops ausbrechen (aber ganz aus der Funktion mittels return).
5) Obwohl es möglich ist, sollte die Iterationsvariable im For-Loop nicht verändert werden. Später, für parallele Loops, ist das sogar notwendig.
Vor allem aber wird es schwierig festzustellen, wie lange das Programm tatsächlich läuft (Früher abbrechen ist aber immer erlaubt und eine gute Idee, da es sehr viel CPU-Zeit sparen kann).

Und damit schliesst auch dieses Kapitel. Wie immer sind Verbesserungsvorschläge erwünscht.
Autor
cwriter
First release
Last update
Bewertung
0,00 Stern(e) 0 Bewertungen