Präprozessor-Direktiven und Attribute

Präpozessor-Direktiven

Präprozessor-Direktiven sind Anweisungen, die durch einen Präprozessor vor dem Compilerlauf ausgeführt werden. Alle Zeilen, deren erstes Zeichen ein '#' ist, werden als Präprozessor-Anweisungen interpretiert. Nach dem Symbol '#' folgt die Präprozessor-Direktive und zwischen dem Symbol '#' und der Direktive dürfen beliebig viele Leerzeichen und Tabulatoren stehen. Des Weiteren werden die Direktiven nicht mit einem Semikolon abgeschlossen, da sie keine ausführbaren Programmanweisungen sind.

#include-Direktive

Diese Direktive sollte bekannt sein. Sie fügt eine Datei an der Stelle im Programm ein, an der die Direktive steht. Es gibt zwei Formen der include-Direktive:

#include <DATEI>
#include "DATEI"

Die erste Form sucht die angegebene Datei in einem voreingestellten Pfad. Dieser Pfad wird in der Regel durch die Entwicklungsumgebung 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 iostream, werden in der Regel mittels dieser include-Form eingebunden.

Die zweite Form sucht die angegebene Datei ab dem Verzeichnis, in dem sich die Quelldatei mit der #include-Direktive befindet Wird die einzubindende Datei dort nicht gefunden, wird 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 wie auch relative Pfadangaben zu, wobei in der Pfadangabe anstelle eines Backslash ein Schrägstrich angegeben werden kann.

#define-Direktive

Die Präprozessor-Direktive #define definiert ein Symbol oder Makro:

#define SYMBOL
#define SYMBOL LITERAL
#define MAKRO AUSDRUCK

Für SYMBOL bzw. MAKRO können fast beliebige Namen verwendet werden, jedoch sollte der Name nicht mit einem oder zwei Unterstriche gefolgt von einem Großbuchstaben anfangen. Symbole, die mit einem oder zwei Unterstriche und anschließendem Großbuchstaben beginnen, sind für die Hersteller von Bibliotheken oder Compiler reserviert. Bei den Namen für die Symbole bzw. Makros ist die Groß-/Kleinschreibung relevant.

//Definition des Symbols COMMON_H
#define COMMON_H
// Definition des Symbols DEBUG
#define DEBUG

Die zweite Form definiert ebenfalls ein Symbol, nun aber für ein Literal. Diese Form sollte nur in Ausnahmefällen eingesetzt werden. Besser ist es, hierfür eine Konstante oder constexpr zu verwenden. Trifft der Präprozessor beim Durchlaufen des Quellcodes auf ein Symbol, welches für ein Literal steht, ersetzt der Präprozessor vor dem Übersetzungsvorgang das Symbol durch das Literal, mit folgenden zwei Ausnahmen: Symbolnamen innerhalb von Strings und innerhalb von Kommentaren werden nicht ersetzt.

#include <print>

// Definition des Symbols MAXSIZE für den Wert 10
#define MAXSIZE 10
// Definition des Symbols ERRTEXT für einen String
#define ERRTEXT "Fehler aufgetreten!\n"

int main()
{
    // Ausgabe der 'Symbolwerte'
    std::println("MAXSIZE = {}",MAXSIZE);
    std::println("ERRTEXT = {}",ERRTEXT);
}

MAXSIZE = 10
ERRTEXT = Fehler aufgetreten!

Mithilfe der #define-Direktive können auch Makros definiert werden (Zusammenfassung von mehreren Anweisungen, dritte Form). Diese Form sollte aber nicht mehr verwendet werden, da C++ typsichere Möglichkeiten bietet, um wiederkehrende Anweisungen einzubinden (Stichwort: inline-Funktionen). Wenn Sie mehr über #define-Makros erfahren möchten, sehen Sie bitte im Anhang K: Makro Definitionen nach.

#undef-Direktive

Ein mittels #define definiertes Symbol oder Makro kann mit der Direktive

#undef SYMBOL

wieder 'gelöscht' werden. Eine weitere Verwendung des Symbols oder Makros nach der #undef-Direktive führt zu einem Fehler.

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

#ifdef-Direktiven

Die #ifdef-Direktiven dienen zur Abfrage, ob ein Symbol definiert ist.

#ifdef SYMBOL
   C++-Anweisungen1
[#elifdef SYMBOL
   C++-Anweisungen2]
 ...
[#else
   C++-AnweisungenX]
#endif

Und je nachdem, ob das Symbol definiert ist oder nicht, werden die C++-Anweisungen mit in den Quellcode übernommen und im nachfolgenden Compilerlauf mit übersetzt.

#include <print>

int main()
{
    // Pruefen mit welchem Compiler das
    // Programm uebersetzt wird
#ifdef __MINGW32__
    std::println("Compiler ist GCC");
#elifdef _MSC_VER
    std::println("Compiler ist MSVS");
#else
    std::println("Unbekannter Compiler");
#endif
}

