Einleitung
include-Direktive
define-Direktive
undef-Direktive
ifdef-, ifndef- und endif-Direktive
if-, elif-, else und endif-Direktive
defined-Direktive und vordefinierte Symbole
Weitere Präprozessor-Direktiven
Präprozessor-Direktiven sind Direktiven, die vor dem eigentlichen Übersetzen des Programms durch den Präprozessor ausgeführt werden. Alle Zeilen die mit dem Zeichen '#' beginnen werden als Präprozessor-Direktive interpretiert. Vor dem Symbol '#' dürfen beliebig viele Leerzeichen und Tabulatoren stehen. Nach dem Symbol '#' folgt die eigentlichen Präprozessor-Direktive. Und auch hier gilt: zwischen dem Symbol '#' und der Direktive dürfen wieder beliebig viele Leerzeichen bzw. Tabulatoren stehen. Direktiven werden nicht mit einem Semikolon abgeschlossen.
Sehen wir uns die erste, Ihnen bereits bekannte, Präprozessor-Direktive #include einmal näher an. Es gibt zwei Arten von Include-Direktiven:
| #include <DATEI> #include "DATEI" |
Die erste Form der Include-Direktive sucht die angegebene Datei in einem voreingestellten Pfad. Dieser Pfad wird in der Regel durch die Entwicklungsumgebung (IDE = Integrated Development Environment) und/oder einer Environment-Variable (z.B. INCLUDE_DIR) festgelegt. Header-Dateien die mit dem Compiler ausgeliefert werden, wie z.B. die Datei cmath oder auch iostream, werden in der Regel durch diese Include-Form eingebunden.
Die zweite Form hingegen sucht die angegebene Datei zuerst in dem Verzeichnis, in dem sich die Datei befindet, die die Include-Direktive enthält. Wird die einzubindende Datei dort nicht gefunden, wird wieder im voreingestellten Include-Pfad nach der Datei gesucht. Diese Form wird hauptsächlich für eigene einzubindende Dateien verwendet.
|
// Suche nur im Standard-Include-Pfad #include <iostream> // Suche im akt. Verzeichnis und im dann im Standard-Include-Pfad #include "common.h" // Suche in einem relativen Pfad #include "../include/myfile.h" |
Beide Include-Formen lassen sowohl absolute oder relative Pfadangaben zu. Beachten Sie hierbei aber bitte, dass in der Pfadangabe nur einfache Backslash-Zeichen stehen, wenn Sie für die Pfadangabe schon Backslash-Zeichen verwenden wollen. Pfadangaben in Include-Anweisungen funktionieren auch immer mit Slash-Zeichen anstelle von Backslash-Zeichen, so wie im Beispiel oben!
Die nächste Präprozessor-Direktive #define definiert ein Symbol oder Makro und hat folgende Syntax:
| #define SYMBOL #define SYMBOL Konstante #define MAKRO Ausdruck |
Für SYMBOL bzw. MAKRO können Sie fast beliebige Namen einsetzen, jedoch sollte (oder besser darf) der Namen nicht mit einem oder zwei Underscore anfangen und als ersten Buchstaben einen Großbuchstaben besitzen. Die define-Direktive #define _MySymbol sollte also besser nicht verwendet werden. Schreiben Sie stattdessen #define MySymbol. Symbole die mit einem oder zwei Underscore und anschließendem Großbuchstaben beginnen sind eigentlich für Hersteller von Bibliotheken oder Compiler reserviert. Außerdem sind die Namen für die Symbole bzw. Makros case-sensitiv, d.h. es wird nach Groß-/Kleinschreibung unterschieden.
|
//Definition des Symbols COMMON_H #define COMMON_H // Definition des Symbols DEBUG #define DEBUG |
Die zweite Form definiert ebenfalls ein Symbol für einen Wert. Diese Form sollte aber nur noch in Ausnahmefällen eingesetzt werden, besser ist es hierfür eine benannte Konstante zu verwenden.
|
// Definition des Symbols MAXSIZE für den Wert 10 #define MAXSIZE 10 // Definition des Symbols ERRTEXT für einem String #define ERRTEXT "Fehler aufgetreten!\n" |
|
|
Trifft der Präprozessor auf ein Symbol das für einen Wert steht, so ersetzt der Präprozessor vor dem Compiliervorgang das Symbol durch den entsprechenden Wert, aber mit folgenden zwei Ausnahmen: Symbole innerhalb von Strings (zweite cout Anweisung im Beispiel) und innerhalb von Kommentaren werden niemals ersetzt.
Bei der Angabe des Wertes für ein Symbol dürfen auch bereits definierte Symbole verwendet werden, so wie im Beispiel bei der Definition des Symbols LARGESIZE.
|
// Definition eines Text-Symbols #define GSTRING "Guten Tag\n" // Definition zweier int-Symbole #define MAXSIZE 10 #define LARGESIZE (MAXSIZE*2) // Variablendefinition // Die folgende Anweisung wird durch den Präprozessor folgendermaßen erweitert: // char array[10]; char array[MAXSIZE]; // main() Funktion int main () { // Die folgende Anweisung wird erweitert zu: // cout << "Guten Tag\n"; cout << GSTRING; // Aber keine Erweiterung dieser Anweisung da das Symbol innerhalb eines Strings steht! cout << "GSTRING"; } |
Ein definiertes Symbol kann mit der Direktive #undef auch wieder 'gelöscht' werden. Eine Referenz auf das Symbol oder Makro nach der #undef-Direktive führt dann selbstverständlich zu einem Fehler.
|
// Symbol DEBUG definieren #define DEBUG ... // Symbol wieder löschen #undef DEBUG ... |
Kehren wir wieder zur Definition eines Symbols mittels #define-Direktive zurück. Vielleicht haben Sie sich in der Zwischenzeit gefragt, was Sie mit einem solchen definierten Symbol denn anfangen können. Oft es so, dass während der Entwicklung eines Programms verschiedene Meldungen zur Kontrolle des Programmablaufs ausgegeben werden sollen. In der fertigen, auslieferfähigen Version sollen diese Meldungen dann natürlich nicht mehr erscheinen. Sie könnten jetzt hergehen und alle Kontrollmeldungen entfernen, müssten dann aber bei einem erneuten Programmtest die Meldungen wieder einbauen. Aber es geht natürlich auch einfacher.
Mit der Präprozessor-Direktive #ifdef SYMBOL können Sie ein Symbol abfragen, ob es definiert ist oder nicht. Ist das Symbol definiert, so übernimmt der Präprozessor alle Anweisungen die zwischen der #ifdef Direktive und der zu ihr dazugehörigen #endif Direktive stehen. Ist das Symbol dagegen nicht definiert, so werden diese Anweisungen vor dem Compiliervorgang 'entfernt', d.h. der Compiler bekommt die Anweisungen erst gar nicht 'zu Gesicht', und erzeugt somit auch keinen Code dafür. Im nachfolgenden Beispiel werden die beiden Ausgaben nur dann in den Code übernommen, wenn das Symbol DEBUG definiert ist.
|
#define DEBUG ... int main () { ... #ifdef DEBUG cout << "Programmpunkt1" << endl; #endif ... #ifdef DEBUG cout << "Variablenwert" << ... #endif ... } |
Außer der #ifdef SYMBOL Direktive gibt es noch die #ifndef SYMBOL Direktive. Sie hat die entgegengesetzte Wirkung, d.h. die zwischen #ifndef und #endif stehende Anweisungen werden nur dann übernommen, wenn das Symbol nicht definiert ist. Diese Direktive spielt bei den Dateien eine wichtige Rolle, die mittels #include eingebunden werden. Sehen Sie sich dazu einmal das nachfolgende Beispiel an. Die Datei FILE1.H enthält z.B. beliebige Definitionen und Deklarationen. Einige dieser Definitionen und Deklarationen werden jetzt z.B. auch von der Datei FILE2.H benötigt, weshalb FILE2.H die Datei FILE1.H einbindet. So weit ist alles noch in Ordnung. Sehen wir uns jetzt einmal eine typische Anwendung dazu an. Da die Quellcode-Datei MAIN.CPP sowohl Definitionen und Deklarationen aus der Datei FILE1.H wie auch aus FILE2.H benötigt, wird MAIN.CPP auch beide Dateien einbinden.
Und schon hätten wir ein 'kleines' Problem. Denn zuerst bindet MAIN.CPP die Datei FILE1.H ein und fügt dadurch bestimmte Deklarationen bzw. Definitionen ein. Anschließend bindet MAIN.CPP nun die Datei FILE2.H ein. Da FILE2.H selbst aber nochmals die Datei FILE1.H einbindet, würden damit die Definitionen und Deklarationen aus FILE1.H doppelt in MAIN.CPP vorkommen, was dann zu einem Übersetzungsfehler führt. Auch das Vertauschen der #include-Direktiven in MAIN.CPP bringt keine Lösung. Vielleicht sagen Sie sich nun: dann binde ich in der Datei FILE2.H die Datei FILE1.H einfach nicht mehr ein und schon funktioniert's. Im Prinzip haben Sie damit auch recht, doch sollten Sie niemals das Einbinden einer Datei vom vorherigen Einbinden einer anderen Datei abhängig machen. Im Beispiel wäre das erfolgreiche Einbinden der Datei FILE2.H dann vom vorherigen Einbinden der Datei FILE1.H abhängig. Und solche Abhängigkeiten führen früher oder später zu einer nicht mehr überschaubaren Abhängigkeit.
Aber keine Panik, denn jetzt kommt die Lösung dieses Problems durch die beiden Direktiven #ifndef und #define. Schließen Sie in Zukunft jede einzubindende Datei in einen #ifndef...#endif Zweig ein, so wie im Beispiel angegeben. Als abzufragenden Symbolnamen sollten Sie den Namen der entsprechenden einzubindenden Datei verwenden. Wurde die Datei noch nicht eingebunden, d.h. das Symbol ist noch nicht definiert, so definieren Sie das Symbol mit der #define-Direktive und führen anschließend wie gewohnt die Definitionen und Deklarationen durch. Wird die Datei dann ein zweites Mal eingebunden, so ist das entsprechende Symbol bereits definiert, und damit wird der Inhalt der Datei einfach übersprungen. So einfach geht's, wenn man es weiß!
|
// Datei FILE1.H #ifndef FILE1_H #define FILE1_H // Definition und Deklaration ... #endif |
|
// Datei FILE2.H #ifndef FILE2_H #define FILE2_H #include "FILE1.H" // weitere Definitionen und Deklarationen ... #endif |
|
// Datei MAIN.CPP #include "FILE1.H" #include "FILE2.H" ... |
Außer der einfache Abfrage, ob ein Symbol definiert ist oder nicht, gibt es auch ein IF-ELIF-ELSE-ENDIF Konstrukt, das im Prinzip genauso arbeitet wie die verwandte C++ Verzweigung. Der Unterschied zwischen der Präprozessor-Verzweigung und der C++ Anweisung ist der, dass die Präprozessor-Verzweigung zum einen vor dem Compilerdurchlauf ausgewertet wird und zum anderen auch nur mit Präprozessor-Symbolen arbeitet, also nicht C++ Daten. Im Beispiel werden je nach Wert des Symbols MAX verschiedene Text ausgegeben. Neu ist die #elif Direktive, sie ist eine Kombination aus ELSE und IF.
|
#define MAX 10 char array[MAX]; int main() { #if MAX<10 cout << "kleines Feld"; #elif MAX == 10 cout << "10er-Feld"; #elif MAX < 50 cout << "mittleres Feld"; #else cout << "grosses Feld"; #endif cout << endl; } |
Die #if und #elif Direktiven können auch mehrere Ausdrücke in der Bedingung auswerten. Die einzelnen Ausdrücke werden dann entweder durch den Operator || verodert oder durch && verundet.
Ebenfalls im Zusammenhang mit der #if-Präprozessor-Direktive steht die Direktive defined(...). defined(...) dient zum überprüfen ob ein Symbol definiert ist. Das zu überprüfende Symbol wird innerhalb einer Klammer angegeben. defined(...) liefert 1 zurück, wenn das Symbol definiert ist und ansonsten 0. Vor defined(...) kann noch der Operator '!' stehen um das Abfrageergebnis zu negieren. Im Beispiel werden je nach verwendetem Compiler verschiedene Ausgaben mit ins Programm übernommen. Die in den defined(...) Direktiven stehenden Symbole (beginnend mit 2 Underscore und einem nachfolgenden Großbuchstaben!) sind Symbole die vom Compiler definiert werden. So definiert der BORLAND-Compiler z.B. das Symbol _BORLANDC_ während der MICROSOFT Compiler das Symbol _MSVC_ definiert. Welche Symbole Ihr Compiler definiert, entnehmen Sie bitte aus der Dokumentation zum Compiler.
|
int main () { #if defined(_BORLANDC_) cout << "Mit Borland übersetzt!"; #elif definied(_MSVC_) cout << "Mit Microsoft übersetzt!"; #else cout << "Compiler unbekannt!"; #endif ... } |
Auch die Sprache C++ selbst definiert einige Symbole die Sie der folgenden Tabelle entnehmen können:
| Symbol | Bedeutung |
| __LINE__ | Enthält aktuelle Zeilennummer im Quellcode |
| __FILE__ | String mit dem Namen der akt. Datei |
| __DATE__ | String mit dem aktuellen Datum in der Form Monat/Tag/Jahr |
| __TIME__ | String mit der aktuellen Uhrzeit in der Form Stunde:Minute:Sekunde |
| __STDC__ | Compilerspezifisch, in der Regel ist dieses Symbol definiert wenn nur ANSI C/C++ Code vom Compiler akzeptiert wird. |
| __cplusplus | C++ Standard-konforme Compiler definieren für dieses Symbol einen Wert mit mind. 6 Ziffern, alle anderen C++ Compiler einen Wert mit bis zu 5 Ziffern. C Compiler definieren dieses Symbol überhaupt nicht. |
Die Direktive #error bewirkt einen Abbruch des Compilerlaufs. Nach der Präprozessor-Direktive kann noch ein Text stehen (muss nicht in Anführungszeichen stehen!), der beim Abbruch mit ausgegeben wird. In der Regel steht die #error Direktive innerhalb einer #if-Direktive.
Die #line Nummer ["Datei"] Direktive dient zum Umdefinieren der beiden Symbole __LINE__ und __FILE__ (siehe vorherige Tabelle). Die Angabe von Datei ist optional.
Und die letzte Präprozessor-Direktive #pragma dient zum Definieren von compilerspezifischen Präprozessor-Direktiven. Welche Direktive hier zulässig sind, das müssen Sie Ihrer Compiler-Dokumentation entnehmen. Kennt ein Präprozessor die hinter einem #pragma stehende Direktive nicht, so wird sie einfach ignoriert, d.h. es erfolgt keine Fehlermeldung.