Compiler

Die Anweisungen, die man in den Sprache C oder C++ schreibt, können von dem Prozessor eines Computers nicht direkt verstanden werden. Bevor die Anweisungen ausgeführt werden können, muss der gesamte Code (der sogenannte Sourcecode) in ein lauffähiges Programm (das sogenannte Binary) übersetzt werden. Die Sprachen C und C++ verwenden hierfür einen Compiler, welcher den gesamten Sourcecode auf einmal in ein vollständiges, lauffähiges Programm umwandelt, welches danach direkt auf dem Computer abgearbeitet werden kann.

Details

Ein Prozessor versteht nur eine Sprache, die sogenannte Maschinensprache, welche für jeden Prozessor unterschiedlich sein kann. Die Programmiersprache Assembler definiert für jede Maschinensprache (welche grundsätzlich für den Menschen unverständlich ist) eine mehr oder weniger lesbare Umsetzung der einzelnen Befehle, doch für jeden Prozessortyp existiert somit ein eigener Assembler.

Da ein Programmierer nicht für jeden neuen Prozessortyp sein Programm umschreiben möchte, wurden sogenannte Hochsprachen entwickelt, welche über allen Assemblern stehen und den Programmcode auf verschiedene Art und Weise automatisch in den gewünschten Assembler und schlussendlich in die Maschinensprache umwandeln. Man unterscheidet grob zwischen zwei Hochsprachentypen: Interpreter und Compiler. Ein in den letzten Jahren zusätzlich in Mode gekommener Typ ist die virtuelle Maschine, welche jedoch genau genommen nur eine Kombination der beiden erstgenannten ist. Alle drei Typen sind heute in verschiedenen Sprachen in Gebrauch.

Bei einer Interpreter-Sprache werden die einzelnen Anweisungen eine nach der anderen während der Laufzeit interpretiert und direkt umgewandelt. Der Vorteil hiervon ist, dass das Programm direkt gestartet werden kann, ohne dass der original-Programmtext vom Anwender in irgendeiner Weise verändert oder umgewandelt werden muss. Der Nachteil jedoch ist eine verminderte Geschwindigkeit sowie das wiederholte Übersetzen des Programmcodes bei jedem Start des Programmes. Wichtige Beispiele für Interpretersprachen sind BASIC, Perl, PHP, Javascript sowie grundsätzlich sämtliche Skriptsprachen.

Eine Compiler-Sprache übersetzt den kompletten Programmcode ein Mal und kann danach beliebig oft ohne erneute Compilierung gestartet werden. Der Vorteil hiervon ist, dass das Programm sehr schnell ist und zudem eigenständig arbeitet. Der Nachteil ist jedoch, dass die Compilierung viel Zeit kostet und das schlussendliche Resultat ohne neu-Compilierung grundsätzlich nur auf dem Prozessortyp läuft, für den compiliert wurde. Wichtige Beispiele sind C und Pascal.

Eine virtuelle Maschine schlussendlich ist eine Mischform der obengenannten Typen. Eine solche Maschine bezeichnet einen künstlichen Prozessor, der nicht als Hardware vorhanden, sondern nur softwaremässig ausprogrammiert ist. Der Vorteil hierbei ist, dass dieser künstliche Prozessor auf jedem Computer ausprogrammiert werden kann und somit sämtliche Programme dieser virtuellen Maschine auf jedem beliebigen Computer direkt abgearbeitet werden können. Die Programmcodes werden hierbei mittels eines Compilers in den sogenannten Bytecode umgewandelt, welcher das direkte Pendant zum Maschinencode eines Prozessors ist. Bei der Abarbeitung des Programmes schlussendlich läuft der virtuelle Prozessor im Hintergrund und interpretiert diesen Bytecode Stück für Stück und wandelt ihn in die eigentliche Maschinensprache um. Wichtige Beispiele für virtuelle Maschinen sind Java und C# (sprich: C-Sharp).

Die Sprachen C und C++ werden mit Compilern in Maschinencode umgewandelt. Während einige eingefleischte Programmierer auf ältere Compiler aus früheren Jahren schwören, sind heutzutags besonders zwei Compiler in der breiten Masse vertreten: Der C++ Compiler des Visual Studios von Microsoft und der GNU C Compiler GCC. Der GCC ist auf den gängigen Systemen verfügbar und zudem gratis, wohingegen der Compiler von Microsoft in der Vollversion nur über das Visual Studio Developer Packet für Windows erhältlich ist.

