Präprozessor, Compiler und Linker¶
Ein klassischer C-Compiler besteht aus drei Teilen: Einem Präprozessor, dem eigentlichen Compiler, und einem Linker:
- Der Präprozessor bereitet einerseits den Quellcode vor (entfernt beispielsweise Kommentare und Leerzeilen); andererseits kann er mittels der im nächsten Abschnitt näher beschriebenen Präprozessor-Anweisungen Ersetzungen im Quellcode vornehmen.
- Der Compiler analysiert den Quellcode auf lexikalische oder syntaktische
Fehler, nimmt gegebenenfalls Optimierungen vor und wandelt schließlich die
aufbereiteten Quellcode-Dateien in binäre Objekt-Dateien (Endung:
.o
) um. - Der Linker ergänzt die Objekt-Dateien um verwendete Bibliotheken und setzt die einzelnen Komponenten zu einem ausführbaren Gesamt-Programm zusammen.
Präprozessor-Anweisungen¶
Der Präprozessor lässt sich im Wesentlichen durch zwei Anweisungen steuern, die
jeweils durch ein Hash-Symbol #
zu Beginn der Anweisung gekennzeichnet sind
und ohne einen Strichpunkt abgeschlossen werden:
#include
– Einbinden von Header-Dateien¶
Mittels #include
können weitere Quellcode-Teile in das Programm
integriert werden. Diese Dateien werden vom Präprozessor eingelesen und an
Stelle der #include
-Anweisung in die Datei geschrieben.
Unterschieden wird bei #include
-Anweisungen zwischen Bibliotheken, die sich
in einem Standardpfad im System befinden und dem Compiler bekannt sind, und
lokalen Header-Dateien, die sich üblicherweise im gleichen
Verzeichnis befinden. Die Bibliotheken aus dem Standard-Pfad erhalten um ihren
Namen eckige Klammern, die Namen der lokalen Header-Dateien werden in doppelte
Anführungszeichen gesetzt:
// Standard-Bibliothek stdio.h importieren:
#include <stdio.h>
// Lokale Header-Datei input.h importieren:
#include "input.h"
#define
– Definition von Konstanten und Makros¶
Mittels #define
können Konstanten oder Makros definiert werden. Bei der
Definition einer Konstanten wird zunächst der zu ersetzende Name anschließend
der zugehörige Wert angegeben:
#define HALLO "Hallo Welt!"
#define PI 3.1415
Eine Großschreibung der Konstantennamen ist nicht zwingend nötig, ist in der Praxis jedoch zum Standard geworden, um Konstanten- von Variablennamen unterscheiden zu können. Nicht verwendet werden dürfen allerdings folgende Konstanten, die im Präprozessor bereits vordefiniert sind:
__LINE__
: Ganzzahl-Wert der aktuellen Zeilennummer__FILE__
: Zeichenkette mit dem Namen der kompilierten Datei__DATE__
: Zeichenkette mit aktuellem Datum__TIME__
: Zeichenkette mit aktueller Uhrzeit
Eine Festlegung mittels #define
bleibt allgemein bis zum Ende der
Quelldatei bestehen. Soll eine erneute Definition einer Konstanten NAME
erfolgen, so muss die bestehende Definition erst mittels #undef NAME
rückgängig gemacht werden.
Bei der Definition eines Makros mittels #define
wird zunächst der Name des
Makros angegeben. In runden Klammern stehen dann, wie bei der Definition einer
Funktion, die Argumente, die das Makro beim Aufruf
erwartet.[1] Unmittelbar anschließend wird der Code angegeben, den das Makro
ausführen soll.
#define QUADRAT(x) ((x)*(x))
Bei der Definition von Makros muss beachtet werden, dass der Präprozessor die
Ersetzungen nicht wie ein Taschenrechner oder Interpreter, sondern wie ein
klassischer Text-Editor vornimmt. Steht im Quellcode beispielsweise die Zeile
result = QUADRAT(n)
, so wird diese durch den Präprozessor gemäß dem obigen
Makro zu result = ((n)*(n))
erweitert. In diesem Fall erscheinen die
Klammern als unnötig. Steht allerdings im Quellcode die Zeile result =
QUADRAT(n+1)
, so wird diese mit Hilfe der Klammern zu ((n+1)*(n+1))
erweitert. Ohne die zusätzlichen Klammern in der Makro-Definition würde der
Ausdruck zu n+1*n+1
erweitert werden, was ein falsches Ergebnis liefern
würde.
Innerhalb von Makro-Definitionen kann ein spezieller Operatoren verwendet
werden: Der Operator #
kann auf einen Argumentnamen angewendet werden und
setzt den Namen der konkret angegebenen Variablen in doppelte Anführungszeichen:[2]
#define QUADRAT(x) print("Der Quadrat-Wert von %s ist %i.\n", #x, (x)*(x))
Ein Minimalbeispiel für dieses Makro könnte folgendermaßen aussehen:
// Datei: makro-beispiel-1
// Compilieren: gcc -o makro-beispiel-1 makro-beispiel-1
// Aufruf: ./makro-beispiel-1
// Ergebnis beim Aufruf: Der Quadrat-Wert von num ist 121.
#include <stdio.h>
#define QUADRAT(x) printf("Der Quadrat-Wert von %s ist %i.\n", #x, (x)*(x))
void main()
{
int num=11;
QUADRAT(num);
}
Ist eine #define
-Anweisung zu lange für eine einzelne Code-Zeile, so kann
die Anweisung an einer Whitespace-Stelle mittels \
unterbrochen und in der
nächsten Zeile fortgesetzt werden. Eventuelle Einrückungen (Leerzeichen,
Tabulatoren) werden dabei vom Präprozessor automatisch entfernt.
Ein entscheidender Vorteil von #define
-Anweisungen ist, dass so definierte
Konstanten oder Makros an beliebigen Stellen im Code eingesetzt werden können
und zugleich bei Bedarf nur an einer einzigen Stelle im Programm geändert
werden müssen.
#if
, #ifdef
, #ifndef
– Bedingte Compilierung¶
Mittels #if
, #ifdef
oder #ifndef
können Teile einer Datei zur
„bedingten Compilierung“ vorgemerkt werden. Ein solcher Code-Teil wird nur
dann vom Compiler berücksichtigt, wenn die angegebene Bedingung erfüllt ist.
Beispielsweise kann auf diese verhindert werden, dass Header-Dateien oder
Quellcode-Bibliotheken mehrfach geladen werden. Beispielsweise kann man in
einer Header-Datei input.h
gleich zu Beginn prüfen, ob eine Konstante
INPUT_H
definiert ist. Falls nicht, so kann wird der folgende Code
berücksichtigt, wobei darin auch die Konstante INPUT_H
mit dem Wert 1
definiert wird:
// Datei: input.h
#ifndef INPUT_H
#define INPUT_H = 1
// ... eigentlicher Inhalt ...
//#endif
Die Variable INPUT_H
ist nur beim ersten Versuch, die Datei mittels
#include
zu importieren, nicht definiert. Ein mehrfaches Importieren wird
somit verhindert. Ebenso kann beispielsweise mittels #ifdef DEBUG
ein
Code-Teil nur zu Testzwecken eingefügt werden (der durch eine Zeile #define
DEBUG 1
am Beginn der Datei aktiviert wird). Es kann auch ein Teil eines
Codes nur in Abhängigkeit von einer Versionsnummer ausgeführt werden,
indem beispielsweise #if VERSION < 1.0
geprüft wird.
Ob weitere Präprozessor-Anweisungen vom Compiler unterstützt werden, hängt von dessen Version und vom konkreten Betriebsystem ab. Üblicherweise werden daher nur die oben genannten Anweisungen verwendet.
Compiler-Optionen¶
Der Standard-C-Compiler kann mit einer Vielzahl an Optionen aufgerufen werden,
mit denen der Compilier-Ablauf gesteuert werden kann. Möchte man beispielsweise
lediglich überprüfen, welche Ersetzungen vom Präprozessor vorgenommen wurden,
aber den Quellcode nicht kompilieren, so kann die Option -E
verwendet
werden:
gcc -E -o mycode.i mycode.c
In diesem Beispiel wird die Ausgabe, die der Präprozessor bei der Verarbeitung
der Datei mycode.c
erzeugt, in die Datei mycode.i
geschrieben. Mit der
Option -o
(„output“) wird bei gcc
allgemein der Name der Ausgabedatei
angegeben.
Verlinken von Bibliotheken¶
Jeder Compiler bringt mehrere so genannte Bibliotheken („Libraries“) mit sich. Diese enthalten fertige Funktionen in bereits compilierter Form, die von anderen C-Programmen genutzt werden können. Der Linker sucht die benötigten Funktionen aus den Bibliotheken heraus und fügt sie dem zu compilierenden Programm hinzu.
Anmerkungen:
[1] | Zu beachten ist, dass bei der Definition eines Makros kein Leerzeichen zwischen dem Makronamen und der öffnenden runden Klammer der Argumentenliste vorkommen darf. Der Präprozessor würde ansonsten den Makronamen als Namen einer Konstanten interpretieren und den geamten Rest der Zeile als Wert dieser Konstanten interpretieren. |
[2] | Zudem können mit dem zweiten möglichen Makro-Operator ## die Namen
von zwei oder mehreren übergebenen Argumenten zu einer neuen Bezeichnung
verbunden werden. Dieser Operator wird allerdings nur sehr selten
eingesetzt. |