Programmiersprachen

In der Datenverarbeitung haben sich viele Programmiersprachen mehr oder weniger durchgesetzt; manche Sprachen, die vor zehn Jahren noch sehr modern waren, wie APL oder FORTRAN, müssen sich jetzt gegen "modernere" Sprachen behaupten, wie z.B. Pascal oder C (die auch schon gut 30 Jahre alt sind). Vielen Sprachen liegen ähnliche Konzepte zugrunde, da letztlich eine Umsetzung der Programmiersprache auf die Maschinensprache des Computers erfolgen soll. Man kann in diesem Zusammenhang zwischen drei Klassen von Programmiersprachen unterscheiden:

Wir werden in diesem Skript hauptsächlich imperative (prozedurale) Programmiersprachen untersuchen. Von ihnen unterscheiden sich die objektorientierten Sprachen, z.B. Smalltalk, die logischen Sprachen (Prolog) und die funktionalen Programmiersprachen wie Lisp oder Miranda. Fast alle Programmiersprachen basieren jedoch auf dem Grundgerüst der lexikalischen und der syntaktischen Analyse, die in den nächsten Abschnitten eingehend beschrieben werden.

Maschinensprache

Die Zentraleinheit (=CPU / Central Processing Unit) des Computers führt die eigentlichen Rechenvorgänge aus. Nun ist es so, daß die CPU nur sehr einfach strukturierte Befehle ausführen kann. Die Befehle sind als Bitmuster im Hauptspeicher abgelegt. Jeder Befehl umfaßt ein oder mehrere Bytes. Ein solches Bitmuster, das die CPU als Befehl erkennt und ausführt, wird Maschinenbefehl genannt. Die Gesamtheit aller Maschinenbefehle, die die CPU überhaupt ausführen kann, wird als Maschinensprache bezeichnet.

Die Maschinensprache hängt in aller Regel vom Computer bzw. der Zentraleinheit ab, für die sie geschaffen wurde. Eine Ausnahme stellen die Maschinensprachen der sogenannten Prozessorfamilien dar. Bei ihnen kann ein modernerer und weiter entwickelter Prozessor die Befehle seines Vorgängers unverändert ausführen. Man spricht in diesem Zusammenhang von Codekompatibilität. Typische Beispiele für solche Prozessoren sind die Intel-80x86-Familie oder die Motorola-680x0-Familie.

Eine Befehlsfolge in Maschinensprache auf dem weit verbreiteten 8086 (PC-Welt) sieht z.B. so aus:
     90
B81000
BB0500
F6E3

Diese Befehlsfolge wurde bereits in für menschliche Leser besser verständliche Hexadezimalcodes umgesetzt. Beachten Sie, daß hier Zeilenwechsel eingeführt wurden, um die Befehle voneinander zu trennen. Befehle der Maschinensprache können unterschiedliches Befehlsformat haben, so daß manche Befehle nur ein Byte benötigen, andere hingegen mehrere. Je nach Befehl wertet die CPU die an den eigentlichen Befehlscode (Op-Code) anschließenden Bytes aus.

Programmieren in Maschinensprache ist eine unbequeme und extrem fehleranfällige Tätigkeit. Bereits in der Frühzeit des Computers wurde daher die Assemblersprache entwickelt.

 Assembler

Assembler ist die älteste Programmiersprache überhaupt. Sie wurde erfunden, um sich die lästige Eingabe der Maschinensprache zu erleichtern. Assembler ist eine sehr schlichte Sprache, die die Maschinenbefehle direkt in eine für den Menschen lesbare Form abbildet. Assembler hat auch heute noch im Betriebssystembau und in der Programmierung einfacher Computer Bedeutung. Durch die der CPU angepaßten Strukturierung erfordert das Programmieren in Assembler eine sehr klare Vorstellung von den Vorgängen im speziellen Computer. Selbst einfache Aufgabenstellungen können in Assembler beträchtlichen Umfang annehmen, da alle Teilschritte einer Rechnung in geeignete, elementare Befehle umgesetzt werden müssen - darin unterscheidet er sich in keiner Weise von der Maschinensprache.

