Mehr Präprozessor
Der Präprozessor ist nicht nur dazu da, Dateien zusammenzukopieren und an den Compiler zu übergeben. Er stellt noch eine ganze Menge zusätzlicher Möglichkeiten bereit, Ersetzungen im Code vorzunehmen.
Video
Code
Als Demonstration für einige Fähigkeiten des Präprozessors dienen die beiden Dateien logging.hpp
und test.cpp
logging.hpp
|
|
test.cpp
|
|
Erklärung
Die Möglichkeiten des Präprozessors, den Code zu modifizieren, bevor er zum Compiler geht, lassen sich grob in verschiedene Kategorien einteilen.
Bedingte Compilierung
Unter Umständen kann es notwendig sein, Code von der Übersetzung durch den Compiler auszuschließen.
Das können bspw. plattformabhängige Stellen im Code sein, wenn Code auf mehrere Betriebssysteme
portiert werden soll. Dazu stellt der Präprozessor die Anweisungen #if
, #ifdef
und #ifndef
zur Verfügung. Diese können bestimmte Bedingungen prüfen und abhängig davon Code-Abschnitte ein-
und ausblenden. Abschnitte beginnen immer nach der entsprechenden Präprozessor-Direktive und enden am
nächsten #elif
, #else
oder #endif
-
#if
- Prüft den angegebenen Ausdruck auftrue
und bindet ggf. den folgenden Code ein.Beispiel:
#if VERSION > 123
- prüft die (ggf. von außen definierte) VariableVERSION
auf> 123
. Derartiger Code findet häufig Verwendung, um Anpassungen an bestimmte Versionen externer Bibliotheken vorzunehmen. -
#ifdef/#ifndef
- prüft ob das angegebene Präprozessorsymbol definiert ist. Wird häufig als sogenannter Include-Guard (um ein mehrfaches Einbinden desselben Headers zu vermeiden) oder für betriebssystemabhängigen Code (oft in Verbindung mit#elif
) verwendet.Beispiel:
1 2 3 4 5 6
#ifdef _LINUX void doTheLinuxThing() { // ... Linux-spezifischer Code } #endif
-
#elif
-#if
, das nur ausgeführt wird, wenn ein vorheriges#if/#elif
fehlgeschlagen ist (“else if”). Wird häufig verwendet, um Ketten von Prüfungen durchzuführen und dann im abschließenden#else
einen Standardfall anzugebenBeispiel:
1 2 3 4 5 6 7
#if defined(_LINUX) // .. Linux-spezifischer Code #elif defined (_WINDOWS) // .. Windows-spezifischer Code #else // Fehlermeldung oder generischer Standardcode #endif
-
#else
- Unbedingter Alternativzweig. Wird immer dann ausgeführt, wenn das vorhergehende#if
nicht betreten wurde. -
#endif
- Abschluss eines#if/#ifdef/#ifndef
-Blocks
Symbole definieren: #define
/#undef
Die Befehle #define
und #undef
definieren Präprozessorsymbole oder löschen deren Definition. #define
nimmt optional
zusätzlich zum Symbolnamen einen Wert, den dieses Symbol annehmen soll. Wird das Symbol dann irgendwo im Quelltext verwendet,
so wird dieser Wert (ggf. rekursiv, falls im Wert wiederum Präprozessorsymbole verwendet werden) aufgelöst.
Dateien einbinden
Externe Dateien können mit #include
eingebunden werden. Dabei findet eine (ggf. rekursive) Ersetzung
der #include
-Anweisung durch den Dateiinhalt statt. Die beiden Formen mit <...>
und "..."
unterscheiden
sich in der Quelle, aus der die Header eingebunden werden. <...>
bindet Systemheader ein. Hierbei ist nicht
genauer vorgegeben, wo und wie diese vorliegen. Meist finden sich diese in fest konfigurierten Pfaden in der
Compiler-Installation. Quasi alle Kompiler erlauben die Erweiterung des Systemsuchpfades durch Kommandozeilenparameter
(bspw. zur Einbindung zusätzlicher Bibliotheksheader). "..."
bindet Dateien aus lokalen Pfaden ein. Der übergebene
Pfad wird hierbei immer ausgehend von der gerade kompilierten Datei aufgelöst. Bei rekursiven Einbindungen wird
ebenso ab dem Pfad der einbindenden Datei gesucht (bspw. relevant, wenn ein Bibliotheks-Header wiederum andere
Header relativ einbindet).
Vordefinierte Symbole
Compiler können bestimmte Symbole vordefinieren, die vom Präprozessor genutzt werden können, um Code einzubinden etc.
Standardmäßig vordefinierte Symbole sind:
-
__LINE__
und__FILE__
- Die aktuelle Zeile und Datei, in der die Symbole stehen. Wird gern genutzt zur Ausgabe von Debug-Meldungen, um den Programmierer gleich auf die richtige Code-Stelle zu verweisen. -
__cplusplus
- Wird vom Compiler definiert, wenn er als C++-Compiler läuft. Wird manchmal benötigt, um Header-Dateien zu konstruieren, wie in C und C++ parallel verwendet werden können.Beispiel:
1 2 3 4 5 6 7
#ifdef __cplusplus extern "C" { #endif // Deklaration von C-Funktionen #ifdef __cplusplus } #endif
In diesem Beispiel werden im C++-Modul die C-Funktionen in einen
extern "C" { }
-Block eingefügt, so dass der Compiler Name Mangleing abschaltet und die C-Variante des Stacklayouts bei Funktionsaufrufen verwendet. Das ermöglicht die Einbindung von in C geschriebenen Bibliotheken in ein C++-Programm. -
__DATE__
und__TIME__
- Das aktuelle Datum und die aktuelle Uhrzeit während des Builds. Wird gern als Metadatum genutzt, um später feststellen zu können, wann genau ein bestimmter Code gebaut wurde (bspw. als Versionsnummer)
Quasi alle relevanten Compiler und Bibliotheken definieren eine Fülle zusätzlicher Symbole vom aktuell verwendeten C++-Standard bis zu existierenden Bibliotheken, um eine Anpassung des eigenen Codes an die Umgebung zu ermöglichen.