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
#ifndef LOGGING_HPP
#define LOGGING_HPP
#ifndef LOGTARGET
#define LOGTARGET std::cout
#endif // LOGTARGET
#ifdef NDEBUG
#define TRACE(msg, file, line)
#else
#define TRACE(msg, file, line) LOGTARGET << "[TRACE] " << \
msg << " (at " << file << ":" << line << ")\n"
#endif //NDEBUG
#define ENTER(fkt) TRACE("ENTER " fkt, __FILE__, __LINE__)
#define EXIT(fkt) TRACE("EXIT " fkt, __FILE__, __LINE__)
#define CALL(fktcall) TRACE("CALL " #fktcall, __FILE__, __LINE__); fktcall;
#endif // LOGGING_HPP
test.cpp
#include <iostream>
#include "logging.hpp"
void test() {
ENTER("test");
std::cout << "Testing\n";
EXIT("test");
}
int main(int argc, char * argv[]) {
ENTER("main(int,char*[])");
std::cout << "Hello world!\n";
CALL(test());
EXIT("main(int,char*[])");
}
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 auftrueund bindet ggf. den folgenden Code ein.Beispiel:
#if VERSION > 123- prüft die (ggf. von außen definierte) VariableVERSIONauf> 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:
#ifdef _LINUX void doTheLinuxThing() { // ... Linux-spezifischer Code } #endif -
#elif-#if, das nur ausgeführt wird, wenn ein vorheriges#if/#eliffehlgeschlagen ist ("else if"). Wird häufig verwendet, um Ketten von Prüfungen durchzuführen und dann im abschließenden#elseeinen Standardfall anzugebenBeispiel:
#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#ifnicht 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:
#ifdef __cplusplus extern "C" { #endif // Deklaration von C-Funktionen #ifdef __cplusplus } #endifIn 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.