Immer wieder hört und liest man, dass das Erlernen einer Programmiersprache, bzw. die Programmierung selbst schwer sind und viel Zeit verschlingen. Ich habe eine gute und eine schlechte Neuigkeit. Zuerst die schlechte: Leider muss man tatsächlich relativ viel Zeit investieren um überhaupt zu zufriedenstellenden Ergebnissen zu kommen. Es bringt nichts das zu verheimlichen, denn es würde nur zu Desillusionierung und Frust führen. Nun aber die gute: Es ist nicht schwer und jeder kann es erlernen. Um genau zu sein hat jeder von uns schon mal programmiert, wenn auch unbewusst und nicht am Computer. Man programmiert bereits, wenn man den nächsten Tag plant. Man programmiert, wenn man ein Kochrezept niederschreibt. Man programmiert, wenn man an einem Sporttraining teilnimmt. Programmierung begegnet uns überall im Alltag, es ist uns nur nicht bewusst.
Beispiel 1.1: Ein mögliches Programm Anmerkung: Pseudocode ist rein informeller Code, der zwar strukturiert ist, aber keinen besonderen Regeln folgt. Pseudocode wird verwendet, wenn Menschenlesbarkeit vor Maschinenlesbarkeit steht, wie etwa bei generischen (allgemeinen) Beispielen, oder Algorithmen.
Motiviert weiterzulesen? Gut, sehen wir uns dann an, was Programmierung im Kontext des Computers bedeutet: Programmierung ist der Prozess der Entwicklung eines Programms. Ein Programm (auch Software genannt) ist eine Folge von Anweisungen. Eine Anweisung ist mindestens ein Befehl an die CPU.
Natürlich kann kein Computer etwas mit dem obigen Programm anfangen, denn der Programmierer kommuniziert durch sein Programm mit elektronischen Bauteilen und keinen Menschen. Das heißt, Anweisungen müssen bestimmten Regeln folgen und diese Regeln sind Programmiersprachen.
Das obige Programm löst übrigens ein bestimmtes Problem durch eine Handlungsvorschrift in endlich vielen Schritten, nämlich das Hungerproblem. Man bezeichnet solche Lösungsverfahren auch als Algorithmen.
Die Herausforderung des Programmierens ist nun, eine Aufgabe, bzw. ein Problem mit Hilfe von Algorithmen - ausgedrückt in einer Programmiersprache - zu lösen.
2. Grundkenntnisse über den Computer
Programmierung ist heute oft maschinenfern. Moderne Programmiersprachen abstrahieren das Innenleben und die Kommunikationsschnittstelle von Computern nahezu vollkommen. Wer aber ernsthaft Programmieren lernen will, kommt zumindest um ein paar Grundkenntnisse nicht vorbei.
Was ist ein Computer und wie ist er aufgebaut? Ein Computer (Rechner) ist ein programmierbarer Apparat zur Verarbeitung von Daten. Waren Computer in der Vergangenheit noch reine Rechenmaschinen mit einem Aufgabengebiet vergleichbar mit dem heutiger Taschenrechner, so sind moderne Computer zu weit mehr fähig und werden sehr vielseitig eingesetzt. Heutige Computer sind weitgehend dem Von-Neumann-Rechner nachempfunden, weisen natürlich aber unzählige, weitere Komponenten auf. Nachfolgend sind die wichtigsten Computerkomponenten aufgezählt.
CPU (Central Processing Unit, Hauptprozessor): Die CPU führt Programme aus und wird nochmal unterteilt in folgende Unterkomponenten.
Rechenwerk: Das Rechenwerk (auch ALU, Arithmetic Logic Unit, Arithmetisch Logische Einheit genannt) führt elementare Rechenoperationen wie Addition und Subtraktion, logische Verknüpfungen und Schiebeoperationen aus.
Steuerwerk: Das Steuerwerk ist dafür zuständig den Programmablauf zu koordinieren. Anweisungen eines Programms liegen als Maschinencode vor, der erst interpretiert werden muss, bevor das Rechenwerk weiß was zu tun ist. Das Steuerwerk holt und interpretiert Anweisungen eines Programms, rückt nach der Abarbeitung einer Anweisung automatisch zur nächsten vor, und vieles mehr.
Register: Ein Register ist eine einfache Speicherzelle, meistens ein Maschinenwort breit (32 Bit auf 32-Bit-Systemen), und ist direkt mit der Recheneinheit verbunden, so dass darauf sehr schnell zugegriffen werden kann. Register sind die schnellste und damit gleichzeitig teuerste Art Speicher im Computer und dementsprechend wenig hat die CPU davon. Es gibt verschiedene Arten von Registern, wie zum Beispiel Datenregister, Adressregister, Stackregister, usw.
Jeder CPU-Typ hat seinen eigenen Befehlssatz (Instruction Set), der sich von einem Befehlssatz eines anderen Typs deutlich unterscheiden kann. Der Befehlssatz ist die Menge aller Maschinenbefehle, die von der CPU verstanden werden. Ähnliche Befehlssätze werden in einer Befehlssatzarchitektur (ISA, Instruction Set Architecture), zum Beispiel x86 für alle 80x86-Prozessoren, zusammengefasst.
CPUs arbeiten synchron zu einer Taktfrequenz, wobei diese sich von CPU-Typ zu CPU-Typ unterscheidet. Die Taktfrequenz (Clock Rate, Clock Frequency) beschreibt die Anzahl an Clock Cycles, die von der CPU innerhalb einer Sekunde ausgeführt werden können und wird in Hertz angegeben. Ein Clock Cycle ist die Zeit zwischen zwei Impulsen des Oszillators, der die Verarbeitungsgeschwindigkeit der CPU bestimmt. Viele CPUs verarbeiten elementare Anweisungen innerhalb eines Clock Cycles, es gibt aber auch komplexere Anweisungen, die mehrere Clock Cycles benötigen, wie zum Beispiel die Anweisungen für Multiplikation und Division. Superskalare CPUs können auch mehrere Anweisungen innerhalb eines Clock Cycles verarbeiten. Die Dauer eines Clock Cycles ist der Kehrwert der Taktfrequenz. Beispiel: Eine 2,8 Ghz CPU führt 2,8 Milliarden Clock Cycles innerhalb einer Sekunde aus, wobei ein Clock Cycle etwa 0,36 Nanosekunden dauert.
Bus: Der Bus ist ein System zur Datenübertragung zwischen mehreren Computerkomponenten.
Hauptspeicher (Arbeitsspeicher): Der Arbeitsspeicher eines Computers wird benötigt um Programme und Daten während der Ausführungszeit möglichst nah zu speichern. Seine Zugriffszeit ist wesentlich höher als bei Registern, aber deutlich niedriger als bei mechanischen Speichern wie zum Beispiel einer Festplatte. In heutigen Computern wird RAM (Random Access Memory, Speicher mit wahlfreiem Zugriff) auf Halbleitertechnik verwendet. RAM ist ein flüchtiger Speicher, das heißt, nach der Trennung von der Stromversorgung gehen alle Informationen verloren.
Ein-/Ausgabewerk: Das Eingabe-/Ausgabewerk ist die Gesamtheit aller Schnittstellen im Computer, an die Peripherie angeschlossen werden kann, die mit dem Computernutzer kommunizieren kann (Grafikkarte, Soundkarte, Maus, Tastatur, usw).
FPU (Floating Point Unit, Gleitpunkteinheit): Die FPU ist ein spezieller Prozessor zur Ausführung von Gleitpunktoperationen. Die FPU enthält eigene Gleitpunktregister, auf denen sie arbeitet. Eine Maßeinheit für heutige Computersysteme ist FLOPS, oder auch FLOP/s (Floating Point Operations Per Second), die angibt, wieviele Gleitpunktoperationen das System in einer Sekunde durchführen kann. Moderne FPUs führen FLOPS im Milliardenbereich aus.
GPU (Graphics Processing Unit, Grafikkarte): Die GPU ist ein Prozessor spezialisiert auf Verarbeitung von 3D-Grafik-Operationen, wie zum Beispiel Vertex-Transformation und Rasterung. Die GPU enthält einen eigenen Speicher, den Videospeicher (Video Memory), sowie eigene FPUs. Ältere GPUs waren unflexibel und beschränkt in ihrer Funktionalität und gingen nicht über Rasterung hinaus. Heutige GPUs übernehmen viel mehr Verarbeitungsschritte, sind programmierbar und können sogar abseits von 3D-Grafiken als vollwertige Prozessoren für mathematische Zwecke eingesetzt werden, da sie zu FLOPS im Billionenbereich fähig sind.
Bild 2.1: Aufbau eines Computers
Darüber hinaus sind noch die folgenden Komponenten von Interesse.
Cache: Der Cache ist ein Puffer-Speicher innerhalb der CPU und stellt eine Methode um Speicherzugriffe zu reduzieren. Dabei wird bei einem Speicherzugriff (egal ob Schreib- oder Lesezugriff) nicht nur die Information in den Cache geladen, die angefordert wurde, sondern ein ganzer Block (wird auch Cache Line genannt und hat typischerweise eine Länge ab 64 Bytes). Das hat den Vorteil, dass bei sequentiellen Zugriffen (die statistisch gesehen am häufigsten vorkommen), die nächste Information sich bereits im Cache befindet und nicht mehr vom Hauptspeicher, oder gar von einem mechanischen Speichermedium geladen werden muss, deren Zugriffszeit weit höher ist als die des Caches. Wenn Information angefordert wird, die nicht bereits im Cache liegt, spricht man von einem Cache Miss. Es ist oft eine Herausforderung für Programmierer, Algorithmen und Datenstrukturen möglichst cachebewusst (cachefreundlich) zu realisieren. Viele Cache Misses innerhalb einer kritischen Schleife können zu einem gravierenden Performanceverlust führen. Heutige Computer haben oft mehrere Caches für unterschiedliche Verwendung und unterschiedliche Ebenen. So gibt es oft einen L1- und einen L2-Cache, wobei der L1-Cache die bessere Zugriffszeit hat, aber weniger Information speichern kann, als der L2-Cache. Wird Information angefordert, die nicht im L1-Cache liegt, wird zuerst der L2-Cache durchsucht, der immer noch bessere Zugriffszeit hat, als beispielsweise der Hauptspeicher, bevor langsamere Speicher drankommen.
Festplatte (HDD, Hard Disc Drive): Die Festplatte ist ein mechanisches Speichermedium, das auf Magnettechnik aufbaut und beliebige Daten dauerhaft speichern kann. Die Festplatte kann gigantische Datenmengen speichern (zur Zeit gibt es bereits Festplatten mit einem Volumen von mehreren Terrabytes), die Zugriffszeit ist aber weit höher als beim Hauptspeicher, bedingt durch Grenzen der Mechanik.
Sonstige Speichermedien: Als sonstige Speichermedien kann man die CD-Laufwerke (Compact Disc), DVD-Laufwerke (Digital Versatile Disc), die Nachfolger Blu-Ray Disc und HD DVD (High Density DVD), sowie USB-Speichermedien (Universal Serial Bus) wie zum Beispiel USB-Memorysticks zusammenfassen. In der Regel ist die Zugriffszeit bei diesen Speichern deutlich höher als bei einer Festplatte, das Speichervolumen aber wesentlich niedriger.
Ein Programmierer sollte die verschiedenen Komponenten eines Computers kennen und verstehen. Schlechte und besonders zu langsame Programme zeichnen sich oft dadurch aus, dass ihr Programmierer absolut keine Ahnung von den Abläufen im Hintergrund hat.
3. Zahlensysteme, Speicher und Daten
Uns Menschen ist das Dezimalsystem als Zahlensystem vertraut. Ein Zahlensystem ist eine Methode zur Darstellung von Zahlen mit Hilfe von Regeln und Ziffern. Das Dezimalsystem ist ein positionelles Zahlensystem mit der Basis 10. Positionell heißt, dass jede Ziffer anhand ihrer Stelle in der Ziffernfolge eine andere Wertigkeit besitzt. Steht eine Ziffer also rechts (vorausgesetzt das Stellenwertsystem ist aufsteigend von rechts, was in der Regel die Konvention ist), so ist sie niederwertiger als jede Ziffer links von ihr. Den Zahlenwert findet man heraus, indem man jede Ziffer mit der Basis hoch den Stellenwert der Ziffer nimmt und die Ergebnisse addiert. Will man zum Beispiel herausfinden welchen Zahlenwert die Ziffernfolge "1234" im Dezimalsystem hat, geht man folgendermaßen vor:
Natürlich sehen wir Menschen sofort, welchen Zahlenwert eine Ziffernfolge im Dezimalsystem darstellt, das ist aber nur gewohnheitsbedingt. Würden wir uns also in einem anderen Zahlensystem heimisch fühlen, würde uns das Dezimalsystem mehr Kopfzerbrechen bereiten.
Um im Dezimalsystem rechnen zu können, muss man zehn Zustände unterscheiden können, die wiederum durch die Ziffern 0 bis 9 gekennzeichnet sind. Der Computer kennt jedoch nur zwei Zustände, folglich also auch nur ein Zahlensystem, nämlich das Dualsystem. Das Dualsystem ist ein positionelles Zahlensystem mit der Basis 2, da nur zwei Zustände verfügbar sind, wie zum Beispiel die beiden Ladezustände eines Kondensators, die man auf die beiden Ziffern 0 und 1 abbildet. Will man also den Zahlenwert 1234 aus dem obigen Beispiel im Dualsystem darstellen, sähe die Ziffernfolge folgendermaßen aus:
Ein weiteres gebräuchliches Zahlensystem, das eine wichtige Rolle spielt, ist das Hexadezimalsystem. Das Hexadezimalsystem ist ein positionelles Zahlensystem mit der Basis 16. Da man nur die Ziffern 0 bis 9 aus dem Dezimalsystem zur Verfügung hat, für das Hexadezimalsystem aber noch weitere sechs Ziffer nötig sind, verwendet man für diese die ersten sechs Buchstaben des englischen Alphabets, also A, B, C, D, E und F. Da die Basis eine Zweierpotenz ist (2^4), eignet sich das Hexadezimalsystem hervorragend um Zahlen aus dem Dualsystem kompakter darzustellen. Eine Ziffer im Hexadezimalsystem entspricht genau vier Ziffern im Dualsystem, die Umrechnung reduziert sich also auf einfaches Zusammenfassen von jeweils vier Dual-Ziffern zu einer Hexadezimal-Ziffer, bzw. auf die Expansion einer Hexadezimal-Ziffer zu vier Dual-Ziffern. Zum Beispiel kann man 10011010010 (1234) im Hexadezimalsystem als 4D2 darstellen:
Oft kann man bei einer Ziffernfolge nicht auf den ersten Blick das zugrunde liegende Zahlensystem erkennen. Zum Beispiel könnte "101" sowohl eine Dualzahl, als auch eine Dezimal-, oder Hexadezimalzahl sein. Aus diesem Grund verwendet man Suffixe für Dual- und Hexadezimalzahlen. Bei Dualzahlen hängt man ein "b" und bei Hexadezimalzahlen entsprechend ein "h" an.
Man bezeichnet eine Ziffer im Dualsystem als Bit (Binary Digit) und vier zusammengefasste Bits als Nibble. Man kann sich den Speicher als eine sehr lange Folge von Bits und Daten entsprechend als Ausschnitte aus dieser Folge vorstellen.
Bild 3.1: Daten im Speicher
Aus technischen Gründen kann man jedoch nicht auf einzelne Bits im Speicher zugreifen, sondern immer nur auf minimal ein Byte. Ein Byte ist eine Zusammenfassung von - in der Regel - 8 Bits und stellt das kleinste adressierbare Datum im Speicher dar. Jedes größere Datum ist ein ganzzahliges Mehrfaches eines Bytes.
Es ist möglich auf einzelne Bytes eines Multibyte-Datums zu zugreifen. In diesem Fall muss jedoch besondere Beachtung der Byte-Reihenfolge geschenkt werden. Die Byte-Reihenfolge (Endianness) bestimmt wie die einzelnen Bytes eines Multibyte-Datums im Speicher angeordnet sind. Es gibt zwei gebräuchliche Anordnungen, nämlich entweder kommt das Byte mit den höchstwertigen (signifikantesten) Bits zuerst, oder das Byte mit den niederwertigsten Bits. Wenn das höchstwertige Byte zuerst kommt, spricht man von Big-Endian, wenn das niederwertigste Byte zuerst kommt, spricht man entsprechend von Little-Endian. Intel-Prozessoren verwenden in der Regel Little-Endian, während zum Beispiel PowerPC-Prozessoren Big-Endian verwenden. In selteneren Fällen kommt auch das sogenannte Middle-Endian vor. Middle-Endian, oder Mixed-Endian ist, wie der Name bereits vermuten lässt, eine Mischung aus Little- und Big-Endian. Die Bedeutsamkeit der Byte-Reihenfolge kommt immer dann zu tragen, wenn auf einzelne Bytes eines Multibyte-Datums im Speicher zugegriffen wird, oder wenn ein Multibyte-Datum von einer Quelle gelesen wird und dort in einer Byte-Reihenfolge ungleich der eigenen vorliegt. Achtet der Programmierer nicht auf die Byte-Reihenfolge, obwohl erforderlich, kann es zu subtilen Fehlern im Programm kommen.
4. Logische Verknüpfungen und Schiebeoperationen
Logische Verknüpfungen und Schiebeoperationen sind Methoden zur indirekten Bitmanipulation. Logische Verknüpfungen sind Elemente der Booleschen Algebra und arbeiten auf Wahrheitswerten. Ein Wahrheitswert ist ein logischer Wert und entweder "wahr" ("true"), oder "falsch" ("false"), oder abgebildet auf Ziffern, "1" für "wahr" und "0" für "falsch". Logische Verknüpfungen sind durch Wahrheitstabellen definiert, anhand deren man das Ergebnis einer Verknüpfung ablesen kann. Es gibt vier gebräuchliche Arten von Verknüpfungen, NOT, AND, OR, und XOR.
Die NOT-Verknüpfung (Negation) negiert einfach den Wahrheitswert.
Die AND-Verknüpfung (UND-Verknüpfung) hat als Ergebnis immer dann "wahr", wenn beide Operanden den Wahrheitswert "wahr" haben.
Die OR-Verknüpfung (ODER-Verknüpfung) hat als Ergebnis immer dann "wahr", wenn mindestens einer der Operanden den Wahrheitswert "wahr" hat.
Die XOR-Verknüpfung (Exclusive OR, Ausschließendes ODER) hat als Ergebnis immer dann "wahr", wenn nur einer der Operanden den Wahrheitswert "wahr" hat.
Mit Hilfe der Verknüpfungen ist es möglich einzelne Bits eines Datums zu manipulieren, bzw. ihren Status abzufragen. Wenn man zum Beispiel will, dass das vierte Bit von 00110011 gelöscht wird, verknüpft man einfach das Datum mit 11101111 per UND-Verknüpfung und erhält laut Definition dieser Verknüpfung 00100011. Oder wenn man herausfinden will, welchen Wahrheitswert das Bit aus dem vorherigen Beispiel hat, verknüpft man das Datum mit 00010000 - wieder per UND-Verknüpfung - und wenn im Ergebnis alle Bits 0 sind (den Wahrheitswert "falsch" haben), ist das gefragte Bit 0, ansonsten entsprechend 1.
Schiebeoperationen verändern ein Datum, indem sie jedes Bit umplatzieren/verschieben und zwar immer um ganzzahlige Plätze. Es ist möglich nach links und nach rechts zu schieben, es wird jedoch nie über die Grenzen eines Datums geschoben, das heißt, das direkt angrenzende Datum wird nicht verändert. Entstehende "Leerstellen" werden in der Regel mit Nullen gefüllt, beim Rechtsschieben jedoch unter bestimmten Voraussetzungen auch mit Einsen. Ein paar Beispiele mit dem Datum 00111100:
Schiebeoperationen nach links:
01111000: um eine Stelle
11100000: um drei Stellen
00000000: um sechs oder mehr Stellen
Schiebeoperationen nach rechts:
00011110: um eine Stelle
00000011: um vier Stellen
00000000: um sechs oder mehr Stellen
Logische Verknüpfungen und Schiebeoperationen sind Low-Level-Konstrukte und können bei unvorsichtiger Programmierung zu subtilen Fehlern im Programm führen. Nichtsdestotrotz sind sie sehr effizient (für CPUs sind es elementare Operationen, die innerhalb eines Clock Cycles ausgeführt werden) und oft sehr nützlich und ihr Verständnis sollte daher zum Repertoire jedes Programmierers gehören.
5. Aufbau und Ausführung eines Programms
Programme, also Folgen von Anweisungen, können nicht ohne Weiteres von der CPU ausgeführt werden. Es müssen bestimmte Strukturen und Regeln eingehalten werden, damit die CPU ein Programm überhaupt als Programm erkennt und weiß, wie es ausgeführt werden soll.
Programme liegen immer in Dateien auf der Festplatte, oder einem vergleichbaren Speichermedium vor. Eine solche Datei nennt man Programmdatei (EXE, Executable, auch Binary und pl. Binaries). Der wesentliche Inhalt einer Programmdatei setzt sich aus mehreren Komponenten zusammen: dem Header, dem Codesegment und dem Datensegment. (Optional werden auch Ressourcen wie zum Beispiel Bitmaps in eine Programmdatei gepackt, die dann während der Ausführung verwendet werden können.) Header (Kopfdaten) sind Metadaten, also Daten, die Informationen über andere Daten enthalten und stehen in der Regel immer vor dem Datenblock, über den sie informieren. Header werden auch Dateikopf genannt, wenn es sich bei dem Datenblock um eine Datei handelt. Das Codesegment enthält die eigentlichen Anweisungen des Programms und wird auch als Programmtext bezeichnet. Das Datensegment ist ein Speicherbereich für statische Programmvariablen und teilt sich auf in ein Segment (Abschnitt) für initialisierte Variablen, die nicht mit Nullen initialisiert werden (in der Regel als ".data" bezeichnet) und ein Segment für explizit mit Nullen initialisierte und uninitialisierte Variablen (in der Regel als ".bss" bezeichnet), die implizit mit Nullen initialisiert werden.
Bild 3.1: Möglicher Aufbau einer Programmdatei
Soll nun ein Programm ausgeführt werden, wird zuerst der Header validiert (überprüft) und dann die einzelnen Segmente in den Hauptspeicher geladen. Neben dem Code- und dem Datensegment werden bei Programmstart noch der Call Stack und der Heap im Hauptspeicher eingerichtet. Der Call Stack (Execution Stack, Run-Time Stack), oder der Einfachheit halber nur Stack ist ein als Stack strukturiertes Datensegment mit fester Größe und wird während der Ausführung eines Programms zur Ablagerung von temporären Daten verwendet, wie zum Beispiel lokalen Variablen, Funktionsparametern und sonstigen zusätzlichen Informationen, die für einen korrekten Programmablauf von Bedeutung sind. Da der Stack nur über eine feste Größe verfügt, können unvorsichtig geschriebene Programme einen Überlauf herbeiführen, den Stack Overflow, was in der Regel zum sofortigen Abbruch des Programms führt. Der Heap ist ein dynamisches Datensegment und stellt die einzige Möglichkeit für Programme zur Laufzeit bei Bedarf zusätzlichen, beliebig großen Speicher zu allozieren (reservieren) und wieder freizugeben. Den Prozess der Allokation und Freigabe von Speicher nennt man Speicherverwaltung (Memory Management).
Anmerkung: Ein Stack ist eine abstrakte Datenstruktur nach dem LIFO-Prinzip (Last In - First Out, als letztes hinein - als erstes heraus). Die typischen Operationen auf einem Stack sind push (stapeln) und pop (herunternehmen), das heißt, es ist immer nur möglich entweder das "oberste" Element herunterzunehmen, oder darauf ein weiteres zu stapeln.
Wurde alles ordnungsgemäß geladen und eingerichtet, beginnt die CPU mit der Abarbeitung der Programmanweisungen. Dazu hat jedes Programm einen Entry Point (Einsprungspunkt) definiert, eine Speicheradresse der Anweisung, die den Beginn des Programms markiert, wobei diese Anweisung nicht unbedingt die erste im Programm sein muss. Die CPU "springt" nun zu dieser Speicheradresse und beginnt die Abarbeitung des Programms bei dieser Anweisung.
6. Entwicklung und Arten der Programmiersprachen
Die ersten Programme für Computer bestanden aus Nullen und Einsen, dem sogenannten Maschinencode, geschrieben in der Maschinensprache. Maschinencode wird auch als Native Code bezeichnet. Die Maschinensprache ist der Regelsatz für Programme, die von einer CPU ausgeführt werden können und enthält den Befehlssatz der zugehörigen CPU als Teilmenge. Die Maschinensprache ist die einzige Sprache, die eine CPU versteht, das heißt, alle Programme geschrieben in einer anderen Sprache müssen zuerst in die Maschinensprache übersetzt werden, bevor sie ausgeführt werden können. Man bezeichnet den Code der Maschinensprache als Maschinencode, oft gibt es aber keine Trennung zwischen Maschinensprache und Maschinencode und beide Begriffe werden synonym (bedeutungsgleich) verwendet. Die Maschinensprache ist eine Programmiersprache der ersten Generation (1GL, First-Generation Programming Language).
Anmerkung: Unterhalb der Maschinensprache gibt es noch sogenannte Mikrosprachen. Eine Mikrosprache wird beim Computerbau zur Programmierung von Hardwarekomponenten (zum Beispiel CPU) eingesetzt. Eine Maschinencode-Anweisung entspricht typischerweise mehreren bis vielen Mikrocode-Anweisungen. Ein Softwareprogrammierer wird jedoch nie mit Mikrocode in Berührung kommen.
Programmierung in Maschinensprache erwies sich als sehr schwierig und fehlerbehaftet (hatte man zum Beispiel einen Fehler gemacht, dauerte es lange diesen zu finden). Mit dem Ziel Programme besser lesbar zu machen, entwickelte man die Assemblersprachen. Eine Assemblersprache ist eine maschinennahe Programmiersprache (Low-Level Programming Language), also eine Sprache, die sich strukturell der Maschinensprache ähnelt, die Anweisungen aber menschenlesbar symbolisiert. Heutige Assemblersprachen abstrahieren viel mehr als es zu den Anfängen der Fall war und bieten sogar einige Konstrukte an, die man eher aus höheren Sprachen kennt. Die Assemblersprache ist eine Programmiersprache der zweiten Generation (2GL, Second-Generation Programming Language). Beispiele für Assemblersprachen: Pentium, Sparc.
Auf lange Sicht war man mit Assemblersprachen natürlich auch nicht zufrieden. Es gestaltete sich schwierig größere Projekte zu realisieren, Programme waren auf eine Prozessorarchitektur gebunden, usw. Mit "höheren" Programmiersprachen versuchte man nun mehr Produktivität zu erzeugen. In Anführungszeichen deshalb, weil die ersten höheren Programmiersprachen wie Fortran, Lisp und Algol im Vergleich zu heutigen, modernen Hochsprachen vergleichsweise wenig Abstraktion boten. Eine höhere Programmiersprache (High-Level Programming Language) zeichnet sich durch weitgehende Abstraktion der Computerarchitektur und den deutlichen Fokus auf Problemlösung aus. Man nennt solche Sprachen auch problemorientierte Programmiersprachen, sowie Programmiersprachen der dritten Generation (3GL, Third-Generation Programming Language). Höhere Programmiersprachen boten erstmals modulare und strukturierte Programmierung. Beispiele für höhere Programmiersprachen: Fortran, Lisp, Algol, C, C++, D, Java.
Der Text eines Programms, das in einer Programmiersprache vorliegt, nennt man auch Quelltext, Quellcode (Source Code) oder einfach nur Code.
Bei Programmiersprachen unterscheidet man in der Regel zwischen kompilierten und interpretierten Programmiersprachen. Eine kompilierte Programmiersprache (Compiled Programming Language) ist eine Sprache, deren Quelltext vor der Ausführung von einem Compiler einmalig übersetzt wird und danach immer wieder ausgeführt werden kann, ohne erneute Kompilierung. Beispiele für kompilierte Programmiersprachen: C, C++, D, Lisp, Visual Basic. Eine interpretierte Programmiersprache (Interpreted Programming Language) ist eine Sprache, deren Quelltext von einem Interpreter ausgeführt wird. Ein Interpreter ist ein Programm, das den Quelltext einer interpretierten Programmiersprache einliest, interpretiert und direkt ausführt. Beispiele für interpretierte Programmiersprachen: Java, JavaScript, Python, Ruby, Perl, PHP.
Manche Programmiersprachen werden auch als Skriptsprachen bezeichnet. Eine Skriptsprache (Scripting Language) ist eine Programmiersprache, die vornehmlich für kleine, überschaubare Aufgaben gedacht ist und auf komplexe Sprachelemente vollwertiger Programmiersprachen verzichten. Skriptsprachen eignen sich besonders zur Kontrolle von Programmen, die in kompilierten Sprachen geschrieben wurden, bzw. deren Komponenten. Da Skriptsprachen in der Regel interpretierte Sprachen sind, gewinnen kompilierte Programme auf diese Weise an Flexibilität, was sonst schwieriger zu erzielen gewesen wäre. Man bezeichnet den Quelltext einer Skriptsprache auch als Skript. Interpretierte Programmiersprachen sind nicht zwingend auch Skriptsprachen, auch wenn sie als solche verwendet werden können. Python ist zum Beispiel keine Skriptsprache, während JavaScript und PHP es sind.
Bei höheren Programmiersprachen gibt es noch die Gliederung in Programmierparadigmen. Ein Programmierparadigma ist der fundamentale Programmierstil, den ein Programmierer anwenden kann. Es gibt heute viele Programmierparadigmen, die sich sowohl in ihren Konzepten, als auch der Abstraktionstiefe unterscheiden. Heutige Programmiersprachen unterstützen oft mehrere Programmierparadigmen, eine solche Sprache bezeichnet man dann als Multiparadigmen-Programmiersprache (Multi-Paradigm Programming Language). Die zwei wichtigsten Programmierparadigmen sind Imperative Programmierung und Objektorientierte Programmierung. Imperative Programmierung sieht ein Programm als eine Folge von elementaren Anweisungen an, die sequentiell abgearbeitet werden. Alle Assemblersprachen sind imperative Sprachen. Höhere Sprachen sind oft imperativ, oder unterstützen Imperative Programmierung, allerdings nicht alle. Beispiele für imperative Programmiersprachen: C, C++, D, Pascal, Perl, FORTRAN, COBOL. Objektorientierte Programmierung betrachtet Programme als Wechselspiel zwischen Objekten, wobei Objekte Klasseninstanzen sind. Beispiele für objektorientierte Sprachen: C++, D, Java, Smalltalk.
7. Aufbau einer Programmiersprache, Codeübersetzung und Compiler
Eine Programmiersprache wird definiert durch eine Menge an Schlüsselwörtern, die Syntax und die Semantik. Die formale Beschreibung dieser Komponenten wird auch als Programmiersprachenspezifikation bezeichnet (Programming Language Specification), oder einfach Spezifikation. Schlüsselwörter (Keywords) sind reservierte Bezeichner, die das Vokabular einer Programmiersprache darstellen. Die Syntax ist eine Menge an Regeln, die vorschreiben wie die Schlüsselwörter angeordnet werden dürfen und ist effektiv die Grammatik einer Programmiersprache. Ein Satz kann grammatikalisch richtig sein, aber dennoch keinen Sinn ergeben. "Die Kuh fliegt durch das Wasser" wäre grammatikalisch korrekt, ergibt aber keinen Sinn. Die Semantik ist eine Menge an Regeln, die über die Bedeutung einer Anordnung aussagt. Der Satz "Der Fisch schwimmt im Wasser" ist beispielsweise sowohl grammatikalisch (syntaktisch), als auch semantisch korrekt. Damit ein Programm übersetzt werden kann, muss es zuerst die syntaktischen und danach die semantischen Analysen durchlaufen. Trotz all der Überprüfung moderner Compiler auf Korrektheit des Programms, können sich Fehler, sogenannte Bugs einschleichen, die sich erst zur Laufzeit entweder als unbeabsichtigte Fehlfunktionen, oder erzwungene Programmabbrüche durch das Betriebssystem, sogenannte Crashes, verursacht beispielsweise durch Speicherzugriffsverletzungen, offenbaren. Den Prozess des Auffindens eines solchen Bugs nennt man Debugging. In der Regel steht ein Programm zur Verfügung, das den Programmierer beim Debugging unterstützt. Solche Programme nennt man Debugger.
Alle Programme, die nicht in der Maschinensprache geschrieben wurden, müssen vor der Ausführung durch die CPU in die Maschinensprache übersetzt werden. Diesen Übersetzungsprozess nennt man Kompilierung oder Übersetzung (Compiling, Compilation) und Programme, die von einer Sprache in die andere übersetzen sind sogenannte Compiler. Man bezeichnet die Zeit, in der ein Compiler den Quelltext übersetzt, als Übersetzungszeit (Compile Time) und die Zeit, in der ein Programm von der CPU ausgeführt wird - ob direkt, oder indirekt bei interpretierten Sprachen ist irrelevant (bedeutungslos) - als Laufzeit (Run Time). Compiler müssen nicht unbedingt direkt in die Maschinensprache übersetzen, es reicht bereits wenn in die nächstuntere Sprache übersetzt wird, wie zum Beispiel - in den Anfängen von C++ - von C++ zu C, oder von C zu einer Assemblersprache. Die Entscheidung einen Compiler zu entwickeln, der nicht direkt in die Maschinensprache übersetzt hat die Vorteile, dass sich seine Entwicklung deutlich einfacher gestalten lässt, und, dass es sich in der Zielsprache oft besser optimieren lässt. Compiler, die direkt in die Maschinensprache übersetzen, übersetzen normalerweise in die Maschinensprache des Hosts, das heißt des Computers, auf dem die Übersetzung stattfindet. Es ist aber möglich, dass Compiler in eine andere Maschinensprache oder für ein anderes System (zum Beispiel Betriebssystem) übersetzen. Solche Compiler bezeichnet man als Cross Compiler und den Prozess der Übersetzung entsprechend als Cross Compiling oder Cross Compilation.
Anmerkung: Es ist in vielen Fällen nicht möglich den Prozess der Übersetzung rückgängig zu machen, das heißt, aus dem Ergebnis einer Übersetzung wieder den ursprünglichen Quelltext zu gewinnen. In manchen Fällen ist es aber möglich zumindest den ungefähren Quelltext zu bekommen, oder im Fall von Maschinencode, den Assemblercode. Man bezeichnet Programme, die aus Maschinencode Assemblercode generieren als Disassembler und den Prozess der Rekonstruktion der Quellelemente (zum Beispiel des Quelltextes) eines fertigen Systems, oder Produktes (zum Beispiel eines im Maschinencode vorliegenden Programms) als Reverse Engineering.
Interpretierte Programmiersprachen implementieren oft Compiler, die in den sogenannten Bytecode übersetzen. Bytecode (Zwischencode) ist eine besondere Form von übersetztem Code, bei der die Anweisungen nicht in einer Maschinensprache, sondern in einer virtuellen Maschinensprache vorliegen. Eine virtuelle Maschine ist ein Interpreter, der eine virtuelle Maschinensprache bereitstellt und Bytecode ausführt, der in dieser virtuellen Maschinensprache vorliegt. Eine virtuelle Maschine stellt sozusagen eine Softwareimplementierung einer echten, physikalischen Maschine (CPU) dar. Eine virtuelle Maschinensprache ist die Maschinensprache einer virtuellen Maschine. Weil Bytecode plattformunabhängig ist und bereits viele Schritte hinter sich hat, die sonst bei einer vollständigen Interpretierung des Quelltextes anfallen würden, stellt es eine effektive Methode zur Optimierung der Laufzeit von interpretierten Programmen dar.
Eine andere Art von Compiler sind die JIT-Compiler. Ein JIT-Compiler (Just-In-Time Compiler) ist ein Compiler, der (die am öftesten ausgeführten) Teile eines Programms oder das gesamte Programm erst bei Ausführung in die Maschinensprache übersetzt, das Ergebnis im Hauptspeicher lagert und ausführt. Die JIT-Kompilierung (JIT-Compilation) ist eine weitere Methode zur Laufzeitoptimierung von interpretierten Programmen und wird oft zur Steigerung der Effektivität in Kombination mit Bytecode verwendet.
8. Daten abstrakt: Variablen
Zu Zeiten von Maschinensprachen waren Daten als Speicherbereiche, die mit ihren Adressen assoziiert (verknüpft) waren, sehr maschinennah ausgelegt und schwer zu handhaben. Mit den Assemblersprachen wurde erstmals das Konzept der Variablen eingeführt. Eine Variable ist ein Bereich im Speicher, der durch einen Bezeichner symbolisiert wird. Ein Bezeichner (Identifier) ist ein symbolischer und eindeutig kennzeichnender Name. Man spricht von einer Bindung eines Bezeichners an eine Variable.
Variablen werden je nach Größe des zugrunde liegenden Speicherbereichs und der Art der enthaltenen Information bestimmten Typen zugeordnet. Die Trennung nach Typ nennt man auch Typisierung (Typing), oder Klassifikation. Man unterscheidet außerdem zwischen primitiven, sogenannten Basis-, oder Elementartypen und den zusammengesetzten Typen. Es gibt einen speziellen Datentyp, void, der den unspezifizierten Typ darstellt. Der Datentyp void (null, nichts, ungültig) wird in höheren Programmiersprachen verwendet, wenn eine Typangabe keine Rolle spielt, oder es keine Daten gibt, mit denen ein Typ assoziiert werden kann.
Höhere Programmiersprachen verfügen über ein sogenanntes Typsystem. Ein Typsystem (Type System) ist eine Menge an Regeln, die die Typsicherheit (Type Safety) sicherstellen, das heißt sie verhindern, dass einer Variable ein typunverträglicher Wert zugewiesen wird. Bei kompilierten Sprachen wird die Typsicherheit bereits während der Übersetzung sichergestellt. Man spricht dabei von einer statischen Typprüfung (Static Type Checking). Entgegengesetzt spricht man von dynamischer Typprüfung (Dynamic Type Checking), wenn die meiste, oder die gesamte Typprüfung zur Laufzeit stattfindet. Variablen in dynamisch typisierten Programmiersprachen sind in der Regel nicht an einen Datentyp gebunden und können während der Laufzeit Werte unterschiedlicher Typen annehmen. Man unterscheidet zudem zwischen starker und schwacher Typisierung. Starke Typisierung (Strong Typing) erfordert, dass Operationen mit unverträglichen Typen nicht ausgeführt werden, während schwache Typisierung (Weak Typing) solche nach festgelegten Regeln zulässt. Programmiersprachen mit dynamischer Typisierung sind in der Regel auch schwach typisiert.
Basistypen (Primitive Data Types) sind Typen, die direkt von der Programmiersprache unterstützt werden. In der Regel sind es auch Typen, die von der CPU, oder einem Koprozessor in einer Operation verarbeitet werden können. Da sie immer nur einen Wert zur gleichen Zeit beinhalten können, bezeichnet man sie auch als Skalare (Scalars) Dazu gehören numerische Typen wie Ganz- und Gleitpunktzahlen, der literarische Typ Zeichen, der Boolesche Typ, sowie die Adresstypen Zeiger und Referenzen.
Eine Ganzzahl ist die einfachste Art Zahl, die ein Computer kennt. Wie der Name bereits andeutet, können immer nur ganze Zahlen (... -2, -1, 0, 1, 2 ...) dargestellt werden. Der Wertebereich hängt dabei von der Größe des zugewiesenen Speichers ab. Bei einem Byte und 8 Bit pro Byte, kann entweder der Wertebereich von 0 bis 255, oder von -128 bis 127 dargestellt werden, wobei beim letzteren Wertebereich ein Bit benötigt wird, um die Information zu speichern, ob die Zahl negativ (das Bit hat den Wert 1), oder positiv (das Bit hat den Wert 0) ist, deswegen sind auch die Unter- und Obergrenzen -128 und 127, statt etwa -255 und 255, da nur noch 7 Bit zur Verfügung stehen. Es gilt die Regel: Je mehr Speicherplatz für die Darstellung einer Ganzzahl zur Verfügung steht, desto höhere Ganzzahlen können dargestellt werden. Bei 4 Byte (insgesamt also 32 Bit) wären die möglichen Wertebereiche bereits 0 bis 2^32 und -(2^31) bis (2^31)-1, also im Milliardenbereich. Beim Versuch, einer Ganzzahlvariable einen Wert zu zuweisen, der den darstellbaren Wertebereich der Variable verlässt, geht Information verloren und man spricht von einem Überlauf (Overflow).
Solange das Ergebnis einer Rechenoperation den darstellbaren Wertebereich nicht verlässt, sind Multiplikation, Addition und Subtraktion auf ganzen Zahlen zufriedenstellend. Probleme tauchen aber bei der Division auf, da eventuelle Nachkommastellen nicht in einer Ganzzahl gespeichert werden können und verworfen werden (es wird nicht gerundet). Falls Nachkommastellen beim Endergebnis keine Rolle spielen, kann dieses Verhalten auch zufriedenstellen. Wenn jedoch mit dem "beschnittenen" Ergebnis weitergerechnet wird, kann es unter Umständen schnell zu unerwünscht großen Abweichungen kommen, die das Endergebnis gravierend verfälschen. Um dieses Problem zu lösen, nutzt man oft einen Trick um die Fehler bei längeren Berechnungen möglichst klein zu halten. Dazu werden die Zahlenwerte mit einem Faktor multipliziert, die Rechenoperationen mit den skalierten Zahlenwerten durchgeführt und das Ergebnis schließlich durch den gleichen Faktor dividiert. Man nutzt hierbei in der Regel Zweierpotenzen als Skalierungsfaktoren, da Multiplikationen und Divisionen mit diesen im Binärsystem als Schiebeoperationen nach links und rechts umgesetzt werden können. Dieses Rechenverfahren wird als Festpunktarithmetik (Fixed-Point Arithmetic) bezeichnet, da das (gedachte) Dezimaltrennzeichen immer an gleicher Stelle bleibt. Als FPUs noch nicht in Computern verbaut und Gleitpunktoperationen in Software implementiert und dementsprechend langsam waren, wurde die Festpunktarithmetik überwiegend eingesetzt. Heute dauern Gleitpunktoperationen dank Hardwareunterstützung in der Regel nicht länger als eine Ganzzahloperation, und Festpunktarithmetik wird nur noch in besonderen Fällen eingesetzt, zum Beispiel wenn keine FPU vorhanden ist, wie es etwa bei eingebetteten Systemen der Fall ist.
Eine Gleitpunktzahl ist eine Methode zur näherungsweisen Darstellung reeller Zahlen im Computer. Es gibt viele Gleitpunktformate, darunter das am häufigsten verwendete IEEE-Format (Institute of Electrical and Electronics Engineers). Im wesentlichen funktioniert die Gleitpunktarithmetik aber nach dem selben Prinzip. Eine Gleitpunktzahl besteht aus vier Informationen, dem Vorzeichen, der Mantisse, dem Exponent und der Basis. Die Basis ist jedoch innerhalb eines Formats immer konstant und kann weggelassen werden. Das Vorzeichen belegt immer ein Bit, wieviel die Mantisse und der Exponent belegen, entscheidet das Format und die Genauigkeit. Es gibt zwei gebräuchliche Genauigkeiten, die einfache Genauigkeit und die doppelte Genauigkeit, wobei die einfache 4 Byte (32 Bit) und die doppelte entsprechend 8 Byte (64 Bit) verwenden. Die beiden Genauigkeiten unterscheiden sich - wie der Name bereits vermuten lässt - in der Genauigkeit (also der Anzahl der Nachkommastellen), sowie in dem Wertebereich der darstellbaren Zahlen. Die Zahl berechnet sich dann aus der Formel Vorzeichen*Mantisse*Basis^Exponent, wobei die Basis bei IEEE 2 und das Vorzeichen hier entweder 1, oder -1 ist.
Der Vorteil von Gleitpunktzahlen gegenüber Festpunktzahlen ist offenbar die deutlich erhöhte Präzision und der viel größere Wertebereich bei gleichem Speicherplatzverbrauch. In den meisten Fällen reicht die Gleitpunktarithmetik auch absolut aus, in einigen Fällen jedoch nicht. Zum einen sind Gleitpunktzahlen alles andere als mathematisch genau, da zur Darstellung nur das Binärsystem verwendet werden kann. Es können also weit nicht alle reellen Zahlen aus dem möglichen Wertebereich abgebildet werden, es entstehen also Lücken und damit Fehler. Zum anderen funktioniert Gleitpunktarithmetik in einigen Fällen nicht, wie zum Beispiel bei der Subtraktion von zwei fast gleich großen bis gleich großen Zahlen. Wenn man die Problemstellen jedoch kennt und beachtet, kann man die Fehler in akzeptablen Grenzen halten.
Ein Zeichen (Character) ist ein Index in eine Zeichenkodierung (Character Encoding). Die zwei gebräuchlichsten Zeichenkodierungen sind ASCII (American Standard Code for Information Interchange) und UTF (Unicode Transformation Format). ASCII ist eine historische Kodierung und verwendet pro Zeichen 1 Byte und davon 7 Bit, was den Zeichensatz auf maximal 128 verschiedene Zeichen reduziert. Von den 128 Zeichen reserviert ASCII wiederum die ersten 32 Zeichen für sogenannte Steuerzeichen (Control Character), darunter auch Whitespace Character wie etwa das Leerzeichen, die keine Zeichen darstellen, sondern der Steuerung dienen. Die restlichen Zeichen sind darstellbare Zeichen, darunter das englische Alphabet, sowohl in Klein-, als auch Großschrift, die Ziffern 0 bis 9, Rechenoperationszeichen und einige Symbole. Es gibt auch ASCII-Kodierungen, die alle 8 Bits verwenden, jedoch beschränkt sich der Zeichensatz immernoch auf maximal 256 Zeichen inklusive Steuerzeichen. Der Unicode-Standard verwendet bis zu 32 Bit pro Zeichen (gebräuchlich sind 16 Bit, also 2 Byte) und unterstützt heute mehr als 100000 Zeichen, darunter so gut wie jedes Zeichen aus den gebräuchlichsten Sprachen der Welt und ersetzt ASCII kontinuierlich mit den modernen Programmiersprachen.
Ein Boolescher Datentyp (Boolean Data Type) ist ein Element der Booleschen Algebra (Boolean Algebra) und kann immer nur einen von zwei Wahrheitswerten annehmen, entweder "wahr" ("true"), oder "falsch" ("false"). Boolesche Variablen benötigen folglich nur ein Bit um die Information zu speichern, aus technischen Gründen wird jedoch in der Regel mindestens ein Byte verwendet, wobei "falsch" dann auf 0 und "wahr" auf alle Zahlen ungleich 0 abgebildet werden.
Ein Zeiger (Pointer) ist ein Datentyp, der die Speicheradresse anderer Variablen als Wert enthalten und damit effektiv auf diese "zeigen", bzw. diese referenzieren kann. Es können auch Zeiger auf Zeiger zeigen, da Zeiger wiederum selbst adressierbare Variablen im Speicher sind. Den Zugriff auf den Wert der Variable, deren Adresse ein Zeiger enthält, nennt man Dereferenzierung. Zeiger sind ein Low-Level-Konstrukt und erlauben sehr effiziente Programmierung. Bei fehlender Erfahrung und unvorsichtiger Programmierung, können jedoch sehr subtile Bugs auftreten, weshalb einige moderne Programmiersprachen, wie zum Beispiel Java, erst gar keine Zeiger anbieten, um das Fehlerrisiko zu minimieren. In höheren Programmiersprachen werden Zeiger mit Typen assoziiert, das heißt, ein Zeiger hat immer den Typ "Zeiger auf den referenzierten Typ". Zeigt ein Zeiger also zum Beispiel auf eine Ganzzahl, so hat der Zeiger den Typ "Zeiger auf eine Ganzzahl". Da die Typinformation nur eine zusätzliche Information ist und für Speicheradressen in erster Linie irrelevant ist, lassen sich Zeiger beliebig zu anderen Zeigertypen konvertieren. Es ist sogar möglich einen Zeiger zu dem Typ void zu konvertieren, in diesem Fall ist eine Dereferenzierung jedoch nicht mehr möglich, da void ein ungültiger Typ ist und es muss zuerst in einen validen Typ konvertiert werden. Eine besondere Art Zeiger sind die sogenannten Nullzeiger (Null Pointer). Diese Zeiger haben als Wert oft eine 0, oder einen anderen Wert, der sie als Nullzeiger kennzeichnet und werden benötigt um über einen Zeiger auszusagen, dass dieser keinen sinnvollen Speicherbereich referenziert. Ist ein Zeiger kein void- und kein Nullzeiger, so ist es möglich die sogenannte Zeigerarithmetik anzuwenden. Zeigerarithmetik (Pointer Arithmetic) bezeichnet die Technik den Wert eines Zeigers, also eine Speicheradresse, um einen festen Wert zu inkrementieren, bzw. zu dekrementieren, nämlich um die Größe des vom Zeiger referenzierten Datentyps. Referenziert ein Zeiger zum Beispiel eine 4-Byte-Ganzzahl-Variable, würde sich die Speicheradresse dieser Variable um genau 4 Byte erhöhen, wenn eine 1 zum Zeiger addiert wird und um 4 Byte verringern, wenn eine 1 vom Zeiger subtrahiert wird. Bei einer ganzen Zahl n, würde sich die Speicheradresse entsprechend entweder um n*4 Byte verringern, oder um n*4 Byte erhöhen, bei n = 0 würde sich natürlich nichts ändern.
Eine Referenz (Reference) ist eine besonderere Art Zeiger, deren Wert nicht manipuliert werden kann, zum Beispiel durch Zeigerarithmetik. Referenzen sind entweder Nullzeiger, oder zeigen auf eine gültige Adresse, sind also sozusagen sichere Zeiger. Es ist zwar möglich auch Referenzen in einen ungültigen Zustand zu versetzen, so dass ein Zugriff auf die referenzierte Adresse zu einem Fehler führt, dies ist jedoch in der Regel nicht trivial (einfach).
Zusammengesetzte Typen (Composite Data Types) werden aus den Basistypen mit Hilfe von Werkzeugen, die Programmiersprachen anbieten, beliebig komplex konstruiert. Da sie Daten in einer bestimmten Art und Weise angeordnet speichern und verknüpfen, werden sie auch als Datenstrukturen bezeichnet. Dazu gehören die Zeichenkette, das Feld, die klassische Struktur und in objektorientierten Programmiersprachen die Klasse.
Ein Feld (Array) ist ein sequentieller Container für Daten eines festen Typs. Dabei hat jedes Feld-Element einen eindeutigen Index, über den auf das jeweilige Element zugegriffen werden kann. Besondere Formen von Feldern sind sogenannte Multidimensionale Felder, also Felder, deren Elemente über zwei oder mehr Indizes (Koordinaten) angesprochen werden.
Eine Zeichenkette (String) entspricht weitgehend, besonders in älteren Programmiersprachen wie C, einem Feld aus Zeichen. In modernen Programmiersprachen mit spracheigener Stringunterstützung, ist der Unterschied jedoch viel größer.
Eine Struktur (Structure, auch Record oder Tuple genannt) ist ein sequentieller Container für Daten beliebigen Typs, wobei die Elemente einer Struktur über symbolische Bezeichner, anstatt über numerische Indizes wie bei einem Feld, zugegriffen werden.
Eine Klasse (Class) ist das grundlegende Element der Objektorientierten Programmierung. Die Klasse kann wie eine Struktur Daten beliebigen Typs beinhalten, sogenannte Attribute, und darüber hinaus Methoden, die auf diesen Attributen angewendet werden.
Wenn ein Objekt zur Laufzeit erstellt, einer Variable zugewiesen, als Parameter an eine Funktion übergeben, sowie als Ergebnis zurückgegeben werden kann, bezeichnet man es als First-Class-Objekt. Alle Basistypen sind in der Regel First-Class-Objekte.
Variablen sind immer verknüpft mit Gültigkeitsbereichen. Der Gültigkeitsbereich (Scope) ist ein Kontext im Programmcode, der einen logischen Zusammenhang umschließt. Wird innerhalb eines Gültigkeitsbereiches eine Variable definiert (also konstruiert), wird sie beim Verlassen dieses Gültigkeitsbereiches automatisch wieder zerstört. Gültigkeitsbereiche können beliebig komplex in einander verschachtelt sein. Der äußerste Gültigkeitsbereich wird als globaler Gültigkeitsbereich bezeichnet, entsprechend bezeichnet man auch alle dort definierten Variablen als globale Variablen. Variablen, die nicht global definiert werden, sind sogenannte lokale Variablen.
Gültigkeitsbereiche isolieren Information, indem sie die Bindung eines Bezeichners an eine Variable auf bestimmte Bereiche beschränken. So sind zum Beispiel alle Variablendefinitionen innerhalb eines Gültigkeitsbereiches auch nur dort sichtbar, oder in jedem weiteren darin verschachtelten Gültigkeitsbereich, jedoch nicht außerhalb. Dieser Umstand ermöglicht die Wiederverwendung von konventionellen Bezeichnern, wie zum Beispiel des Bezeichners "i" für Indexvariablen. Haben zwei Variablen a und b den gleichen Bezeichner, "c", und ist a im Gültigkeitsbereich A und b im Gültigkeitsbereich B definiert, wobei B innerhalb von A liegt, so spricht man davon, dass b in B die Variable a aus A verdeckt. Mit welcher Variable nun der Bezeichner "c" innerhalb von B assoziiert wird, hängt davon ab, ob entweder statische oder dynamische Gültigkeitsbereiche unterstützt werden.
Ein statischer Gültigkeitsbereich (Static Scoping) bedeutet, dass immer die Variablen im aktuellen (innersten) Gültigkeitsbereich bevorzugt werden. Damit würde b in B a aus A verdecken und erst nach dem Verlassen von B, wäre a wieder mit "c" assoziiert. Da man bei statischen Gültigkeitsbereichen die Bindung eines Bezeichners an eine Variable bereits beim Analysieren des Quelltextes genau bestimmen kann, bezeichnet man statische Gültigkeitsbereiche auch als lexikalische Gültigkeitsbereiche (Lexical Scoping). Viele Programmiersprachen, wie zum Beispiel C und C++ unterstützen nur statische Gültigkeitsbereiche.
Ein dynamischer Gültigkeitsbereich (Dynamic Scoping) bedeutet, dass die Bindung eines Bezeichners an eine Variable erst zur Laufzeit stattfindet. Bei dynamischen Gültigkeitsbereichen werden Bezeichnern oft eigene Bindungsstacks (Stacks of Bindings) zugewiesen, auf denen eine Referenz jeder neu konstruierten Variable mit diesem Bezeichner abgelegt wird und bei ihrer Zerstörung wieder heruntergenommen. Der Bezeichner wird somit immer an die Variable gebunden, die durch das oberste Element auf dem Stack referenziert wird. Bei dynamischen Gültigkeitsbereichen lässt sich die Bindung eines Bezeichners an eine Variable nicht unbedingt aus dem Quelltext erkennen.
Beispiel 8.1: Ein möglicher Bindungsstack
Gültigkeitsbereiche werden in der Regel durch Blöcke gebildet. Ein Block ist ein zusammengefasster Codeabschnitt und wird entweder in zwei sprachspezifischen Schlüsselwörtern eingeschlossen, wie im folgenden Beispiel, oder auf eine andere Weise gekennzeichnet, wie zum Beispiel durch Einrückung.
Beispiel 8.2: Variablenbindung bei statischen Gültigkeitsbereichen
Globale Variablen werden im Datensegment des Programms abgelegt, lokale Variablen während der Laufzeit auf dem Stack. In Verbindung mit der Möglichkeit Variablen auch auf dem Heap abzulegen, ergibt sich die Lagerklassifikation. Die Lagerklasse einer Variable bestimmt ihren Speicherort und ist immer eine der folgenden drei.
statisch (static): Alle Variablen, die im Datensegment des Programms abgelegt werden. Globale Variablen haben implizit diese Lagerklasse. Einigen Programmiersprachen unterstützen lokal definierte Variablen mit der statischen Lagerklasse. Solche Variablen existieren auch nach dem Verlassen ihres Gültigkeitsbereiches, sind jedoch dann nicht sichtbar, sondern erst wenn der Gültigkeitsbereich wieder betreten wird.
dynamisch (dynamic): Alle Variablen, die auf dem dynamischen Speicher, dem Heap abgelegt werden.
automatisch (automatic): Alle Variablen, die auf dem Stack zur Laufzeit abgelegt werden. Standard für alle lokalen Variablen, deren Lagerklasse nicht explizit angegeben wurde.