Wie Computer Befehle abarbeiten, haben wir im letzten Teil der “Wie Rechner rechnen”-Serie gesehen – Grundlage hierfür waren die Instruktionen, Bitketten, welche die durchzuführende Aufgabe codieren. Da das Programmieren mit Bitketten allerdings nicht sonderlich komfortabel war, wurden die Assemblersprachen eingeführt, die mit sogenannten Mnemonics, kurzen Wörtchen, die Programmierarbeit vereinfachen. Damit konnte man schon ganz vernünftig programmieren, aber etwas umständlich war es immer noch; vor allem mit größer werdenden Programmen ging mit Assembler die Übersicht schnell verloren. Der Weisheit letzter Schluss war Assembler also auch nicht; im Laufe der Jahre wurden daher die verschiedensten höheren Programmiersprachen entwickelt, die das Problem der Übersichtlichkeit lösen sollten. Ich möchte hier keinen geschichtlichen Abriss darüber geben, welche Sprache wann entstanden ist – wen das interessiert, der möge sich selber darüber informieren. Vielmehr soll es in diesem Artikel darum gehen, die grundlegenden Prinzipien von Programmiersprachen im Allgemeinen darzustellen.
Allen Sprachen ist gemein, dass sie einen Aufsatz auf Assembler darstellen und die Assembler-Funktionalität gewissermaßen auf komfortable Art und Weise einkapseln – ähnlich, wie Assembler vorher die Nutzung der Bitketten-Instruktionen vereinfacht hat. Nun war die Entwicklung von Assembler relativ einfach, da es im Grunde lediglich notwendig war, die ohnehin schon vorhandenen Bitketten-Instruktionen durch besser lesbare Befehle zu ersetzen; man konnte also fast eine 1-zu-1-Abbildung durchführen (ganz so einfach ist es natürlich nicht und Assembler kann auch noch mehr Dinge; als Analogie soll es hier aber genügen). Für den nächsten Schritt in der Entwicklung von Programmiersprachen sollte es dann aber schon ein wenig mehr sein.
Das wichtigste zu lösende Problem war die allgemeine Lesbarkeit der Anwendungen. Mit Assembler konnte zwar alles wunderbare ausgedrückt werden, aber wirklich intuitiv war es nicht unbedingt. Schauen wir zum Beispiel einmal auf den folgenden Code:
(2) sub al, var2
(3) mov var3, al
Hier wird nicht anderes gemacht als zwei im Speicher stehende Zahlen voneinander zu subtrahieren und das erhaltene Ergebnis an einer dritten Stelle zu speichern. Aus der Mathematik kennt man hierfür eine weitaus verständlichere Form der Notation:
var3 = var1 - var2
Ist doch so viel besser lesbar, oder? Gut, in der Mathematik hat obige Schreibweise eine etwas andere Bedeutung, nämlich, dass
var3
dem Wert von var1
vermindert um den Wert von var2
entsprechen soll – es ist damit mathematisch gesehen eine Zustandsbeschreibung. Da Computer mit so etwas aber in der Regel nicht klarkommen, wird hier einfach die Bedeutung umdeklariert: var3
soll den Wert annehmen, der sich aus der Subtraktion von var1
und var2
ergibt; damit haben wir eine Anweisungsfolge und keine Zustandsbeschreibung mehr. Wer jetzt der Meinung ist, dass das doch etwas Haarspalterei und am Ende das Gleiche wäre, dem sei dieses Beispiel ans Herz gelegt. Die folgenden beiden Zeilen sind so direkt hintereinander geschrieben in der Mathematik unsinnig:
x = 4
x = x - 1
Die Variable x kann in der Mathematik nicht gleichzeitig den Werten 4 und 3 entsprechen. In der Informatik sieht das allerdings anders aus, da wir es hier mit zwei voneinander getrennten Anweisungen zu tun hätten; zudem ist eine Variable in der Informatik etwas anderes als eine Variable in der Mathematik, da sie bei ersterem eine Stelle im Speicher, bei letzterem aber ein bestimmtes Konzept (etwa einen Wert) symbolisiert. Der folgende Code in der Informatik führt also dazu, dass die Variable x zuerst den Wert 4 und dann den Wert 3 annimmt; man spricht auch von einer Zuweisung. Ich verwende hier übrigens anstatt des in der Informatik oft üblichen Gleichheitszeichens einen Pfeil, um den Unterschied deutlicher zu machen; in gängigen Programmiersprachen wird der Pfeil (leider) durch das Gleichheitszeichen dargestellt (aber das nur am Rande):
(2) x ← x – 1;
Wer sich jetzt fragt, warum ich diesen Unterschied zwischen Mathematik und Informatik so explizit erwähne: eben dieser Unterschied sorgt dafür, dass Mathematiker ohne Informatik-Ausbildung häufig Probleme haben, die Ideen der Programmierung zu verstehen; ebenso ergeht es Programmieranfängern, denen der Unterschied nicht explizit erläutert wird, da sie ihre im Mathematikunterricht erlernten Denkprinzipien auf ein dafür nicht geeignetes Gebiet (eben die Informatik) anwenden wollen.
Damit haben wir gleichzeitig ein wichtiges Verständnisproblem geklärt und unsere ersten Strukturen höherer Programmiersprachen – die Zuweisung und die Notation mathematischer Operationen – kennengelernt. Nur um noch einmal sicherzugehen, dass jeder das eben geschriebene auch verstanden hat, noch einmal die beiden obigen Codezeilen als Assemblercode (ja, ein moderner Assembler würde das wegoptimieren, aber darum geht es hier ja nicht):
(2) mov al, x
(3) sub al, 1
(4) mov x, al
An dieser Stelle seien noch schnell zwei wichtige Begriffe eingeführt, die zur Erklärung von Strukturen innerhalb einer Programmiersprache von Bedeutung sind. Der Begriff der Anweisung (engl. statement) wurde bereits erwähnt und bezeichnet eine in sich abgeschlossene Aktion, die unabhängig von den umgebenden Anweisungen ausgeführt werden kann; die Zeilen x ← 4
und x ← x-1
sind Beispiele für derartige Anweisungen. Eine Anweisung setzt sich immer aus einem oder mehreren Ausdrücken (engl. expression) zusammen. Ein Ausdruck stellt dabei eine Operation dar, die einen Wert liefert, etwa das Ergebnis einer Subtraktion; x-1
wäre so ein Ausdruck, aber auch die Konstante 4
, da auch sie einen Wert liefert – eben den Wert 4. Ich werde im Folgenden diese beiden Begriffe konsequent nach dieser Definition verwenden; dem Leser sei also geraten, dem gewählten Begriff Aufmerksamkeit zu schenken.
Und wenn wir schon einmal bei Begriffserklärungen sind, können auch gleich zwei weitere häufig verwendete Wörter erklärt werden: Syntax und Semantik. Die sind allerdings weniger spektakulär: mit Syntax ist die Art und Weise gemeint, wie Code aufgeschrieben wird und ist vergleichbar mit der Orthographie und Grammatik einer natürlichen Sprache. Jede Programmiersprache definiert ihre eigene Syntax, also eine bestimmte Form, wie Konstrukte (also zum Beispiel Anweisungen und Ausdrücke) zu notieren sind. Demgegenüber steht die Semantik, welche den Inhalt des notierten Codes beschreibt, etwa, was der Ausdruck x-1
genau bedeuten soll; die Semantik entspricht also ganz einfach dem Bedeutungsinhalt natürlicher Sprache. Beide Begriffe werden uns ab und zu begegnen, sollten also verinnerlicht werden.
So, das liest sich zwar ganz schön, aber allein mit Zuweisungen und Rechnungen ist natürlich kein Blumentopf zu gewinnen (geschweige denn, ein komplexes Programm zu schreiben). Für erfolgreiches Programmieren braucht man natürlich noch etwas mehr; zum Beispiel Möglichkeiten, den Programmfluss zu steuern. Wir erinnern uns: im Assembler-Code gab es die Anweisung jump
, mit deren Hilfe unbedingt (immer) oder bedingt (nur bei Gültigkeit einer bestimmten Bedingung) an eine bestimmte Stelle im Programmcode gesprungen werden kann. Nun könnte man diese Anweisung auch in einer höheren Programmiersprachen verwenden (und in der Tat lebt sie hier in Form der goto
-Anweisung fort), verzichtet aber in der Regel aus Gründen der Übersichtlichkeit darauf. Stattdessen greift man hier auf sogenannte strukturierte Anweisungen zurück. In der Regel möchte man zwei Dinge mit Sprüngen erreichen (wie es bereits im Assemblercode demonstriert wurde): eine Verzweigung, so dass ein bestimmter Codeteil nur ausgeführt wird, wenn eine bestimmte Bedingung zutrifft; und eine Wiederholung, so dass ein Codeteil mehrere mal ausgeführt wird.
Zuerst zur Verzweigung, der sogenannten if
-Anweisung. Sie besteht aus zwei Bestandteilen: der Bedingung und dem Körper. Die Bedingung ist ein Ausdruck, welcher den Wert true
(wahr) oder false
(falsch) annehmen muss; der Körper ist eine Folge von Anweisungen, die genau dann ausgeführt wird, wenn die Bedingung zu true
evaluiert (das heißt, den entsprechenden Wert annimmt). Diese Struktur heißt Verzweigung, da sie den Programmfluss in zwei mögliche Zweige aufbricht, von denen mal der eine, mal der andere durchlaufen wird. Ein einfaches Beispiel sieht so aus:
(2) x ← 1
(3) y ← 5
Die beiden Zuweisungen in den Zeilen 2 und 3 werden nur dann ausgeführt, wenn die Variable x
einen Wert größer als 1 besitzt. Da es oft auch nötig ist, nicht nur im Fall der zutreffenden Bedingungen, sondern auch im Gegenfall bestimmten Code auszuführen, wird die if
-Anweisung um einen else
-Zweig erweitert. Erweitern wir obiges Beispiel, ergibt sich dadurch zum Beispiel der folgende Code (der hoffentlich selbsterklärend ist):
(2) x ← 1
(3) y ← 5
(4) else:
(5) x ← 0
(6) y ← 2
Neben den Verzweigungen sind Wiederholungen, auch Schleifen genannt, ein wichtiges Element der strukturierten Programmierung. Grundlegend werden zwei Arten von Schleifen unterschieden: die bedingungsgeprüften bzw. while
-Schleifen und die Zähl– bzw. for
-Schleifen. Während
while
-Schleifen so lange laufen, wie eine bestimmte Bedingung erfüllt ist, zählen for
-Schleifen entsprechend ihrem Namen eine bestimmte Anzahl von Durchläufen. Syntaktisch sieht das etwa so aus:
(2) x ← x – 1
(3) y ← y + 1
(4)
(5) for i from x to y:
(6) z ← z + i
Die erste Schleife läuft so lange, wie die Variable x noch größer 1 ist und reduziert in jedem Durchlauf x um 1 und erhöht y gleichzeitig um 1; die zweite Schleife zählt den Wert i einfach von x
nach y
hoch und addiert ihn dabei immer auf die Variable z
. Wer sich an seine Mathematik-Zeit erinnert: letzteres wurde auch gern mit dem Summenzeichen ∑ notiert.
Nun fehlt nur noch eine Programmstruktur, dann haben wir die wichtigsten und grundlegendsten Dinge für das vernünftige Programmieren zusammen. Glücklicherweise sollte das letzte Konzept allen bereits aus der Mathematik bekannt sein: die Funktionen. In der Mathematik hat es sich als günstig erwiesen, häufig benutzten oder sehr komplizierten Formeln (Ausdrücken) einen Namen zu geben, unter welchem sie schnell und für alle verständlich benutzt werden können. Die bekanntesten Vertreter hierfür sind zweifelsohne die trigonometrischen Funktionen zur Berechnung des Sinus, Cosinus und Tangens. So etwas wie sin(π)
hat sicher jeder schon einmal gesehen; hinter dem sin
verbirgt sich aber nichts anderes als die reichlich komplexe Sinus-Funktion (für Details bitte hier klicken) – man hat ihr also einen Namen gegeben, um sie einfacher ansprechen zu können. Wichtig ist auch die Beobachtung, dass der Sinus-Funktion ein Wert mitgegeben werden muss, auf welchem sie operiert (zum Beispiel das Pi); natürlich können hier nicht nur konkrete Zahlen eingegeben werden, sondern beliebige Ausdrücke und Variablen; so wird etwa der Tangens gerne folgendermaßen definiert:
tan(x) = sin(x) / cos(x)
In der Informatik wird dieses Konzept der Funktionen aufgegriffen und erweitert. Anstatt nämlich lediglich als kürzerer Name für einen komplizierten Ausdruck zu dienen, können Funktionen in der Informatik ganze Anweisungsfolgen vereinen. Dies wird insbesondere dann benutzt, wenn ein komplexerer Code an verschiedenen Stellen im Programm benutzt werden soll; anstatt ihn immer wieder zu schreiben, wird er einmal als Funktion definiert und kann dann an beliebiger Stelle verwendet werden. Praktisch könnte das etwa so aussehen (die Erklärung folgt wie immer unter dem Code):
(2) if x > 0:
(3) return x
(4) else:
(5) return -x
In diesem Beispielcode wird eine Funktion abs
definiert (man spricht von einer Funktionsdefinition oder Funktionsdeklaration), welche den Absolutwert eines gegebenen Wertes x
(also den immer positiven Wert) berechnen soll; den Wert x
nennt man Parameter der Funktion abs
; eine Funktion kann beliebig viele Parameter (auch keinen) erfordern. Da eine Funktion in der Informatik anders als in der Mathematik nicht zwangsläufig nur einen Ausdruck, sondern eine ganze Anweisungsfolge markiert, muss explizit erwähnt werden, zu welchem Wert die Benutzung der Funktion führen soll. Das erfolgt folgerichtig über die return
-Anweisungen in den Zeilen (3) und (5): sie geben einfach an, dass entweder der übergebene Wert selber oder – falls er negativ ist – die Negation davon zurückgegeben, also als Ergebnis der Auswertung der Funktion verwendet werden soll. Benutzt kann sie nun ganz so werden, wie wir es aus der Mathematik kennen, also etwa folgendermaßen:
(2) c ← abs(b)
(3) d ← abs(42)
(4) e ← abs(-1337)
Man spricht hier von einem Funktionsaufruf – die Funktion abs
wurde in diesem Code 4 mal aufgerufen. Die an die Absolutwert-Funktion übergebenen Werte a
, b
, 42
und -1337
nennt man Argumente des Funktionsaufrufes. Und weil es so wichtig ist, wiederhole ich es noch einmal: die beim Aufruf einer Funktion übergebenen Werte heißen Funktionsargumente, die bei der Deklaration der Funktion verwendeten Variablen heißen Parameter – das wird gerne durcheinandergebracht, daher der explizite Hinweis. Mitlesende Nichtinformatiker bitte ich an dieser Stelle, den Artikel noch einmal genau zu lesen und alle Begrifflichkeiten und Bedeutungen zu verinnerlichen – sie werden für die Zukunft wichtig sein (außerdem bitte ich, den Bildermangel zu entschuldigen – Programmiergrundlagen sind einfach kein Thema, dass sich gut bebildern ließe).
So, da nun die grundlegenden Begriffe der modernen Programmierarbeit geklärt sind, können wir uns in zukünftigen Artikeln ins Getümmel stürzen und uns den Feinheiten der Informatikwelt widmen. Ich kann schon versprechen, dass es spannend werden wird!
Kommentare (46)