C++ Tutorial

Präprozessor-Direktiven & Attribute

Präprozessor-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.

1: // Suche nur im Standard-Include-Pfad
2: #include <iostream>
3: // Suche im akt. Verzeichnis und im dann im
4: // Standard-Include-Pfad
5: #include "common.h"
6: // Suche in einem relativen Pfad
7: #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.

1: //Definition des Symbols COMMON_H
2: #define COMMON_H
3: // Definition des Symbols DEBUG
4: #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.

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

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 niemals ersetzt.

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

1: // Symbol DEBUG definieren
2: #define DEBUG
3: ...
4: // Symbol wieder löschen
5: #undef DEBUG
6: ...

#ifdef-Direktiven

Die #ifdef-Direktiven dienen zur Abfrage, ob ein Symbol definiert ist. Folgende #ifdef-Direktiven stehen dazu zur Verfügung:

#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 übernommen und im nachfolgenden Compilerlauf mit übersetzt.

Die #elifdef- (eine Kombination aus else und if) und #else-Direktiven sind optional.

1: #include <iostream>
2:
3: int main()
4: {
5: #ifdef __MINGW32__
6:    std::cout << "Compiler ist GCC\n";
7: #elifdef _MSC_VER
8:    std::cout << "Compiler ist MSVS\n";
9: #else
10:    std::cout << "Unbekannter Compiler\n";
11: #endif
12: }

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 Beispiel an.

1: // Datei file1.h
2: ... // Definition und Deklaration
1: // Datei file2.h
2: #include "file1.h"
3: ... // weitere Definitionen und Deklarationen
1: // Datei main.cpp
2: #include "file1.h"
3: #include "file2.h"
4: ...

Die Datei file1.h enthält beliebige Definitionen und Deklarationen. Einige davon werden ebenfalls in der Datei file2.h benötigt, weshalb file2.h die Datei file1.h einbindet.

Benötigt eine Anwendung Definitionen und Deklarationen sowohl aus der Datei 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 jetzt 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 angegeben. Als abzufragendes Symbol kann (und sollte) der Name der einzubindenden Datei verwendet werden.

1: // Datei file1.h
2: #ifndef FILE1_H
3: #define FILE1_H
4: ... // Definition und Deklaration
5: #endif
1: // Datei file2.h
2: #ifndef FILE2_H
3: #define FILE2_H
4:
5: #include "file1.h"
6: ... // weitere Definitionen und Deklarationen
7: #endif
1: // Datei main.cpp
2: #include "file1.h"
3: #include "file2.h"
4: ...

Wurde die Header-Datei 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
#elifAUSDRUCK_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.

1: #define MAX 10
2: char array[MAX];
3:
4: int main()
5: {
6: #if MAX<10
7:     std::cout << "kleines Feld";
8: #elif MAX == 10
9:     std::cout << "10er-Feld";
10: #elif MAX < 50
11:    std::cout << "mittleres Feld";
12: #else
13:    std::cout << "grosses Feld";
14: #endif
15:    cout << '\n';
16: }

Bei den #if- und #elif-Direktiven können mehrere auszuwertende Bedingungen stehen. Die einzelnen Bedingungen sind durch den Operator || zu verodern bzw. durch && zu verunden.

__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.

1: #if __has_include("SPC580os.h")
2:   #include "SPC580os.h"
3: #elif __has_include("SPC560os.h")
4:   #include "SPC560os.h"
5: #else
6:   #error Keine OS Datei gefunden
7: #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.

1: int main ()
2: {
3: #if defined(__MINGW64__)
4:    std::cout << "Mit GNU übersetzt!";
5: #elif defined(_MSC_VER)
6:    std::cout << "Mit Microsoft übersetzt!";
7: #else
8:    std::cout << "Compiler unbekannt!";
9: #endif
10:   ...
11: }

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__
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 ANSI C/C++ Code vom Compiler akzeptiert wird.
__cplusplus
C++ standardkonforme 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.

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 dient zum Definieren von compilerspezifischen Direktiven. 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.
1: void MyThread [[noreturn]] ()
2: {
3:    while (true)
4:    { ... }
5: }

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.


Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz