C++ Kurs

Präprozessor Direktiven

Die Themen:

Präprozessor-Direktiven sind Anweisungen, die vor dem eigentlichen Übersetzen des Programms durch den Compiler 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 ebenfalls beliebig viele Leerzeichen bzw. Tabulatoren stehen. Die Direktiven werden nicht mit einem Semikolon abgeschlossen, das sie keine ausführbaren Programmanweisungen sind.

include-Direktive

Sehen wir uns die erste, 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 mit der Include-Direktive befindet. 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.

PgmHeader
// 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 als auch relative Pfadangaben zu. Beachten Sie hierbei aber, 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!

define-Direktive

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 fast beliebige Namen verwendet werden, jedoch sollte dieser Namen nicht mit einem oder zwei Underscore einem Großbuchstaben anfangen. Symbole die mit einem oder zwei Underscore und anschließendem Großbuchstaben beginnen sind für die Hersteller von Bibliotheken oder Compiler reserviert. Zudem sind die Namen für die Symbole bzw. Makros case-sensitiv, d.h. es wird nach Groß-/Kleinschreibung unterschieden.

PgmHeader
//Definition des Symbols COMMON_H
#define COMMON_H

// Definition des Symbols DEBUG
#define DEBUG

Die zweite Form definiert ebenfalls ein Symbol, jetzt aber für einen Wert. Diese Form sollte aber nur noch in Ausnahmefällen eingesetzt werden, besser ist es, hierfür eine entsprechende Konstante zu verwenden.

PgmHeader
// 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 beim Durchlaufen des Quellcodes auf ein Symbol welches für einen Wert steht, so ersetzt der Präprozessor vor dem Übersetzungvorgang durch den Compiler das Symbol durch den entsprechenden Wert, aber mit folgenden zwei Ausnahmen: Symbole innerhalb von Strings 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.

PgmHeader
// 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";
}
DetailMit Hilfe der #define-Direktive können auch Makros (Zusammenfassung von mehreren Anweisungen) definiert werden. Dies ist aber im Zeitalter der C++ Programmierung eigentlich überflüssig, da C++ bessere Möglichkeiten bietet um wiederkehrende Anweisungen einzubinden (Stichwort: inline-Funktionen, werden später noch behandelt). Wenn Sie mehr über #define-Makros erfahren möchten, klicken Sie das Symbol links an.

undef-Direktive

Ein mittels #define definiertes Symbol oder Makro kann mit der Direktive #undef wieder 'gelöscht' werden. Eine weitere Verwendung des Symbols oder Makros nach der #undef-Direktive führt dann selbstverständlich zu einem Fehler.

PgmHeader
// Symbol DEBUG definieren
#define DEBUG
...
// Symbol wieder löschen
#undef DEBUG
...

ifdef-, ifndef- und endif-Direktive

Vielleicht haben Sie sich in der Zwischenzeit gefragt, für was ein solches definiertes Symbol nützlich sein kann. Ein Anwendungsfall ist, dass während der Entwicklung eines Programms verschiedene Meldungen zur Kontrolle des Programmablaufs ausgegeben werden sollen. In der fertigen, endgültigen Version sollten diese Meldungen dann natürlich nicht mehr erscheinen. Dazu könnte man dann alle Kontrollmeldungen vor der Erstellung der endgültigen Version entfernen, müssten dann aber bei einem erneuten Programmtest die Meldungen wieder einfügen. Aber es geht natürlich auch einfacher.

Mit der Präprozessor-Direktive #ifdef SYMBOL kann abgefragt werden, ob ein Symbol definiert ist oder nicht. Ist das Symbol definiert, so lässt der Präprozessor alle Anweisungen die zwischen der #ifdef Direktive und der dazugehörigen #endif Direktive stehen. Ist das Symbol dagegen nicht definiert, so werden diese Anweisungen vor dem Compiliervorgang ausgeblendet, 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.

PgmHeader
#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 stehenden Anweisungen werden nur dann übernommen, wenn das Symbol nicht definiert ist. Diese Direktive spielt bei Dateien eine wichtige Rolle, die mittels #include eingebunden werden. Sehen Sie sich dazu einmal das nachfolgende Beispiel an.

PgmHeader
// Datei FILE1.H

// Definition und Deklaration
...PgmHeader
// Datei FILE2.H

#include "FILE1.H"
// weitere Definitionen und Deklarationen
...PgmHeader
// Datei MAIN.CPP
#include "FILE1.H"
#include "FILE2.H"
...

Die Datei FILE1.H soll beliebige Definitionen und Deklarationen enthalten. Einige dieser Definitionen und Deklarationen werden jetzt z.B. auch in der Datei FILE2.H benötigt, weshalb FILE2.H die Datei FILE1.H einbindet. So weit ist alles noch in Ordnung.

Benötigt eine Anwendung dann Definitionen und Deklarationen sowohl aus der Datei FILE1.H als auch aus FILE2.H, dann muss sie auch beide Dateien einbinden. Und damit haben 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 aber nochmals die Datei FILE1.H einbindet, sind damit die Definitionen und Deklarationen aus FILE1.H doppelt in MAIN.CPP vorhanden, 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 ein und schon ist alles in Ordnung. Im Prinzip ist dies möglich, jedoch sollte niemals das Einbinden einer Datei vom vorherigen einbinden einer anderen Datei abhängig sein. Im Beispiel wäre das erfolgreiche einbinden der Datei FILE2.H ja vom vorherigen einbinden der Datei FILE1.H abhängig. Und solche Abhängigkeiten führen früher oder später zu nicht mehr überschaubaren Abhängigkeiten.

Aber keine Panik, hier kommt die Lösung dieses Problems in Form der beiden Direktiven #ifndef und #define. Schließen Sie in Zukunft die Anweisungen einer einzubindende Datei in einen #ifndef...#endif Zweig ein, so wie im Beispiel angegeben. Als abzufragenden Symbolnamen können (und sollten) Sie den Namen der einzubindenden Datei verwenden. Wurde die Datei dann noch nicht eingebunden, d.h. das Symbol wurde noch nicht definiert, so wird das Symbol mit der #define-Direktive definiert und anschließend wie gewohnt die Definitionen und Deklarationen durchgeführt. Wird die Datei dann ein zweites Mal eingebunden, so ist das Symbol bereits definiert und damit wird der Inhalt der Datei einfach übersprungen.

PgmHeader
// Datei FILE1.H
#ifndef FILE1_H
#define FILE1_H

// Definition und Deklaration
...

#endifPgmHeader
// Datei FILE2.H
#ifndef FILE2_H
#define FILE2_H

#include "FILE1.H"
// weitere Definitionen und Deklarationen
...

#endifPgmHeader
// Datei MAIN.CPP
#include "FILE1.H"
#include "FILE2.H"
...

if-, elif-, else und endif-Direktive

Außer der Abfrage, ob ein Symbol definiert ist oder nicht, gibt es auch ein IF-ELIF-ELSE-ENDIF Konstrukt, welches im Prinzip genauso arbeitet wie die verwandte C++ Verzweigung, wobei die #elif Direktive eine Kombination aus ELSE und IF ist. Der Unterschied zwischen der Präprozessor-Verzweigung und der C++ Anweisung ist, 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 wird je nach Wert des Symbols MAX ein entsprechender Text ausgegeben.

PgmHeader
#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.

defined-Direktive und vordefinierte Symbole

Ebenfalls im Zusammenhang mit der #if-Präprozessor-Direktive steht die Direktive defined(...). defined(...) dient zur Überprüfung, ob ein Symbol definiert ist, wobei das zu überprüfende Symbol innerhalb einer Klammer angegeben wird. 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.

PgmHeader
int main ()
{
#if defined(_BORLANDC_)
   cout << "Mit Borland übersetzt!";
#elif defined(_MSVC_)
   cout << "Mit Microsoft übersetzt!";
#else
   cout << "Compiler unbekannt!";
#endif
   ...
}

Und auch die Sprache C++ selbst definiert einige Symbole, die der folgenden Tabelle entnommen werden können:

Symbol Bedeutung
__LINE__ Enthält aktuelle Zeilennummer im Quellcode
__FILE__ String mit dem Namen der aktuellen 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.

Weitere Präprozessor-Direktiven

Die Direktive #error bewirkt einen Abbruch des Präprozessorlaufes. Nach der Direktive kann noch ein beliebiger Text stehen, der nicht in Anführungszeichen eingeschlossen sein muss. Dieser Text wird beim Abbruch mit ausgegeben. 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 Direktiven hier zulässig sind, das müssen Sie Ihrer Compiler-Dokumentation entnehmen. Kennt der Präprozessor die hinter einem #pragma stehende Direktive nicht, so wird sie einfach ignoriert, d.h. es erfolgt keine Fehlermeldung.

Obwohl keine Präprozessor-Direktive, soll an dieser Stelle auf noch auf die Attribute hingewiesen werden. Attribute dienen dazu, um Applikations- oder Compiler-abhängige Spracherweiterung mit den Quellcode aufnehmen zu können. Attribute werden in doppelte eckige Klammern eingeschlossen [[attribute_list]] und beziehen sich stets auf die unmittelbar davor stehende Compiler-Entität. So kennzeichnet das Attribut [[noreturn]] eine Funktion, die niemals zurückkehrt.

void MyThread [[noreturn]] ()
{
    while (true)
       ....

Wenn Sie mehr über Attribute erfahren wollen, sehen Sie bitte in der Compiler-Beschreibung nach.