Compiler ist GCC

Das Beispiel prüft, ob das Programm mit dem MinGW- oder dem Microsoft-Compiler übersetzt wurde und gibt eine entsprechende Meldung aus. Die Symbole __MINGW32__ und _MSC_VER sind vom jeweiligen Compiler standardmäßig definierte Symbole.

Außer den #ifdef-Direktiven gibt es die #ifndef-Direktiven. Sie haben die entgegengesetzte Wirkung, d.h., die zwischen #ifndef und #endif stehenden Anweisungen werden übernommen, wenn das Symbol nicht definiert ist. Diese Direktiven spielen bei Dateien eine wichtige Rolle die mittels #include eingebunden werden. Sehen Sie sich dazu einmal das nachfolgende (sehr gekürzte) Beispiel an.

// Datei file1.h

// Nur zur Demo: Definition von PI
const double PI = 3.1416;
// Datei file2.h
// Benoetigt PI file1.h
#include "file1.h"

// Datum initialsieren
constexpr auto deg2rad = PI*2./360.;
//Datei main.cpp

#include <print>
// Eigene Dateien einbinden
#include "file1.h"
#include "file2.h"

int main()
{
    std::println("Wert PI = {}",PI);
    std::println("90 Grad = {}",deg2rad*90);
}

Die Datei file1.h enthält die Definition de Konstante PI. Diese Konstante wird ebenfalls in der Datei file2.h benötigt, weshalb file2.h die Datei file1.h einbindet.

Benötigt die Anwendung nun Definitionen und Deklarationen sowohl aus file1.h wie auch aus file2.h, muss sie beide Dateien einbinden. Und damit gibt es ein kleines Problem. Denn zuerst bindet main.cpp die Datei file1.h ein und fügt damit deren Deklarationen bzw. Definitionen ein. Anschließend bindet main.cpp die Datei file2.h ein. Da file2.h nochmals die Datei file1.h einbindet, sind die Definitionen und Deklarationen aus file1.h doppelt in main.cpp vorhanden, was zu einem Fehler führt. Vielleicht sagen Sie sich: Dann binde ich in der Datei file2.h die Datei file1.h nicht ein und alles ist in Ordnung. Im Prinzip ist dies möglich, jedoch sollte niemals das Einbinden einer Datei vom vorherigen Einbinden einer anderen Datei abhängig sein. Solche Abhängigkeiten führen früher oder später zu nicht mehr überschaubaren Abhängigkeiten.

Die Lösung für dieses Problem kommt in Form der beiden Direktiven #ifndef und #define. Schließen Sie in Zukunft die Anweisungen einer einzubindenden Header-Datei in einen #ifndef...#endif Block ein, so wie im nachfolgenden Beispiel exemplarisch für die Datei file1.h angegeben. Als abzufragendes Symbol kann (und sollte) der Name der einzubindenden Datei verwendet werden.

// Datei file1.h

#ifndef file1_h
#define file1_h

// Nur zur Demo: Definition von PI
constexpr double PI = 3.1416;

#endif // file1_h

Wurde die Header-Datei bisher noch nicht eingebunden, d.h., das Symbol ist nicht definiert, wird das Symbol mit der #define-Direktive definiert und anschließend folgen die Definitionen und Deklarationen. Wird die Datei dann ein zweites Mal eingebunden, ist das Symbol bereits definiert und der Inhalt der Datei übersprungen.

#if-Direktiven

Außer der Abfrage, ob ein Symbol definiert ist oder nicht, gibt es das Konstrukt

#if AUSDRUCK_1
   ANWEISUNGEN_1
[#elif AUSDRUCK_2
   ANWEISUNGEN_2]
...
[#else
   ANWEISUNGEN_x]
#endif

Der Unterschied zwischen der #ifdef- und #if-Direktive ist, dass #ifdef nur die Definition des Symbols abfragt und #if den Wert des Symbols. Im Beispiel wird je nach Wert des Symbols MAX eine andere Ausgabe in den Quellcode übernommen.

#include <print>

#define MAX 10

// Feld mit MAX Elementen
char array[MAX];

int main()
{
    // Je nach Inhalt von MAX
#if MAX<10
   std::println("kleines Feld");
#elif MAX == 10
   std::println("10er-Feld");
#elif MAX < 50
   std::println("mittleres Feld");
#else
   std::println("grosses Feld");
#endif
}

10er-Feld

Bei den #if- und #elif-Direktiven können auch mehrere auszuwertende Bedingungen stehen, die durch die Operatoren || verodert bzw. durch && zu verundet werden.

__has_include Ausdruck

Mithilfe des Ausdrucks

__has_include(hFile)

