In diesem zweiten Teil werde ich weiter in die Logikoperationen und die Kontrollstrukturen eingehen.
Im ersten Teil wurde bereits gezeigt, dass Programme eine Sequenz beliebig vieler Anweisungen ist.
Nun ist es aber in den meisten Fällen so, dass man nicht immer alles gleich haben will, sondern unter bestimmten Umständen Ausnahmen machen will.
Der Klassiker ist noch immer die Division:
Das funktioniert in den meisten Fällen. Doch wenn ich b auf 0 setze, dann stürzt das Programm ab, da keine gesicherte Ausführung mehr möglich ist. Denn es ist offensichtlich, dass der Programmierer nicht durch 0 teilen lassen wollte; er könnte das Resultat ja gar nicht abschätzen, da es undefiniert ist.
Nun funktionieren CPUs ähnlich wie ein C-Programm: Sie gehen das Programm zeilenweise von oben nach unten durch (jede C Zeile kann mehrere Instruktionen beinhalten; bei Interesse möge man sich über Assembly informieren). Also wie bricht man aus diesem Fluss aus?
Nun, als Ansatz könnte man nehmen, die Instruktionen, die man nicht will, einfach zu überspringen.
Und genau so funktioniert es auch: Wir können in einem gewissen Rahmen über Instruktionen springen, sie also nicht ausführen:
Hierbei sei erklärt: goto (zu deutsch "gehe zu") ist eines der wenigen Schlüsselwörter in C; d.h. als Programmierer darf man seine Variablen nicht "goto" nennen. Als einzigen Parameter nimmt goto ein Label (~"Etikette"), welches mit dem Namen gefolgt von einem Doppelpunkt (':') angegeben wird. Der Effekt ist schnell beschrieben: Sobald der Prozessor diese Zeile erreicht, springt er zum angegeben Label und macht dort weiter. Der Code oben ist daher semantisch äquivalent zu
Wichtig zu beachten: Ein Label ist keine Instruktion. Es hat also keine Grösse und wird nie ausgeführt. Es ist lediglich eine Angabe, zu welcher Instruktion gesprungen werden soll. In diesem Fall wäre es die Instruktion, die direkt auf den Codeabschnitt oben folgt.
Durch diese semantische Äquivalenz scheint es sinnlos, einfach so zu springen, denn man kann den Code ja einfach weglassen.
Aber was, wenn man eigentlich dividieren, aber den Fall b = 0 abfangen will?
Dazu gibt es if (deutsch "Falls"). if erwartet einen bool'schen Parameter. Evaluiert dieser zu True (Wahr), werden die assoziierten Codeabschnitte ausgeführt. Ist es False (Falsch), werden sie übersprungen.
Was ist nun ein bool'scher Ausdruck (engl. "boolean expression")?
Man wollte "Wahr" und "Falsch" mit Computern ausdrücken können. Als einfaches Beispiel wäre es praktisch, den Computer zu fragen, ob man richtig gerechnet hat, also z.B. "Ich habe 1+1 gerechnet und 3 erhalten. Stimmt das?"
Aber wie kann man das Überprüfen?
Dazu muss ein bisschen in die Hardware vorgedrungen werden. Aber keine Angst, es ist es wert.
Prozessoren bestehen aus den Logikgates. Ein Bit ist 0 oder 1 (Falsch oder Wahr).
Die Logik hat folgende Primitiven:
AND: Verundung (beide Werte müssen 1 sein um eine 1 als Resultat zu bekommen):
OR: Veroderung (mindestens einer der beiden Werte muss 1 sein um eine 1 als Resultat zu bekommen):
XOR: eXklusive Veroderung (Mindestens und Maximal eine Eingabe muss 1 sein, um eine 1 als Resultat zu bekommen):
Mit diesem Wissen könnte man ja mit jeder Zahl vergleichen, oder?
Als Beispiel hatten wir "1+1 ? 3".
3 im Binärsystem ist 11b.
Der Computer rechnet "1+1" selbst und kommt auf 2d, binär also 10b.
Wir überprüfen mithilfe der Logik und der XOR-Operation:
11b XOR 10b = (1 XOR 1), (1 XOR 0) = (0), (1) = 01b.
Wir hätten aber erwartet, dass das Resultat 00b ist, also die Rechnung "1+1=3" nicht.
Funktioniert also super. Heutige Prozessoren verodern immer die Resultatbits miteinander, hier wäre es 0 OR 1 = 1. Diese 1 sagt uns: Das Resultat der letzten Operation war nicht 0.
Es scheint offensichtlich: Computer können sehr schnell bestimmen, ob ein Resultat 0 war. Geht das auch mit anderen Zahlen?
Theoretisch ja. Z.B. könnte man auch auf b10 prüfen, indem man alle erwarteten 1-Stellen verundet und alle 0-Stellen verodert. Warum ist das so?
Bei der Oder-Operation dominiert die 1: Ich kann Fantastilliaren an Nullen verodern und habe noch immer 0 als Resultat. Aber eine einzige 1 dreht das Resultat zu 1. Bei UND ist es reziprok (umgekehrt) analog.
Das Problem hierbei: Das ist eine sehr teure Operation und braucht viele Transistoren, also Knete. Daher prüfen heutzutage die Meisten Prozessoren nur auf 0, u.a. da dies mit Abstand die häufigste Zahl in Computern ist.
Auf Bitebene ist es klar: 0 ist Falsch, 1 ist Wahr. Aber heutige Prozessoren rechnen nicht in Bits, sondern in Mehrbytewerten. Was ist denn da Wahr und was ist Falsch?
Falsch ist noch immer 0. Wahr ist alles andere. Warum?
0 zu prüfen ist wie gesagt einfach. Zu prüfen, ob etwas nicht 0 ist, ebenfalls. Natürlich hätte man auch die Hälfte des int-Bereichs für False und die andere für True nehmen können. Aber man wollte Bool möglichst einfach gestalten.
Aber genug Theorie:
Wenn 3 ^ (1 + 1) nicht 0 ist (also 3 ungleich 2), dann überspringe die Ausgabe, dass die Rechnung korrekt war. "goto skip" ist hier die Anweisung, die zum If assoziiert. Nun wäre es aber sehr doof wenn man das immer so schreiben müsste (bei 100 if-Verzweigungen bräuchte man 100 skip Label).
Daher kann man den Ausdruck auch logisch umkehren und den konditionellen Code als Assoziierten hinzufügen (man kann mehrere Statements zu einem zusammenpacken, indem man "{}" benutzt.
Wichtig: Es sieht zwar anders aus, intern ist der Code aber derselbe. Der Vorteil dieser Schreibweise ist, dass der Code leserlicher wird. Bitte keine gotos in solchen Fällen verwenden.
würde das angesprochene Beispiel in Code ausdrücken.
if() erwartet einen Bool'schen Ausdruck. Wir wissen, dass 2 nicht gleich 3 ist, XOR also Nicht-Null ausgibt, wenn die Operanden nicht gleich sind. Wir wollen aber die Bestätigung nur dann ausgeben, wenn die Operanden gleich sein. Also müssen wir mit ! (NOT) das Resultat von (3 ^ (1+1)) = (0b10), also non-null, zu Null, also False, drehen.
Was wäre nun, wenn man eine Ordnungsrelation (z.B. > oder <) testen will?
XOR versagt hier. Was tun?
Man nimmt schlicht die Subtraktion. Mithilfe der Subtraktion kann man auch Gleichheit prüfen, da a - a = 0.
Die Subtraktion in Hardware ist recht kompliziert (siehe two's-complement), daher beschränke ich mich auf das Modell:
Der Computer prüft beim Resultat, ob es negativ und ob es 0 ist. Daraus lassen sich die folgenden Möglichkeiten ableiten:
Negativ und nicht 0: < 0
Nicht negativ und 0: 0
Nicht negativ und nicht 0: > 0
Nun könnte man monieren: "Aber da fehlt doch >= (grösser/gleich) und <= (kleiner/gleich).
Nein, tut es nicht. Denn was kleiner ist, ist nicht grösser/gleich.
Was grösser ist, ist nicht kleiner/gleich. Wodurch diese beiden Relationen einfach durch ein zusätzliches NOT erreicht sind.
Wir halten also fest:
ist dasselbe wie:
Und schliesslich ist es keine gute Idee, ^ statt == zu schreiben. == überprüft schlicht Gleichheit. Der Programmierer sollte Lesbarkeit über Pseudo-Optimierung stellen. Ist XOR schneller als -? Sicher. Ist es in Prozessoren schneller? Selten (die Langsamen Instruktionen geben die Geschwindigkeit vor, schnellere warten halt ein bisschen). Und selbst wenn: Diese Art von Optimierung sollte man als allerletztes machen (oder sie dem Compiler überlassen).
Warum trete ich das so breit: Es ist wichtig, diese Tricks zu kennen. Zum Beispiel sieht man oft folgenden Code:
Also was macht es?
Es verundet eine Variable "flags" mit (1 << 13), was wiederum ein Wert ist, wo ausschliesslich das 14. Bit gesetzt ist (wir schieben die 1 um 13 Stellen nach links, also von:
0b0000000000001
Zu:
0b1000000000000
Dann verunden wir das mit der Variable "flags".
Was ist das Resultat?
Der If-Block wird genau dann ausgeführt, wenn flags eine 1 an der 14. Stelle hatte. Warum? Nun, der 2. Operand (1 << 13) hat überall Nullen, ausser an der 14. Stelle. Bitwise AND sagt nun: Hat irgendeiner der beiden Operanden an einer Stelle eine 0, ist diese Stelle auch im Resultat 0. Der einzige Weg also, einen Non-Null-Wert zu errreichen, ist, wenn flags and der 14. Stelle auch eine 1 hat. D.h. im Klartext: Der If-Block wird nur ausgeführt, wenn flags an der 14. Stelle eine 1 hat.
Diese Technik wird oft für Bitfields und Bitflags verwendet. Bei Interesse gerne danach Googlen.
Puh. Aber nun möchten wir das Umsetzen:
Was ist hier falsch?
Die Division wird immer ausgeführt. Der Unkonditionale Code (also der, der immer ausgeführt wird) ist:
Wir haben also nichts gewonnen. Wir müssten a / b nur dann rechnen, wenn b != 0 ist. Also nächster Versuch:
Geht das? Ja. Aber b == 0 und b != 0 sind Komplemente, und dafür haben wir eine schönere Schreibweise: else.
Und wir sind fertig.
Ein Klassiker ist diese Aufgabe: "Schreibe ein Programm, dass dir Komplimente Abhängig von deinem Reichtum macht."
Wir beginnen also:
Das Problem hier: Wenn jemand mehr als 100 (Irgendwas) hat, dann wird nicht nur "Wow, hast du Kohle!", sondern "Aha. Ja, nicht schlecht. Wow, hast du Kohle!" ausgegeben. Der Grund ist einfach: > 100 impliziert auch > 50. Und dadurch auch > 10.
Ok, dann bauen wir um:
Allerdings gilt auch hier wieder: Mehr als 100 impliziert mehr als 50 usw. Aber wir haben ja goto:
Und dieses Problem wäre gelöst. Aber auch hier: Wir wollen goto nicht verwenden. Also nutzen wir else if ("Ansonsten, falls..."):
Auch hier: Der Effekt ist derselbe. Aber Achtung: Die unwahrscheinlichere Bedingung muss immer über der wahrscheinlicheren stehen. Z.B.:
Geht auch nicht, da geld > 10 die anderen Bedingungen auch schon abdeckt.
Auch beim else-if kann ein else angehängt werden, wo alle anderen Fälle betrachtet werden.
Dieser Abschnitt war schon wieder ein bisschen länger als erwartet und ich werde daher Loops in einem nächsten Teil behandeln.
Wie immer: Konstruktive Kritik sehr erwünscht. Es ist mir bewusst, dass dieser Teil schon sehr in die Details einging (die eher mit Digitaltechnik als mit C zu tun haben), aber ich denke, dass man diese Dinge schon früh lernen sollte.
Im ersten Teil wurde bereits gezeigt, dass Programme eine Sequenz beliebig vieler Anweisungen ist.
Nun ist es aber in den meisten Fällen so, dass man nicht immer alles gleich haben will, sondern unter bestimmten Umständen Ausnahmen machen will.
Der Klassiker ist noch immer die Division:
C:
int divide(int a, int b)
{
return a / b;
}
Nun funktionieren CPUs ähnlich wie ein C-Programm: Sie gehen das Programm zeilenweise von oben nach unten durch (jede C Zeile kann mehrere Instruktionen beinhalten; bei Interesse möge man sich über Assembly informieren). Also wie bricht man aus diesem Fluss aus?
Nun, als Ansatz könnte man nehmen, die Instruktionen, die man nicht will, einfach zu überspringen.
Und genau so funktioniert es auch: Wir können in einem gewissen Rahmen über Instruktionen springen, sie also nicht ausführen:
C:
int a = 42;
int b = 0;
goto skip; //b ist 0, wir dürfen nicht dividieren
int c = a / b;
skip:
Code:
int a = 42;
int b = 0;
//goto skip;
//int c = a / b;
//skip:
Durch diese semantische Äquivalenz scheint es sinnlos, einfach so zu springen, denn man kann den Code ja einfach weglassen.
Aber was, wenn man eigentlich dividieren, aber den Fall b = 0 abfangen will?
Dazu gibt es if (deutsch "Falls"). if erwartet einen bool'schen Parameter. Evaluiert dieser zu True (Wahr), werden die assoziierten Codeabschnitte ausgeführt. Ist es False (Falsch), werden sie übersprungen.
Was ist nun ein bool'scher Ausdruck (engl. "boolean expression")?
Man wollte "Wahr" und "Falsch" mit Computern ausdrücken können. Als einfaches Beispiel wäre es praktisch, den Computer zu fragen, ob man richtig gerechnet hat, also z.B. "Ich habe 1+1 gerechnet und 3 erhalten. Stimmt das?"
Aber wie kann man das Überprüfen?
Dazu muss ein bisschen in die Hardware vorgedrungen werden. Aber keine Angst, es ist es wert.
Prozessoren bestehen aus den Logikgates. Ein Bit ist 0 oder 1 (Falsch oder Wahr).
Die Logik hat folgende Primitiven:
AND: Verundung (beide Werte müssen 1 sein um eine 1 als Resultat zu bekommen):
Code:
0 AND 0 = 0
0 AND 1 = 0
1 AND 0 = 0
1 AND 1 = 1
OR: Veroderung (mindestens einer der beiden Werte muss 1 sein um eine 1 als Resultat zu bekommen):
Code:
0 OR 0 = 0
0 OR 1 = 1
1 OR 0 = 1
1 OR 1 = 1
XOR: eXklusive Veroderung (Mindestens und Maximal eine Eingabe muss 1 sein, um eine 1 als Resultat zu bekommen):
Code:
0 XOR 0 = 0
0 XOR 1 = 1
1 XOR 0 = 1
1 XOR 1 = 0
Mit diesem Wissen könnte man ja mit jeder Zahl vergleichen, oder?
Als Beispiel hatten wir "1+1 ? 3".
3 im Binärsystem ist 11b.
Der Computer rechnet "1+1" selbst und kommt auf 2d, binär also 10b.
Wir überprüfen mithilfe der Logik und der XOR-Operation:
11b XOR 10b = (1 XOR 1), (1 XOR 0) = (0), (1) = 01b.
Wir hätten aber erwartet, dass das Resultat 00b ist, also die Rechnung "1+1=3" nicht.
Funktioniert also super. Heutige Prozessoren verodern immer die Resultatbits miteinander, hier wäre es 0 OR 1 = 1. Diese 1 sagt uns: Das Resultat der letzten Operation war nicht 0.
Es scheint offensichtlich: Computer können sehr schnell bestimmen, ob ein Resultat 0 war. Geht das auch mit anderen Zahlen?
Theoretisch ja. Z.B. könnte man auch auf b10 prüfen, indem man alle erwarteten 1-Stellen verundet und alle 0-Stellen verodert. Warum ist das so?
Bei der Oder-Operation dominiert die 1: Ich kann Fantastilliaren an Nullen verodern und habe noch immer 0 als Resultat. Aber eine einzige 1 dreht das Resultat zu 1. Bei UND ist es reziprok (umgekehrt) analog.
Das Problem hierbei: Das ist eine sehr teure Operation und braucht viele Transistoren, also Knete. Daher prüfen heutzutage die Meisten Prozessoren nur auf 0, u.a. da dies mit Abstand die häufigste Zahl in Computern ist.
Auf Bitebene ist es klar: 0 ist Falsch, 1 ist Wahr. Aber heutige Prozessoren rechnen nicht in Bits, sondern in Mehrbytewerten. Was ist denn da Wahr und was ist Falsch?
Falsch ist noch immer 0. Wahr ist alles andere. Warum?
0 zu prüfen ist wie gesagt einfach. Zu prüfen, ob etwas nicht 0 ist, ebenfalls. Natürlich hätte man auch die Hälfte des int-Bereichs für False und die andere für True nehmen können. Aber man wollte Bool möglichst einfach gestalten.
Aber genug Theorie:
C:
if((3 ^ (1+1))) goto skip: //^ ist XOR (und nicht "hoch", wie man vielleicht denken könnte)
printf("Rechnung war korrekt\n");
skip:
Daher kann man den Ausdruck auch logisch umkehren und den konditionellen Code als Assoziierten hinzufügen (man kann mehrere Statements zu einem zusammenpacken, indem man "{}" benutzt.
Wichtig: Es sieht zwar anders aus, intern ist der Code aber derselbe. Der Vorteil dieser Schreibweise ist, dass der Code leserlicher wird. Bitte keine gotos in solchen Fällen verwenden.
C:
if(!(3 ^ (1+1))) //^ ist XOR (und nicht "hoch", wie man vielleicht denken könnte)
{
printf("Rechnung war korrekt\n");
}
if() erwartet einen Bool'schen Ausdruck. Wir wissen, dass 2 nicht gleich 3 ist, XOR also Nicht-Null ausgibt, wenn die Operanden nicht gleich sind. Wir wollen aber die Bestätigung nur dann ausgeben, wenn die Operanden gleich sein. Also müssen wir mit ! (NOT) das Resultat von (3 ^ (1+1)) = (0b10), also non-null, zu Null, also False, drehen.
Was wäre nun, wenn man eine Ordnungsrelation (z.B. > oder <) testen will?
XOR versagt hier. Was tun?
Man nimmt schlicht die Subtraktion. Mithilfe der Subtraktion kann man auch Gleichheit prüfen, da a - a = 0.
Die Subtraktion in Hardware ist recht kompliziert (siehe two's-complement), daher beschränke ich mich auf das Modell:
Der Computer prüft beim Resultat, ob es negativ und ob es 0 ist. Daraus lassen sich die folgenden Möglichkeiten ableiten:
Negativ und nicht 0: < 0
Nicht negativ und 0: 0
Nicht negativ und nicht 0: > 0
Nun könnte man monieren: "Aber da fehlt doch >= (grösser/gleich) und <= (kleiner/gleich).
Nein, tut es nicht. Denn was kleiner ist, ist nicht grösser/gleich.
Was grösser ist, ist nicht kleiner/gleich. Wodurch diese beiden Relationen einfach durch ein zusätzliches NOT erreicht sind.
Wir halten also fest:
C:
if(2 < 3)
{
}
C:
if(2 - 3 < 0)
{
}
Und schliesslich ist es keine gute Idee, ^ statt == zu schreiben. == überprüft schlicht Gleichheit. Der Programmierer sollte Lesbarkeit über Pseudo-Optimierung stellen. Ist XOR schneller als -? Sicher. Ist es in Prozessoren schneller? Selten (die Langsamen Instruktionen geben die Geschwindigkeit vor, schnellere warten halt ein bisschen). Und selbst wenn: Diese Art von Optimierung sollte man als allerletztes machen (oder sie dem Compiler überlassen).
Warum trete ich das so breit: Es ist wichtig, diese Tricks zu kennen. Zum Beispiel sieht man oft folgenden Code:
C:
if(flags & (1 << 13))
{
//....
}
Es verundet eine Variable "flags" mit (1 << 13), was wiederum ein Wert ist, wo ausschliesslich das 14. Bit gesetzt ist (wir schieben die 1 um 13 Stellen nach links, also von:
0b0000000000001
Zu:
0b1000000000000
Dann verunden wir das mit der Variable "flags".
Was ist das Resultat?
Der If-Block wird genau dann ausgeführt, wenn flags eine 1 an der 14. Stelle hatte. Warum? Nun, der 2. Operand (1 << 13) hat überall Nullen, ausser an der 14. Stelle. Bitwise AND sagt nun: Hat irgendeiner der beiden Operanden an einer Stelle eine 0, ist diese Stelle auch im Resultat 0. Der einzige Weg also, einen Non-Null-Wert zu errreichen, ist, wenn flags and der 14. Stelle auch eine 1 hat. D.h. im Klartext: Der If-Block wird nur ausgeführt, wenn flags an der 14. Stelle eine 1 hat.
Diese Technik wird oft für Bitfields und Bitflags verwendet. Bei Interesse gerne danach Googlen.
Puh. Aber nun möchten wir das Umsetzen:
C:
int divide(int a, int b)
{
int ret;
if(b == 0) ret = 0;
ret = a / b;
return ret;
}
Die Division wird immer ausgeführt. Der Unkonditionale Code (also der, der immer ausgeführt wird) ist:
C:
int divide(int a, int b)
{
int ret;
ret = a / b;
return ret;
}
C:
int divide(int a, int b)
{
int ret;
if(b == 0) ret = 0;
if(b != 0) ret = a / b;
return ret;
}
C:
int divide(int a, int b)
{
int ret;
if(b == 0) ret = 0;
else ret = a / b;
return ret;
}
Ein Klassiker ist diese Aufgabe: "Schreibe ein Programm, dass dir Komplimente Abhängig von deinem Reichtum macht."
Wir beginnen also:
C:
//int geld = ...;
if(geld > 10)
{
printf("Aha.\n");
}
if(geld > 50)
{
printf("Ja, nicht schlecht.\n");
}
if(geld > 100)
{
printf("Wow, hast du Kohle!\n");
}
Ok, dann bauen wir um:
C:
//int geld = ...;
if(geld > 100)
{
printf("Wow, hast du Kohle!\n");
}
if(geld > 50)
{
printf("Ja, nicht schlecht.\n");
}
if(geld > 10)
{
printf("Aha.\n");
}
C:
//int geld = ...;
if(geld > 100)
{
printf("Wow, hast du Kohle!\n");
goto skip;
}
if(geld > 50)
{
printf("Ja, nicht schlecht.\n");
goto skip;
}
if(geld > 10)
{
printf("Aha.\n");
goto skip;
}
skip:
C:
//int geld = ...;
if(geld > 100)
{
printf("Wow, hast du Kohle!\n");
}
else if(geld > 50)
{
printf("Ja, nicht schlecht.\n");
}
else if(geld > 10)
{
printf("Aha.\n");
}
C:
//int geld = ...;
if(geld > 10)
{
printf("Aha.\n");
}
else if(geld > 50)
{
printf("Ja, nicht schlecht.\n");
}
else if(geld > 100)
{
printf("Wow, hast du Kohle!\n");
}
Auch beim else-if kann ein else angehängt werden, wo alle anderen Fälle betrachtet werden.
Dieser Abschnitt war schon wieder ein bisschen länger als erwartet und ich werde daher Loops in einem nächsten Teil behandeln.
Wie immer: Konstruktive Kritik sehr erwünscht. Es ist mir bewusst, dass dieser Teil schon sehr in die Details einging (die eher mit Digitaltechnik als mit C zu tun haben), aber ich denke, dass man diese Dinge schon früh lernen sollte.