Das vorige Beispiel in Maschinensprache sieht - etwas lesbarer - in 8086-Assembler so aus:
     NOP
MOV AX,10H
MOV BX,5
MUL BL

Die Unzulänglichkeiten der ersten Assembler, die nach wie vor die direkte Verwendung von Zahlenwerten für Adressen, Operanden usw. erforderte, führte dazu, daß sogenannte Makro-Assembler geschaffen wurden. Ihr wesentliches Merkmal sind die mehr oder weniger komfortable Definition symbolischer Bezeichnungen für Speicherobjekte und die Makrofähigkeit. Ein Makro ist ein Textbaustein, der Assemblerbefehle und Makroaufrufe enthält. Makros sind parametrisierbar. Im Quellprogramm führt der Makroaufruf dazu, daß die aktuellen Parameter des Makros in die Befehle des Makrorumpfes eingesetzt werden, sowie zur Erzeugung der in der Makrodefinition festgelegten Assemblerbefehlsfolge. Diese Makroexpansion findet vor dem eigentlichen Übersetzungslauf statt. Das Ergebnis dieser durch Textersetzung erzeugten Programmquelle wird wie gewohnt assembliert. Makros haben also keine Ausprägung im lauffähigen Code, im Gegensatz zu Unterprogrammaufrufen. Ein Makro zum Retten einer Anzahl Register auf den Stack sieht (im 8086 TASM) z.B. so aus:
     SAVE_REGS MACRO regs
IRP REG,<regs>
PUSH REG
ENDM
ENDM

Aufgerufen wird dieser Makro mit der Liste von Registern, die zu retten sind:
    SAVE_REGS AX,BX,CX

Dadurch wird folgender Assembler-Quellcode generiert:
     PUSH AX
PUSH BX
PUSH CX

Als Programmiersprache, die einen Überblick über die Arbeitstechniken mit dem Computer geben soll, ist Assembler denkbar ungeeignet: Sie erfordert Detailkenntnisse über den Computer, sie verführt zu schlechtem Programmierstil, und sie ist schwer zu erlernen. Da sie noch dazu von der jeweiligen CPU abhängig ist, schränkt sie den Horizont unnötig ein. Bald nach der Erfindung des Assemblers wurden - nicht zuletzt aus diesen Gründen - die ersten sogenannten höheren Programmiersprachen entwickelt.

 Höhere Programmiersprachen

Der Wunsch nach bequemer und sicherer Programmierung war ausschlaggebend für den Entwurf der höheren Programmiersprachen. Darüberhinaus konnten nun Programme von einem Computer auf einen anderen übertragen werden, ohne daß sie in der jeweiligen Assemblersprache neu entworfen werden mußten. Damit war erstmalig eine weitgehende Unabhängigkeit vom Rechner selbst möglich. Dieses Portieren gestattete erst die weite Verbreitung gleicher Programme auf den unterschiedlichsten Rechnern. Höhere Programmiersprachen werden oft auch als problemorientierte Sprachen bezeichnet, da viele von ihnen für bestimmte Aufgabengebiete geschaffen wurden:
     COBOL     kaufmännisch: COmmon Business Oriented Language
FORTRAN     technisch-wissenschaftlich: FORmula TRANslator
ALGOL     technisch-wissenschaftlich: ALGOrithmic Language
Pascal     Programmierausbildung

Die Folgerung allerdings, daß z.B. die Sprache COBOL zum Lösen einer spezifischen kaufmännischen Fragestellung die am besten geeignete sei, ist nicht zwingend.

Der Programmentwickler verwendet den Computer so, als wäre er eine abstrakte Maschine (z.B. Pascal-Maschine), die eine bestimmte höhere Programmiersprache versteht und ihre Befehle ausführen kann. Die Entwicklung des Programms findet jetzt ausschließlich auf der Ebene der höheren Programmiersprache statt.