(2 Unterstriche am Anfang!) kann der Präprozessor überprüfen, ob eine Header-Datei vorhanden ist oder nicht. Ist die Header-Datei vorhanden, liefert der Ausdruck den Wert 1 zurück und ansonsten 0. __has_include kann zum Beispiel eingesetzt werden, wenn ein Programm für verschiedene Plattformen übersetzt werden soll und je nach Plattform unterschiedliche Header-Dateien einzubinden sind.

#if __has_include("SPC580os.h")
   #include "SPC580os.h"
#elif __has_include("SPC560os.h")
   #include "SPC560os.h"
#else
   #error Keine OS Datei gefunden
#endif

defined-Direktive und vordefinierte Symbole

Im Zusammenhang mit der #if-Direktive steht die Direktive

defined(SYMBOL)

defined() dient zur Überprüfung, ob ein Symbol definiert ist, wobei das zu überprüfende Symbol innerhalb einer Klammer anzugeben ist. defined() liefert 1 zurück, wenn das Symbol definiert ist und ansonsten 0. Vor defined() kann der NOT-Operator '!' stehen, um das Abfrageergebnis zu negieren. Im Beispiel werden wieder je nach verwendetem Compiler verschiedene Ausgaben ins Programm übernommen.

#include <iostream>
int main ()
{
#if defined(__MINGW64__)
   std::cout << "Mit GNU übersetzt!";
#elif defined(_MSC_VER)
   std::cout << "Mit Microsoft übersetzt!";
#else
   std::cout << "Compiler unbekannt!";
#endif
}

Mit GNU uebersetzt!

Und auch C++ definiert von Haus aus einige Symbole, die in der folgenden Tabelle aufgeführt sind:

Symbol Bedeutung
__LINE__ Enthält die aktuelle Zeilennummer im Quellcode.
__FILE__ C-String mit dem Namen der aktuellen Datei.
__DATE__ C-String mit dem aktuellen Datum in der Form Monat/Tag/Jahr des Übersetzungsvorgangs.
__TIME__ C-String mit der aktuellen Uhrzeit in der Form Stunde:Minute:Sekunde des Übersetzungsvorgangs.
__STDC__ Compilerspezifisch, in der Regel ist dieses Symbol definiert, wenn ANSI C/C++ Code vom Compiler akzeptiert wird.
__cplusplus Standardkonforme C++-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 nicht.
#include <print>

int main ()
{
    std::println("Heute ist der {}, {} Uhr",__DATE__, __TIME__);
    std::println("Diese Ausgabe erfolgt aus der Datei {}",__FILE__);
}

Heute ist der Nov 22 2025, 00:48:45 Uhr
Diese Ausgabe erfolgt aus der Datei C:\temp\cpp_testprojekte\est1\main.cpp

Weitere Präprozessor-Direktiven

Die Direktive

#error [text]

bewirkt einen Abbruch des Präprozessorlaufes und damit des Übersetzungsvorgangs. Nach der Direktive kann optional 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.

Nicht ganz so folgenschwer ist die Direktive

#warning [text]

 Sie gibt lediglich einen nach der Direktive stehenden Text aus. Auch sie steht in der Regel 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 comp_spec

dient zum Definieren von compilerspezifischen Direktiven comp_spec. Welche Direktiven hier zulässig sind, ist von Compiler zu Compiler unterschiedlich und der Compiler-Dokumentation zu entnehmen. Kennt der Präprozessor die hinter einem #pragma stehende Direktive nicht, wird sie ignoriert.

Attribute

Attribute dienen unter anderem dazu, applikations- oder compilerabhängige Spracherweiterung mit in den Quellcode aufzunehmen. Attribute werden in doppelte eckige Klammern eingeschlossen [[attribute_list]] und beziehen sich stets auf die unmittelbar davorstehende Compiler-Entität (Variable, Funktion, Klasse usw.). Die wichtigsten Attribute sind:

Attribut Bedeutung
[[noreturn]] Kennzeichnet eine Funktion die nicht zurückkehrt.
[[carries_dependency]] Kennzeichnet eine Datenabhängigkeit von Parametern oder Returnwerten zwischen Threads.
[[deprecated("reason")] Kennzeichnet einen Namen oder eine Entity als erlaubt, aber veraltet.
[[fallthrough]] Kennzeichnet ein nicht vorhandenes break in einem case Zweig als beabsichtigt.
[[nodiscard("text")]] Erzeugt eine Warnung beim Übersetzen, wenn der Returnwert einer Funktion nicht ausgewertet wird.
[[maybe_unused]] Unterdrückt Meldung über nicht verwendete Entities.

Beispiel anhand einer Funktion die nicht zurückkehrt.

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

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

Damit ist der Grundlagen-Teil abgeschlossen und es folgt der zweite Teil, die Objektorientierte Programmierung.