Wenngleich alle existierenden Compiler grundsätzlich die Aufgabe haben, Sourcecode in ein lauffähiges Programm umzuwandeln, unterscheiden sie sich doch teils markant in den Details. Beim Umsteigen von dem einen auf den anderen Compiler können Fehler angezeigt werden, die ursprünglich nicht erkannt wurden. In einigen Fällen ist es sogar möglich, das ein Programm nicht mehr richtig läuft. Insbesondere, wenn ein Programm auf verschiedenen Systemen zum Laufen gebracht werden soll, wird man auf die eine oder andere Überraschung stossen. Die Fülle an Unterschieden kann hier nicht aufgezählt werden, doch jeder, der sich an ein sogenanntes Cross-Compiling heranwagt, wird einige Überraschungen erleben und vieles über die Sprachen C und C++ lernen.

Die Übersetzung von C und C++

Die Übersetzung des Sourcecodes in ein Binary geschieht grob über drei Schritte: Preprocessing, Compiling, Linking. Hierbei ist zu beachten, dass ein Programm in C oder C++ häufig nicht nur aus einer einzelnen Datei besteht, sondern aus mehreren. Die Vorverarbeitung (das Preprocessing) und das eigentliche Übersetzen (Compilieren) wird für jede Datei einzeln eingeleitet. Erst durch das Zusammenfügen (Linken) werden die Teile zu einem einzigen lauffähigen Programm. Hier ein kurzer Durchgang durch eine vollständige Übersetzung eines Programmes:

Eine Datei, die in C oder C++ geschrieben ist, beinhaltet normalerweise Angaben wie beispielsweise Kommentare oder Leerzeichen, die der Programmierer für sich in den Programmtext eingefügt hat, die jedoch der Compiler nicht zu wissen braucht. Solche rein unterstützenden Angaben werden vom Preprozessor aus dem Quellcode herausgelöscht. Des weiteren hat der Programmierer mittels des Preprozessors die Möglichkeit, andere Dateien einzubinden, Makros zu definieren, sowie bedingte Compilierung und Fehlermeldungen zu steuern. Eine umfassende Erklärung der Möglichkeiten der Steuerung des Preprozessors kann in der Sektion Preprozessor nachgelesen werden.

Die daraus entstehende, vorverarbeitete (preprocessed) Datei wird sodann compiliert. Diese Übersetzung kann in mehrere Schritte aufgeteilt werden: Zuerst werden sämtliche Angaben über Typen, Variablen, Funktionen, Klassen... usw, sowie der gesamte Aufbau der Programmierung zusammengetragen und in einer internen Struktur des Compilers gespeichert. Diese Phase nennt man das Parsen des Programmcodes. Während dieser ersten Phase wird der Code auf korrekte Syntax, also korrekte Anordnung der einzelnen Teile geprüft.

Danach erfolgt eine Konsistenzprüfung, welche überprüft, ob sämtliche angesprochenen Symbole definiert wurden, ob die Typen korrekt gesetzt sind, dass keine Schutzverletzungen auftreten... usw. Hat der Compiler bis hier keinen Fehler gefunden, so beginnt er mit der Übersetzung der einzelnen Anweisungen in Assemblercode, Stück für Stück. Danach folgt auf Wunsch eine Optimierung des Codes, womit sowohl Platzverbrauch und insbesondere auch Geschwindigkeit des schlussendlichen Programmes verbessert werden. Ganz zum Schluss übernimmt ein Assembler die Übersetzung in Maschinencode.

Das Resultat der Übersetzung einer einzelnen Datei wird in eine sogenannte Objektdatei gespeichert. Mittels des Linkers werden sämtliche Objektdateien nun zu einem kompletten Programm zusammengefügt. Der Linker überprüft hierbei für jede Objektdatei, welche externen Definitionen noch fehlen. Findet der Linker die Definition in keiner vorhandenen Objektdatei, so entsteht ein Linker-Fehler und das Programm kann nicht fertiggestellt werden. Sind jedoch alle Angaben komplett vorhanden, so wird der Linker eine einzelne Datei herstellen, die sämtliche übersetzten Teile enthält, die korrekt miteinander verbunden wurden und somit ein lauffähiges Programm darstellen.

Schlussbemerkung

Der konkrete Ablauf der Kompilierung spielt beim Programmieren selten eine Rolle, kann jedoch bis ins kleinste Detail konfiguriert werden. Spezifische Einstellungen für die einzelnen Teile der Übersetzung sind jedoch für die eigentliche Programmierarbeit normalerweise sekundär. Im alltäglichen Programmieren wird man höchstens einige Preprozessor-Anweisungen schreiben, ein paar (viele) Compilerfehler ausbügeln müssen und ein paar wenige Linker-Fehler erwischen.