Höhere Programmiersprachen können nicht mehr direkt von der CPU ausgeführt werden. Um aus einem Programm (geschrieben in der höheren Programmiersprache) ein Maschinenprogramm zu erzeugen, ist wieder ein Programm nötig: der Übersetzer.

Es gibt zwei grundlegend verschiedene Übersetzer-Bauarten: Compiler und Interpreter. Für manche Anwendungen können Mischformen nützlich sein, die einen Compiler für die Übersetzung zu einem Zwischencode und einen Interpreter für den Zwischencode zur Verfügung stellen.

In verschiedenen höheren Programmiersprachen sind Makros zugelassen, die durch einen Präcompiler expandiert werden. Typischer Vertreter ist die Sprache C. Alle Programmzeilen, die mit einem '#' beginnen, sind Anweisungen an den Präprozessor.

Weitere Beispiele für Präprozessoren sind:
     SQL-Präprozessor für Datenbankfunktionen in C-Programmen (ORACLE)
RATFOR für FORTRAN
COLUMBUS für COBOL und Assembler

Sowohl beim Assembler als auch bei höheren Programmiersprachen wird für das Fertigstellen des lauffähigen Codes in der Regel ein Bindeprogramm (Binder, Linker) benötigt. Gewöhnlich wird bei der Übersetzung nur ein Objektcode erzeugt. Ein Objektcode ist für sich nicht lauffähig, sondern muß erst durch den Binder in eine für das Betriebssystem des Zielrechners geeignete Form (lauffähiger Code, Executable Code) gebracht werden. Der Ablauf der Übersetzung vom Quellprogramm bis hin zum lauffähigen Code ist detailliert im Kapitel 4 beschrieben.

 Eigenschaften höherer Programmiersprachen

Höhere Programmiersprachen gestatten das Verfassen von Ausdrücken, die z.B. der üblichen Aufschreibung von Formeln ähnlich ist. Darüberhinaus stellen sie in der Regel prozedurale Konzepte sowie Blockorientierung, Kontrollstrukturen und lokale Variablen zur Verfügung. Betrachten wir die Eigenschaften im einzelnen!

 Typkonzept

Höhere Programmiersprachen verfügen im allgemeinen über ein Typkonzept. Dieses Konzept kann so einfach sein wie in FORTRAN, das nur wenige Grundtypen und Arrays zuläßt, oder so ausgeprägt wie in Modula oder Ada. Je nachdem, wie Zuweisungen zwischen unterschiedlichen Typen zulässig sind, spricht man von strong typing (Modula, Ada) oder weak typing (FORTRAN, C). In der Regel kennt jede höhere Programmiersprache das Konzept der impliziten Typaufweitung, das z.B. die Zuweisung eines Ganzzahl-Wertes an eine Real-Variable zuläßt. Darüberhinaus gibt es explizite Typanpassungen, wie z.B. das Runden einer Gleitpunktzahl zu einer Ganzzahl. Daneben ist auch noch der Typecast zu finden, der oft speicherflächenorientiert arbeitet, nicht immer aber den Inhalt der beteiligten Operanden gemäß einer immer algorithmisch sinnvollen Regel konvertiert. Typecasting ist z.B. in der Sprache C zu finden, wo es sowohl explizit als auch implizit stattfinden kann (und eine Fehlerquelle darstellt).

Ziele des Typkonzeptes sind bessere Lesbarkeit der Programmquelle und die Prüfung auf Typkonformität bei Operationen. Insbesondere die Typprüfung durch den Compiler verhindert bereits zur Übersetzungszeit Fehler, die zur Laufzeit nur schwer auffindbar wären. Sprachen mit strong typing sind deshalb Sprachen mit weak typing vorzuziehen.

 Ausdrücke und Wertzuweisungen

Anders als in Assembler können bei einem Ausdruck mehrere Operanden und Operationen beteiligt sein. In Pascal ist z.B. ohne weiteres eine Zuweisung wie die folgende zulässig:
    y := sin(x) + 4 * (x - a);

Geeignete Typen der Operanden sind vorausgesetzt. Man erkennt, daß Funktionsaufrufe (sin), arithmetische Operatoren (wie z.B. +) und Operanden in einem Ausdruck vorkommen können.

Weiterhin ist eine implizite und eine explizite Vorrangreihenfolge der Operationen gegeben. So binden der Funktionsaufruf und die runden Klammern stärker als die Operatoren '*' und '+'. In Pascal wurde, wie in fast allen höheren Programmiersprachen, die mathematische "Punkt-vor-Strich"-Regel berücksichtigt. Neben den Vorrangregeln für mathematische Operationen gibt es in vielen Programmiersprachen weitere Vorrangregeln, was die Behandlung anderer Operationen, z.B. für Zeiger, betrifft.

 Kontrollstrukturen: Verzweigungen und Schleifen

Fast alle höheren Programmiersprachen stellen folgende Kontrollstrukturen zur Verfügung:

Ziel der Kontrollstrukturen ist die Verwirklichung des Blockkonzeptes, das für jeden Ablauf genau einen Eingang und genau einen Ausgang fordert. Werden in einer höheren Programmiersprache die goto-Anweisung und äquivalente Konstruktionen (vgl. return in C) nicht zur Verfügung gestellt, dann wird die blockstrukturierte Programmierung erzwungen.

 Prozeduren und Funktionen

Prozeduren und Funktionen, auch Unterprogramme oder Subroutines genannt, dienen zum Zerlegen eines Programms in funktionell zusammengehörige Anweisungsfolgen. Vorteile des Konzeptes sind die mehrfache Verwendbarkeit desselben Codes an verschiedenen Stellen des Programms, ohne den eigentlichen Programmcode unnötig aufzublähen, sowie die Universalität, die in der Regel durch die Parametrisierung sichergestellt wird.

 Lokale Gültigkeit von Objekten

Objekte können innerhalb eines Blockes oder einer Prozedur (Funktion) lokal vereinbart werden. Solche Objekte sind z.B. Variable oder Prozeduren. Nicht alle Programmiersprachen lassen alle Arten von Objekten als lokal zu. So kennt zwar Pascal lokale Funktionen, während C sie verbietet. Alle lokalen Objekte unterliegen dem Konzept der lokalen Gültigkeit. Der Effekt, daß ein Objekt eines äußeren Blockes in einem weiter innen liegenden Block nicht gültig ist, falls im inneren Block ein Objekt gleichen Namens existiert, wird als Verschattung bezeichnet.

Die Vorteile der lokalen Gültigkeit sind insbesondere:

Gegeben sei das nachstehende Pascal-Programm. In den Spalten auf der rechten Seite ist jeweils markiert, welche Variable an welcher Stelle Gültigkeit hat.

***Bild/Tabelle***

 Cross-Übersetzung

Eine Besonderheit stellt die Cross-Übersetzung dar. Ein Cross-Übersetzer setzt ein Quellprogramm in einen Zielcode um, der auf einem anderen Rechner ablauffähig ist als auf dem Rechner, auf dem der Cross-Übersetzer läuft. Cross-Übersetzer werden häufig bei der Entwicklung von Softwaresystemen auf Einplatinen-Computern eingesetzt, da solche einfachen Computer in der Regel keine komfortable Entwicklungsumgebung zur Verfügung stellen. Ein weiteres wichtiges Einsatzgebiet ist das Entwickeln von Programmen für Computer, die noch nicht als konkrete Hardware verfügbar sind. Durch die Cross-Entwicklung und eine geeignete Simulation des Zielrechners kann man bereits frühzeitig Fehler im Zielrechner erkennen und beseitigen. Die Marktakzeptanz eines Rechners ist in hohem Maße von der für ihn verfügbaren Software abhängig. Durch die Cross-Entwicklung lassen sich die Entwicklungszeiten verkürzen: Parallel zur Hardware-Entwicklung kann die notwendige Softwarebereitstellung erfolgen.
weiter im Text


Stand: 19.11.2002 /
 HPs Home      Compiler/Home