© SteveSummit (Übersetzung: HelmutLeitner und BerndLuevelsmeyer; wertvolle Korrekturen und Anregungen: F. Fritsche, DavidSchmitt)
Diese Übersetzung ist im Mai 2002 entstanden. Obwohl sich diese Seite - wie jede Wiki-Seite - auch direkt editieren lässt, wäre es besser, allfällige Anmerkungen oder Verbesserungsvorschläge auf der Seite SteveSummit/Übersetzung zu platzieren.
Wer diese Seite von außerhalb verlinken möchte, kann auch http://www.wikiservice.at/dse/wiki.cgi?EKEIDP verwenden. Innerhalb des Wiki geht dementsprechend {{EKEIDP}}.
Einleitung |
Im Grunde bedeutet Programmieren einfach, einem Computer zu sagen, was er tun soll, und diese wahrscheinlich seicht-klingende Erklärung ist keineswegs ein Scherz. Es gibt keine anderen wirklich fundamentalen Aspekte des Programmierens. Worüber wir sonst noch sprechen werden, sind Details besonderer - oft künstlich wirkender - Mechanismen, dem Computer zu sagen, was er tun soll. Manche dieser Mechanismen werden gewählt, weil sie der Programmierer (Mensch) angenehm findet; andere, weil sie der Computer leicht verstehen kann. Die erste Hürde beim Programmieren ist, diese Mechanismen zu erlernen, sich mit ihnen vertraut zu machen und sie zu akzeptieren, egal ob sie nun "sinnvoll" erscheinen, oder nicht.
Man sollte sich auch keine Sorgen machen, wenn einige - oder sogar viele - dieser Mechanismen, die beim Programmieren verwendet werden, nicht einleuchtend sind. Es hat keine besondere Bedeutung, dass bei der Armatur der Kaltwasserhahn links und der Warmwasserhahn rechts angebracht ist; das ist nur eine Konvention, auf die man sich geeinigt hat. In ähnlicher Weise sind viele Programmier-Mechanismen nicht theoretisch motiviert, sondern willkürlich festgelegt, weil man eindeutige Wege benötigt, um dem Computer etwas zu sagen.
Diese Programmier-Einführung enthält Abschnitte über die beim Programmieren benötigten Fähigkeiten, ein vereinfachtes Programmiermodell, die Bestandteile echter Programmiersprachen, die Darstellung von Zahlen, Zeichen und Zeichenketten ("strings") im Computer und die Compiler-Terminologie.
Für das Programmieren benötigte Fähigkeiten |
Ich möchte hier nicht behaupten, dass das Programmieren leicht ist, aber es ist jedenfalls nicht aus jenen Gründen schwer, von denen Leute gewöhnlich ausgehen. Programmierung ist weder eine hochtheoretische Disziplin wie Chemie oder Physik noch braucht man eine akademische Ausbildung um darin gut zu sein. (Es gibt wichtige Prinzipien der Informatik, aber man kann Informatik studieren und seinen Abschluss machen und trotzdem nur vage Vorstellungen davon haben, wie man sie beim Programmieren anwendet. Es gibt sehr viele erfolgreiche Programmierer, die nicht Informatik studiert haben. So werden auch wir wichtige Kenntnisse der Informatik in der Praxis erwerben, ohne uns durch abstrakte Notationen zu kämpfen.)
Vergleicht man das Programmieren mit körperlichen Leistungen, so erfordert es - im Gegensatz zum Sport, zur Malerei oder zum Gesang - keine angeborenen Talente oder Fähigkeiten. Man muss nicht stark sein, eine besondere Körperbeherrschung oder das absolute Gehör mitbringen. Andererseits erfordert das Programmieren handwerkliche Sorgfalt, so wie sie etwa ein Tischler oder ein Feinmechaniker benötigt. Wer je einen Werkkurs besucht hat, wird beobachtet haben, dass manche Teilnehmer mühelos schöne Werkstücke produzieren, während andere - mit zwei linken Händen - genau jene Fehler machen, vor denen der Lehrer gerade warnt. Die geschickten Handwerker sind nicht besser oder schlauer, sondern in jeder Hinsicht aufmerksamer, sorgfältiger und planvoller in ihrer Vorgangsweise. (Vielleicht sind aber auch Aufmerksamkeit und Sorgfalt ebenso angeboren wie Körperbeherrschung; ich bin mir da nicht sicher.)
Eigenschaften, die man beim Programmieren jedenfalls braucht sind: (1) Aufmerksamkeit für Details, (2) eine gewisse Form von Dummheit, (3) ein gutes Gedächtnis und (4) die Fähigkeit abstrakt und vielschichtig zu denken. Lasst uns diese Fähigkeiten noch ein wenig eingehender betrachten:
1. Aufmerksamkeit für Details |
Beim Programmieren sind die Details wichtig, denn Computer sind unglaublich dumm (mehr darüber in Kürze). Wir können uns nicht vage ausdrücken; beispielsweise 3/4 eines Programmes schreiben und dann sagen "du weißt schon, was ich meine?" und dem Computer den Rest überlassen. Alles muss auf Punkt und Komma stimmen. Wenn die Programmiersprache verlangt, dass Variablen vor der Verwendung zu deklarieren sind, dann müssen wir das tun. Wenn in einem Zusammenhang runde Klammern, an anderen Stellen eckige Klammern und an einer dritten Stelle geschwungene Klammern gefordert werden, dann müssen wir dem folgen.
2. Eine gewisse Form von Dummheit |
Computer sind geradezu unglaublich dumm. Sie tun genau das, was man ihnen sagt, nicht mehr und nicht weniger. Gäbe man einem Computer eine Flasche mit Shampoo und den Auftrag die Anweisungen zu lesen und sich die Haare zu waschen, dann sollte man sich vergewissern, dass die Flasche groß ist, denn der Computer wird sich die Haare anfeuchten, einschäumen, ausspülen, wiederholen, Haare anfeuchten, einschäumen, ausspülen, wiederholen, Haare anfeuchten, einschäumen, ausspülen, wiederholen, ...
Ich erinnere mich an die Werbung eines Microprozessor-Herstellers mit all den "intelligenten" Geräten, die es in Zukunft geben würde, im Vergleich zu "dummen" Geräten, wie z. B. Toastern. Ich sehe das anders. Ein Toaster (zumindest einer von der altmodischen Art) hat zwei Regler und einer davon ist optional: wenn man keinen Bräunungsgrad einstellt, wird er trotzdem sein Bestes geben. Man muss dem Toaster nicht sagen, welche Art von Brot oder wieviele Scheiben man toasten will (bei "modernen" Toastern scheint sich der Trend umzukehren...). Im Vergleich dazu die Bedienung von manchen Mikrowellenherden: man kann nicht einmal die Zeit einstellen, solange man nicht schon vorher die Energiestufe festgelegt hat. Beim Programmieren hilft es sehr, wenn man in der Lage ist genauso engstirnig zu "denken", wie es der Computer tut; in diesem Geisteszustand fällt es leichter, alles bis ins letzte Detail festzulegen und keinesfalls anzunehmen, dass das Richtige von selbst geschieht. (Das heißt nicht, dass man alles spezifizieren muss; der Sinn höherer Programmiersprachen wie C ist es, dem Programmierer Detailarbeit abzunehmen. Ein C Compiler macht einige Dinge intuitiv - wenn man beispielsweise einen ganzzahligen Wert in eine Gleitkommavariable speichert, dann wird er die Umwandlung automatisch durchführen. Aber man muss die Regeln kennen, denen der Compiler folgt und wissen, was man explizit spezifizieren muss.)
3. Ein gutes Gedächtnis |
Während des Programmierens muss man vieles im Kopf haben: die Syntax der Programmiersprache, die Menge der verfügbaren Bibliotheksfunktionen und welche Parameter sie verlangen, welche Variablen und Funktionen man selbst definiert hat und wofür man sie benutzt, Vorgehensweisen - die man selbst schon eingesetzt oder von denen man gelesen hat - zur Lösung neu auftauchender Probleme, und ehemals begangene Fehler, um sie nach Möglichkeit zu vermeiden oder schnell an den Symptomen zu erkennen. Je mehr man sich von diesen Details merken kann (im Gegensatz zum ständigen zeitraubenden Nachschlagen), umso leichter wird einem das Programmieren fallen.
4. Die Fähigkeit zu abstrahieren und vielschichtig zu denken |
Dies ist vermutlich die wichtigste Fähigkeit beim Programmieren. Computer gehören zu den komplexesten System, die gebaut werden und wenn man beim Programmieren jedes funktionelles Detail des Computers auf allen Ebenen im Kopf haben müsste, dann wäre es eine Herkules-Aufgabe, auch nur ein simples Programm zu schreiben.
Eine der wirkungsvollsten Techniken zum Umgang mit der Komplexität von Software-Systemen (beliebiger komplexer Systeme) ist die Zerlegung in kleine Black-Box-Prozesse, die nützliche Aufgaben erfüllen und Details in sich aufnehmen, damit man sie nicht im Kopf behalten muss. Wenn ich jemand bitte, im Geschäft Milch zu kaufen, dann sage ich ihm nicht, dass er zur Tür gehen, sie öffnen, hinausgehen, die Autotür öffnen, ins Auto einsteigen, zum Geschäft fahren, aus dem Auto aussteigen, ins Geschäft gehen soll, etc. Insbesonders sage ich nicht dazu - ja denke nicht einmal darüber nach - dass beim Gehen die Füße zu heben sind, beim Öffnen der Türen nach den Türgriffen zu greifen ist, etc. Man braucht sich auch (außer im Falle einer schweren Krankheit) keine Gedanken über die Atmung und den Blutkreislauf zu machen, der uns in die Lage versetzt all diese Aufgaben und Teilaufgaben zu erfüllen.
Wir können dieses kleine Beispiel auch in die andere Richtung ausdehnen. Bei der Zubereitung von Eiscreme könnte man feststellen, dass die Milch fehlt und ohne besonderen Auftrag welche einkaufen. Bei einer Party-Vorbereitung könnte die Eiscreme-Zubereitung Teil dieser größeren Aufgabe sein. Und so weiter, und so fort. Die Zerlegung in Teilaufgaben bzw. die Abstraktion ist eine lebenswichtige Fähigkeit beim Programmieren bzw. bei der Handhabung komplexer Systeme. Trotz des unter Punkt 3 über das Gedächtnis gesagten, können wir immer nur eine kleine Anzahl von Dingen gleichzeitig im Kopf haben. Ein großes Programm kann 100.000 oder 1.000.000 oder 10.000.000 Programmzeilen haben. Wenn es notwendig wäre, alle Zeilen im Kopf zu haben und vor allem gleichzeitig zu verstehen, könnte man so ein Programm unmöglich erstellen oder verstehen. Nur weil es möglich ist, über jeweils kleine Teile isoliert nachzudenken, kann man mit so großen Programmen umgehen.
Aufgabenzerlegung ist zwar ein mächtiges Werkzeug, ergibt sich aber einerseits nicht automatisch und ist andererseits auch kein Patentrezept für jedes organisatorische Problem. Wir agieren ständig mit einem Bündel von Annahmen darüber, wie die Dinge um uns herum funktionieren, und alles klappt solange diese Annahmen zutreffen. Um auf das letzte Beispiel zurückzukommen, wenn ich jemand bitte, ins Geschäft zu gehen und Milch zu kaufen, dann gehe ich davon aus, dass er weiß, welche Milch und welches Geschäft ich meine und wie er dorthin fährt, etc. Wenn diese Annahmen nicht stimmen, oder wenn es Alternativen gibt, dann muss ich meinen Auftrag genauer formulieren, indem ich z. B. von einem bestimmten Supermarkt spreche, den Weg beschreibe oder die 2%-Milch verlange.
Aus diesem Grund reicht es nicht, einfach alle Aufgaben weiter und weiter zu zerlegen und so die Probleme der Komplexität zu erledigen. Wir müssen immer zumindest einen Teil der Annahmen, die unsere Strukturierung begleiten, im Kopf haben. We müssen wissen, was wir von den Prozessen (Personen, Computerprogrammen, etc.), die wir für unsere Aufgaben verwenden, erwarten können bzw. nicht erwarten dürfen. Wir müssen sicherstellen, dass wir unseren Teil dieser Verträge erfüllen, die eigenen Zusagen und Versprechungen einhalten, auf die sich andere Teile des Systems verlassen.
Gleichzeitig einerseits über die Wechselwirkungen in diesen hierarchischen Beziehungen nachzudenken während man sich andererseits mit Details auf jeder Ebene beschäftigen muss, ist das was ich mit "vielschichtigem Denken" meinte. Es sind knifflige Aufgaben (wie man sieht, sogar knifflig zu beschreiben) aber dies ist die einzige erfolgversprechende Methode um große und komplexe Probleme zu bearbeiten.
Was das Programmieren noch schwierig macht (neben den vier oben angeführten Problemfeldern) sind die organisatorischen und menschlichen Probleme, sowie die Fülle an nervtötenden Mini-Problemen aller Art. Ein großes Programm ist ein erstaunlich komplexes System und ein großes Programmierprojekt mit vielen Beteiligten muss sehr viele periphere Kommunikations- und Dokumentationsaufgaben erfüllen, um nicht in der Flut von Detailinformationen und fehlererzeugenden Missverständnissen zu ertrinken.
Ein vereinfachtes Bild des Programmierens |
Stellen wir uns einen gewöhnlichen Taschenrechner vor, der Addieren, Subtrahieren, Multiplizieren und Dividieren, sowie sich in einigen Speicherregistern ein paar Zahlen merken kann. In einer übersimplifizierten Sicht (die wir in Kürze wieder über Board werfen) können wir uns einen Computer als Taschenrechner vorstellen, der seine eigenen Tasten drücken kann. Ein Computerprogramm ist dann lediglich eine Liste von Anweisungen, die dem Computer sagen, welche Tasten er wann drücken soll (es gibt sogar tastenprogrammierbare Taschenrechner die genau auf diese Art funktionieren).
Nun soll unser Rechner folgende Aufgabe ausführen:
Die Anweisungen könnten nun folgendermaßen aussehen:
Reale Computer können einiges mehr, als ein simpler Taschenrechner mit seinen 4 Grundrechnungsarten; z. B. können sie Texte verarbeiten und mit andere Daten als nur mit Zahlen umgehen. Wir vergessen aber jetzt den Taschenrechner und schauen uns an, was reale Computer (zumindest wenn sie von Programmiersprachen wie C gesteuert werden) leisten können.
Ein wirklichkeitsgetreues Bild des Programmierens |
Ein Computerprogramm besteht aus zwei Teilen: Code und Daten. Als Code bezeichnet man die Folge von Befehlen für die Durchführung einer Aufgabe. Als Daten bezeichnet man die "Register" oder "Speicherplätze" welche das Programm im Zuge der Berechnungen für Zwischenresultate oder Endergebnisse verwendet.
An dieser Stelle ist zu beachten, dass der Code ist relativ statisch ist, die Daten dagegen dynamisch sind. Wenn ein Programm geschrieben ist und zufriedenstellend arbeitet, ändert sich der Code nicht mehr. Dagegen wird das Programm bei jedem Aufruf mit anderen Daten arbeiten, wodurch die Speicherplätze verschiedene Werte enthalten.
Wenn jemand ein Programm schreibt, dann hat er damit etwas geschaffen, das dem Computer eine neue Fähigkeit verleiht. Alle Applikationen die Computer enthalten (Textverarbeitung, Grafikprogramme, Spiele, etc.), sind auch "nur" Programme, die von anderen Programmierer geschrieben wurden und zwar mit Programmiersprachen, die für jeden erhältlich sind.
Elemente realer Programmiersprachen |
Es gibt eine Anzahl von Elementen, die in Programmen bzw. in Programmiersprachen typischerweise vorkommen. Diese Elemente findet man in jeder Sprache, nicht nur in C. Wenn man mit diesen Elementen und ihre Anwendung vertraut ist, dann versteht man nicht nur C, sondern auch andere Programmiersprachen besser und das Erlernen weiterer Sprachen bzw. das Wechseln zwischen verschiedenen Sprachen wird viel einfacher.
1. Es gibt Variablen (oder Objekte), in welche man jene Daten speichern kann, mit denen ein Programm arbeitet. Variablen sind das Mittel der Wahl, mit dem Speicherplätze (Daten) zugänglich gemacht werden. Sie entsprechen den Registern in unserem Taschenrechner-Beispiel. Jede Variable kann entweder global (allen Teile eines Programmes) oder lokal (nur einzelnen Teilen eines Programmes) zugänglich sein.
2. Es gibt Ausdrücke, die zur Berechnung neuer Werte aus vorhandenen Daten dienen.
3. Es gibt Zuweisungen zur Speicherung von Werten (anderer Variablen oder berechneter Ausdrücke) in Variablen. In vielen Sprachen wird die Zuweisung durch ein Gleichheitszeichen symbolisiert, so könnten uns Zuweisungen wie
b = 3oder
c = d + e + 1begegnen. Die erste Zuweisung setzt die Variable b auf den Wert 3; Die zweite Zuweisung setzt die Variable c auf die berechnete Summe der Variablen d und e vermehrt und der Zahl 1.
Die Verwendung des Gleichheitszeichens kann anfangs leicht verwirren sein. In der Mathematik bedeutet das Gleichheitszeichen die behauptete Identität zweier Ausdrücke unabhängig von der Zeit. Beim Programmieren gibt es den Faktor Zeit und einen Ursache-Wirkung-Zusammenhang: Nach der Zuweisung ist das Objekt links vom Gleichheitszeichen ident mit dem Wert des Ausdrucks auf der rechten Seite im Augenblick vor der Zuweisung. Um sich das zu verdeutlichen sollte man eine Zuweisung a = 3 nicht als "a gleich 3" sondern als "a wird 3 zugewiesen" oder "a erhält den Wert 3" lesen.
(Einige wenige Programmiersprachen verwenden andere Symbole für die Zuweisung
a <-- 3um den Vorgang der Zuweisung zu verdeutlichen, aber diese Notationen sind nicht sehr populär, vielleicht nur aus dem Grund, weil es kaum Zeichensätze mit Linkspfeilen gibt bzw. weil die Taste mit dem Linkspfeil den Cursor verschiebt und keinen Linkspfeil am Bildschirm erscheinen lässt.)
Wenn Zuweisungen zunächst natürlich und klar erscheinen, was kann dann
i = i + 1bedeuten? In der Algebra subtrahieren wir i auf beiden Seiten und kommen mit
0 = 1zu einem klaren Widerspruch. Beim Programmieren sind jedoch Zuweisungen wie
i = i + 1alltäglich und - wenn man sich die Arbeitsweise einer Zuweisung in Erinnerung ruft - auch nicht allzu schwer zu verstehen: Die Variable i auf der linken Seite bekommt einen neuen Wert, der sich - wie immer - aus der Berechnung des Ausdruckes auf der rechten Seite ergibt. Der Ausdruck holt den aktuellen (alten) Wert von i, addiert 1 dazu und dieser neue Wert kommt in die Variable i. So erhöht "i=i+1" den Wert von i um 1; Wir sagen: i wird "inkrementiert". (übrigens wird sich herausstellen, dass in C eine Zuweisung auch zugleich ein Ausdruck ist)
4. Es gibt Vergleiche, mit denen man überprüfen kann, ob eine Bedingung erfüllt ist: Etwa ob eine Zahl größer ist als eine andere. (In einigen Sprachen inklusive C sind Vergleiche nichts anderes als Ausdrücke, die zwei Werte vergleichen und als Resultat entweder wahr (true) oder falsch (false) errechnen.
5. Variablen und Ausdrücke besitzen einen Typ (Datentyp), der die Art der zu erwartenden oder zu speichernden Daten angibt. Man könnte beispielsweise deklarieren (festlegen), dass eine Variable eine Zahl speichern soll, während eine andere Variable zur Speicherung eines Textes bestimmt ist. In vielen Sprachen (inklusive C) muss man sogar Name und Typ einer Variablen ausdrücklich deklarieren, bevor man sie verwenden kann.
Im Rahmen der verschiedenen Programmiersprachen gibt es alle möglichen Arten von Datentypen: für einzelne Zeichen, Ganzzahlen und Gleitkommazahlen (reelle Zahlen). Es gibt Strings (Zeichenketten, Text), Felder (Arrays, Vektoren, Matrizen) von Zahlen oder andere Datentypen. Zuletzt gibt es auch vom Programmierer definierte Datentypen wie z. B. Datensätze oder Strukturen, welche die Beschreibung komplizierterer Objekte mit Hilfe von Datenstrukturen erlauben, die aus einfacheren Typen zusammengesetzt werden.
6. Es gibt Befehle (Anweisungen) die den Ablauf eines Programmes Schritt für Schritt beschreiben. Befehle können Ausdrücke berechnen, Zuweisungen durchführen oder Funktionen aufrufen (siehe unten).
7. Es gibt Befehle zur Ablaufkontrolle, welche die Ausführung anderer Befehle beeinflussen. Ein bestimmter Befehl könnte z. B. nur dann ausgeführt werden, wenn eine bestimmte Bedingung erfüllt (wahr) ist.
8. Gruppen von Befehlen, Deklarationen und Ablaufkontroll-Befehlen können in Funktionen (Prozedur, Unterprogramm, Methode) zusammengefasst werden. Sie wirken wie neue Befehle, der man von anderen Stellen im Programm vielfach aufrufen (verwenden) kann.
Beim Aufruf einer Funktion wird ihr die Kontrolle so lange übertragen, bis sie ihre Arbeit erledigt hat und die Kontrolle zurückgegeben hat. Beim Aufruf können einer Funktion auch Parameter (Werte) mit übergeben werden, welche die Arbeitsweise der Funktion steuern. Bei der Rückgabe kann die Funktion Rückgabewerte (Resultate, return-Werte) liefern.
Durch die Platzierung von Befehlen in Funktionen vermeidet man nicht nur Wiederholungen, wenn gleiche Aktionen mehrfach an verschiedenen Stellen im Programm erforderlich sind, sondern das Programm wird auch leichter verständlich. Man kann erkennen, dass eine Funktion - hoffentlich mit einer klaren Aufgabenstellung - aufgerufen wird und braucht sich nicht um die Details zu kümmern (Wer jemals gestrickt hat weiß, dass Strickanleitungen oft kleine Unter-Anleitungen oder Muster enthalten, die oftmals und an verschiedenen Orten eingesetzt werden. Diese Unter-Anleitungen ähneln sehr stark den Aufrufen von Funktionen beim Programmieren.)
9. Ein Programm besteht aus einer Gruppe von Funktionen, globalen Variablen und anderen Elementen. Als zusätzliche Komplikation können die Befehle eines Programmes (der Source) über ein oder mehrere Textdateien (Sourcefiles) verteilt sein. Andererseits können mehrere Programme als Programmpaket zur Erledigung einer größeren Aufgabe zusammenarbeiten, aber wir lassen diese großräumigere Programmintegration vorläufig außer Acht.
10. Wenn man ein Programm in der für einen Compiler passenden Form erstellt, dann gibt es üblicherweise einige logistische Details, um die man sich kümmern muss. Das kann die Spezifikation von Compiler-Parametern betreffen oder die Abhängigkeiten zwischen Funktionen oder anderen Programmteilen. Diese Details müssen oft in einer unterschiedlichen (meist vorlagen- oder bausteinartigen) Syntax spezifiziert werden.
Viele der besprochenen Elemente sind in Hierarchien organisiert. Ein Programm besteht typischerweise aus Funktionen und globalen Variablen. Eine Funktion besteht aus Befehlen. Befehle enthalten Ausdrücke. Ausdrücke wirken auf Objekte. (Wir können die Hierarchie auch in die andere Richtung weiterführen; z. B. werden oft Programme mit unterschiedlicher aber sich ergänzendem Profil zu Programmpaketen zusammengefasst. Beispielsweise die verschiedenen "Office-Pakete" bestehend aus Textverarbeitung, Tabellenkalkulation, etc.).
Wie schon erwähnt, haben viele Konzepte der Programmierung etwas Willkürliches an sich. Das gilt besonders für die Begriffe Funktion, Befehl und Ausdruck. Man könnte jeden dieser Begriffe als "Element eines Programmes, das etwas tut" definieren. Die Unterschiede bestehen hauptsächlich in der Abstraktionsebene, auf der dieses "etwas" geschieht, aber wir brauchen uns - zumindest im Moment - nicht um eine exaktere Definition dieser Ebenen zu bemühen, denn man entwickelt leichter ein Verständnis dafür, sobald man mit dem Schreiben von Programmen beginnt.
Vielleicht hilft uns eine Analogie: So wie ein Buch aus Kapiteln, Abschnitten, Absätzen, Sätzen und Wörtern (und die wieder aus Buchstaben) besteht, so besteht ein Programm aus Funktionen, Funktionen aus Befehlen und diese wiederum aus Ausdrücken (sie enthalten wiederum kleinere Elemente, um die wir uns jetzt nicht kümmern). Jedoch hat jede Analogie ihre Grenzen und auch diese Analogie sagt uns nichts darüber, was die Begriffe Funktion, Befehl und Ausdruck wirklich bedeuten. Lediglich das Bild dieser buchähnlichen hierarchischen Ordnung vermag vielleicht ein bisschen zum Verständnis beitragen.
Unsere Ausführungen sind sehr allgemein gehalten und beschreiben Elemente, die wir in den meisten "konventionellen" Programmiersprachen vorfinden. Wenn man diese Elemente in ihrer abstrakten Form verstanden hat, dann wird das Erlernen einer neuen Sprachen zur relativ simplen Frage, wie diese Sprache diese Elemente implementiert. (Natürlich kann man die Elemente nicht in völlig abstrakter Form verstehen; man muss sie mit realen Beispielen in Beziehung setzen. Für jemanden, der noch nie programiert hat, war der größte Teil dieses Abschnittes vermutlich unverständlich. Man sollte nicht allzu viel Zeit darauf verschwenden, die gesamte Bedeutung zu erfassen, sondern statt dessen nach dem Erlernen einer Programmiersprache wie C wieder zu diesem Text zurückkehren und ihn nochmals lesen).
Zum Abschluß: es gibt keinen Grund, die Abstraktion auf die Spitze zu treiben. Wir werden es mit einfachen Progammen (in Programmiersprachen wie C) zu tun haben, bei denen Abfolgen von Berechnungen und anderen Operationen ziemlich einfach in Ausdrücke, Befehle und Funktionen übersetzt werden, so dass sie der Computer verstehen bzw. ausführen kann. Funktionen werden aufgerufen, um Teilaufgaben zu erfülllen und geben Resultate an die darauf wartende, aufrufende Stelle zurück. Die Befehle werden nacheinander ausgeführt, es sei denn, der Ablauf wird durch Bedingungen oder Wiederholungen abgewandelt.
Zahlendarstellung im Rechner |
Meist werden ganze Zahlen im Rechner binär mit einer gewissen Anzahl Bits dargestellt. Mit 16-Bit-Zahlen können Werte von 0 bis 65535 (also 0 bis 2^16-1) dargestellt werden; oder, falls die Hälfte des Bereichs für negative Werte vorgesehen ist, von -32767 bis 32767. (Die Einzelheiten der Darstellung negativer Zahlen übergehen wir vorerst.) Eine 32-Bit-Zahl kann Werte von 0 bis 4.294.967.295 annehmen, oder von -2.147.483.647 bis +2.147.483.647.
Die meisten heutigen Rechner stellen Fliesskommazahlen (gebrochene Zahlen) mit Hilfe der Exponentialnotation dar. Der Vorteil dieser Darstellung ist, dass man den Wertebereich und die Anzahl gültiger Stellen beide sinnvoll ausnutzen kann. Es ist jedoch nicht möglich, alle gebrochenen Werte als Fliesskommazahlen abzubilden. Da es pro Zahl nur eine begrenzte Menge an Speicherplatz gibt, sind sowohl der Genauigkeit als auch dem Wertebereich Grenzen gesetzt.
Angenommen, man verwendet 6 Dezimalziffern für eine Fliesskommazahl, und teilt diese auf in 3 Vorkommaziffern und 3 Nachkommaziffern, dann kann man Werte von -999,999 bis 999,999 damit speichern; die kleinste positive Zahl wäre 0,001. Auch die Auflösung wäre 0,001, man könnte also z. B. 0,001 und 0,002 darstellen, ebenso wie 999,998 oder 999,999. Dies wäre eine mögliche Abbildung, wenn auch eine unpraktische, da sowohl Wertebereich als auch Auflösung nicht besonders beeindruckend sind.
Wenn man jedoch andererseits eine exponentielle Darstellung verwendet, die 4 Ziffern für die Mantisse und 2 Ziffern für den Exponenten nutzt, dann kann man Werte zwischen 9,999*10^99 und -9,999*10^99 abbilden, und die kleinste positive Zahl ist 1*10^-99. (Mit nichtnormalisierter Darstellung sogar: 0,001*10^-99.) Dies erlaubt die Verwendung betragsmäßig viel größerer und viel kleinerer Werte; dem entgegen steht, dass die Auflösung nicht mehr konstant ist, sondern schlechter wird, je grösser die Zahlen werden. Beispielsweise muss 123,456 als 123,4 gespeichert werden, und 123.456 als 123.400. 999,999 kann überhaupt nicht mehr dargestellt werden, man muss sich zwischen 999,9 (in Wirklichkeit 9,999*10^2) oder 1000 (in Wirklichkeit 1,000*10^3) entscheiden. 999,998 und 999,999 sind nicht mehr unterscheidbar.
Zur Minimierung des Schreibaufwands wird normalerweise eine andere Schreibweise in Programmquellen verwendet. Die Zahl 1,234*10^5 wäre dann als 1.234e5 zu schreiben; das "e" darin bedeutet "mal 10 hoch". Auch ist das Dezimalkomma durch den angloamerikanischen Dezimalpunkt zu ersetzen, und die Trennpunkte der Tausenderstellen entfallen.
Gebrochene Zahlen in Exponentialschreibweise werden im Zusammenhang mit Rechnern oft als "Fließkommazahlen" oder "floats" bezeichnet. Man verwendet auch den Begriff "double" zur Benennung von Fließkommazahlen mit doppelter Genauigkeit (d.h. die Mantisse hat die doppelte Länge). Letztlich ist es auch möglich, "echte Fliesskommazahlen" zu verwenden, das sind diejenigen die nicht in Exponentialdarstellung gespeichert werden, wie im ersten Beispiel mit den 3 Stellen vor dem und nach dem Komma; diese werden hier jedoch nicht weiter beschrieben.
Wichtig ist, stets zu bedenken, dass die Auflösung von Fließkommawerten begrenzt ist, was zu Rundungsfehlern führt, die oft unerwartete Ergebnisse hervorrufen. Als Beispiel: 1/3 kann nicht exakt dargestellt werden, da der periodische Bruch 0,33333... nicht endet. Folglich liefert die Berechnung von (1/3)*3 nicht 1 sondern 0,999999999... Ein weiteres Beispiel: 1/10 kann in der Binärdarstellung nicht exakt dargestellt werden sondern ist darin ebenfalls ein periodischer Bruch; folglich ist auch (1/10)*10 oft nicht exakt 1 sondern eher 0,999999999... Aus all diesen Gründen liefern Berechnungen mit Fliesskommazahlen normalerweise keine exakten Ergebnisse sondern Annäherungen. Insbesondere sollten Fließkommazahlen nicht auf Gleichheit getestet werden; und der Programmierer muss stets darauf achten, dass Rundungsfehler sich nicht derart aufschaukeln, dass das Endergebnis völlig unbrauchbar wird.
Zeichen, Zeichenketten und Zahlen |
Die allerersten Rechner konnten nur Zahlen verarbeiten, aber alle heutigen Rechner verarbeiten auch Texte. Rechner (und Programmiersprachen) unterscheiden aber immer genau zwischen Zahlenwerten und Texten, und so müssen auch wir in unseren Köpfen diese Trennung aufrecht erhalten.
Die Grundlage der Bearbeitung von Texten ist der zugehörige Zeichensatz. Der Zeichensatz nummeriert die Menge aller Zeichen, die der Rechner verarbeiten und darstellen kann (jedes Zeichen hat ein Bitmuster für die Ausgabe am Bildschirm, die meisten Zeichen sind durch Tasten oder Tastenkombinationen mittels der Tastatur eingebbar). Ein Zeichensatz besteht aus Buchstaben, Ziffern, Satzzeichen und so weiter, aber wesentlich ist nicht so sehr die Eigenart all dieser Zeichen, sondern vielmehr, dass man Zeichen, Zeichenketten und Zahlen sorgfältig auseinanderhalten muss.
Ein Zeichen ist, nun ja, ein einzelnes Zeichen. Eine Variable, die ein Zeichen speichert, könnte z.B. den Buchstaben 'A' oder die Ziffer '2' oder das Symbol '&' als Wert enthalten.
Eine Zeichenkette ist eine Folge von null oder mehr Zeichen. Beispielsweise besteht die Zeichenkette "und" aus den Zeichen 'u', 'n', und 'd'. Die Zeichenkette "K2!" besteht aus den Zeichen 'K', '2' und '!'. Die Zeichenkette "." besteht nur aus einem einzigen Zeichen '.' (trotzdem ist die Zeichenkette "." nicht identisch mit dem Zeichen '.'). Die leere Zeichenkette "" als Sonderfall enthält überhaupt keine Zeichen. Als abschließende Beispiele: Die Zeichenkette "123" besteht aus den Zeichen '1', '2' und '3', und die Zeichenkette "4" besteht aus dem einen Zeichen '4'.
Die letzten beiden Beispiele zeigen einen wichtigen und vielleicht überraschenden oder ärgerlichen Unterschied: Das Zeichen '4' und die Zeichenkette "4" sind konzeptionell verschieden, und beide sind wiederum verschieden von der Zahl 4. Die Zeichenkette "123" besteht aus 3 Zeichen; für uns sieht sie aus wie die Zahl 123, aber für den Rechner ist es keine Zahl sondern einfach nur eine Zeichenkette [und er speichert sie auch nicht wie einen Zahlenwert ab]. Ein Zahlenwert für Berechnungen würde nicht in Form der einzelnen Ziffern gespeichert, sondern typischerweise als binäre 16-Bit-Zahl oder 32-Bit-Zahl. Eine Zeichenkette aus Ziffern, die als Zahlenwert aufgefasst werden soll, muss zuvor umgewandelt werden. Ebenso muss ein numerischer Wert in eine Zeichenkette umgewandelt werden, wenn er gedruckt oder am Bildschirm ausgegeben werden soll.
Es kommt auch vor, dass man zwischen den Zeichen und ihren zugehörigen Codes hin- und herwechseln muss. Zum Beispiel hat im ASCII-Zeichensatz der Buchstabe 'A' den Code 65, der Punkt '.' hat den Code 46, und die Ziffer '4' vielleicht überraschenderweise den Code 52.
Compiler-Terminologie |
C ist eine compilierte Programmiersprache. Das heißt, die in C geschriebenen Programme werden durch einen sogenannten Compiler in die direkt ausführbaren Maschinenanweisungen übersetzt. Solche Programme benötigen keine Laufzeitumgebung und starten und arbeiten sehr schnell. Weder Programmquelle noch Compiler sind während der Programmausführung erforderlich und es genügt, die ausführbaren Programme an die Anwender zu verteilen. Auf Grund der hohen Geschwindigkeit sind sie besonders für solche Anwendungen geeignet, bei denen Programme millionenfach unverändert über Jahre hinweg und an vielen Orten Tag für Tag eingesetzt werden.
Ein Compiler ist eine besondere Art Programm: Ein Programm, das andere Programme erzeugt. Der Compiler wird aufgerufen, liest die in der Programmiersprache geschriebenen Anweisungen der Programmquelle, und erzeugt daraus das ausführbare Programm. Nachdem der Compiler seine Arbeit beendet hat, kann das gerade erzeugte ausführbare Programm sofort aufgerufen und ausprobiert werden.
Die Alternative zum Compiler ist der Interpreter, etwa für die Programmiersprache BASIC. Ein Interpreter führt die Befehle eines Programms unmittelbar aus (d. h. er übersetzt Befehl für Befehl in Maschinenanweisungen und führt diese dann aus), statt daraus ein ausführbares Programm zu erzeugen. Der Interpreter ist langsamer, weil er während des Programmablaufs ständig übersetzt. Wird ein Programm einer interpretierten Sprache weitergegeben, so muss auf dem Zielrechner der Interpreter verfügbar sein, um es dort auszuführen; ein einzelnes selbständig lauffähiges Programm existiert dann nicht.
Zusammenfassend: Ein Compiler erzeugt für jede Programm-Anweisung eine Folge zugehöriger Maschinenanweisungen mit derselben Bedeutung, während ein Interpreter jede Programm-Anweisung unmittelbar übersetzt und ausführt.
Der Vorteil eines Interpreters ist, dass der Zwischenschritt des Compilierens entfällt. (Heutzutage ist jedoch weder Compilieren noch Interpretieren besonders zeitaufwendig, so dass dieser Unterschied an Bedeutung verliert.)
In Wirklichkeit ist es jedoch nicht von der Programmiersprache abhängig, ob compiliert oder interpretiert wird. So gibt es z.B. auch Compiler für BASIC und Interpreter für C. Wenn eine Sprache entworfen wird, dann hat der Sprachentwickler dabei aber normalerweise entweder einen Interpreter oder einen Compiler im Sinn, und wenn man später eine Interpreter-Sprache compilieren will oder umgekehrt, dann ist das oft mit einigen Schwierigkeiten verbunden.
So wichtig der Unterschied zwischen Compiler und Interpreter ist, er sollte auch nicht überbewertet werden. Während man ein Programm schreibt, gewöhnt man sich so sehr an diese Details, dass sie zur Nebensache werden. Aber es ist immerhin nützlich, die grundsätzlichen Unterschiede zu kennen, da sie uns manche Aspekte der verschiedenen Programmiersprachen besser verstehen lassen.
Bei der Arbeit mit einem Compiler gibt es diverse Feinheiten zu beachten. Zunächst erstellt man den Quelltext bzw. die Quelltexte in der jeweiligen Programmiersprache mit einem Text-Editor; typischerweise nicht mit einer kompletten Textverarbeitung, denn ein Compiler kann mit Formatierungen und Layout wenig anfangen. Jede dieser Quelldateien wird dann einzeln durch den Compiler übersetzt, und das Ergebnis ist jeweils eine sogenannte Objekt-Datei mit den Maschinenanweisungen. Die Objekt-Dateien sind jedoch in der Form noch nicht direkt ausführbar, denn im Allgemeinen enthalten sie Aufrufe von Funktionen, die in ihnen nicht enthalten sind, insbesondere Aufrufe von Funktionen aus der Standardbibliothek des Compilers. Die Objekt-Dateien müssen folglich mit diesen Funktionen zusammengefasst werden, was die Aufgabe des Linkers ist. Der Linker kombiniert alle Objekt-Dateien und die erforderlichen Bibliotheksfunktionen in eine ausführbaren Programmdatei. Sollte das dem Linker nicht möglich sein - etwa weil er eine aufgerufene Funktion nicht findet - dann endet das mit einer Fehlermeldung.
Verwendet man andererseits eine sogenannte integrierte Programmierumgebung, so finden die Aufrufe von Compiler und Linker meistens automatisch und reibungslos im Hintergrund statt, so dass wir davon kaum etwas wahrnehmen.
Anmerkungen von Lesern |
Ich finde diese Seite recht gut. Wenn man sie von oben bis unten in einem Zug durchliest, stellt sich allerdings der Eindruck ein, daß es in der zweiten Hälfte stetig gedrängter und kurzatmiger wird, so als ob der Autor es zu Beginn noch ausgehalten hat.
Anmerkung eines HTML-Menschen |
Das Kapitel über das notwendige Maß an Dummheit ist genial ... (vor allem der vollintelligente Toaster ... der ist bei mir rausgeflogen ... ich wende manuell nach dem Programm "Wenns raucht, dann schmeckts")... beste Realsatire. Nur einen Aspekt vermisse ich: Die menschliche Dummheit. Anwender eines Programms und Besucher einer Webseite haben eines gemeinsam: Sie klicken Knöpsches ... und der Programmierer wie der Webdesigner haben die Verpflichtung, im Voraus zu ahnen, was die Benutzer erwarten und ihnen genau das anzubieten.