C++-Programmierung/ Weitere Grundelemente/ Vorarbeiter des Compilers
Bevor der Compiler eine C++-Datei zu sehen kriegt, läuft noch der Präprozessor durch. Er überarbeitet den Quellcode, sodass der Compiler daraus eine Objektdatei erstellen kann. Diese werden dann wiederum vom Linker zu einem Programm gebunden. In diesem Kapitel soll es um den Präprozessor gehen, wenn Sie allgemeine Informationen über Präprozessor, Compiler und Linker brauchen, dann lesen Sie das Kapitel „Compiler“.
#include
[Bearbeiten]Die Präprozessordirektive #include
haben Sie schon häufig benutzt. Sie fügt den Inhalt der angegebenen Datei ein. Dies ist nötig, da der Compiler immer nur eine Datei übersetzen kann. Viele Funktionen werden aber in verschiedenen Dateien benutzt. Daher definiert man die Prototypen der Funktionen (und einige andere Dinge, die Sie noch kennenlernen werden) in so genannten Headerdateien. Diese Headerdateien werden dann über #include
eingebunden, wodurch Sie die Funktionen usw. aufrufen können.
Ausführlich Informationen über Headerdateien erhalten Sie im gleichnamigen Kapitel.
#include
bietet zwei Möglichkeiten, Headerdateien einzubinden:
#include "name" // Sucht im aktuellen Verzeichnis und dann in den Standardpfaden des Compilers
#include <name> // Sucht gleich in den Standardpfaden des Compilers
„Aktuelles Verzeichnis“ bezieht sich immer auf das Verzeichnis, in welchem die Datei liegt.
Die erste Syntax funktioniert immer, hat aber den Nachteil, dass dem Compiler nicht mitgeteilt wird, dass es sich um eine Standardheaderdatei handelt. Wenn sich im aktuellen Verzeichnis beispielsweise eine Datei namens iostream
befände und Sie versuchten über die erste Syntax, die Standardheaderdatei iostream
einzubinden, bänden Sie stattdessen die Datei im aktuellen Verzeichnis ein, was sehr unwahrscheinlich sein sollte, da Sie hoffentlich immer Dateiendungen wie .hpp oder .h für Ihre eigenen Header verwenden. Außerdem verlängert das Einbinden von Standardheadern in Anführungszeichen den Präprozessordurchlauf, je nach Anzahl der Dateien im aktuellen Quellpfad.
Aus diesem Grund ist es wichtig zu wissen, ob der eingebundene Header für eine Bibliotheksfunktionalität in den vordefinierten Pfaden des verwendeten Compilers steht, oder ob es eigener Inhalt ist, oder es sich um Zusatzbibliotheken handelt, deren Include-Verzeichnisse an anderen Stellen zu finden sind. Es sollte die Variante gewählt werden, bei der sich die Headerdatei vom Präprozessor am schnellsten finden lässt.
Verwenden Sie für Verweise auf Ihre eigenen Includes immer eine relative Pfadangabe mit normalen Slashes '/' als Verzeichnisseparator, damit Sie Ihre Quellcodeverzeichnisse auch an anderen Stellen und in anderen Entwicklungsumgebungen schneller kompiliert bekommen. Eine Verzeichnisangabe wie "/home/ichuser/code/cpp/projekt/zusatzlib/bla.h" oder "c:\users\manni\Eigene Dateien\code\cpp\projekt\zusatzlib\bla.h" ist sehr unangenehm und sorgt für große Verwirrung. Schreiben Sie es nun in "../zusatzlib/bla.h" um, können Sie später Ihr gesamtes Projekt leichter in anderen Pfaden kompilieren und sparen sich selbst und Ihrem Präprozessor einiges an Ärger und Verwirrung.
#define
und #undef
[Bearbeiten]#define
belegt eine Textsubstitution mit dem angegebenen Wert, z.B.:
Makros sind auch möglich, z.B.:
#define ADD_DREI(x, y, z) x + y + z + 3
int d(1), e(20), f(4), p(0);
p = ADD_DREI(d, e, f); // p = d + e + f + 3;
Diese sind allerdings mit Vorsicht zu genießen, da durch die strikte Textsubstitution des Präprozessors ohne jegliche Logikprüfungen des Compilers fatale Fehler einfließen können, und sollten eher durch (inline
)-Funktionen, Konstanten oder sonstige Konzepte realisiert werden. Manchmal lässt es sich allerdings nicht umgehen, ein Makro (weiter) zu verwenden. Beachten Sie bitte immer, dass es sich hierbei um Anweisungen an eine Rohquelltextvorbearbeitungsstufe handelt und nicht um Programmcode.
Da anstelle von einfachen Werten auch komplexere Terme als Parameter für das Makro verwendet werden können und das Makro selbst auch in einen Term eingebettet werden kann, sollten immer Klammern verwendet werden. Bsp.:
#define MUL_NOK(x, y) x*y
#define MUL_OK(x, y) ((x)*(y))
resultNok = a*MUL_NOK(b, c + d); // a*b*c + d = a*b*c + d <= falsch
resultOk = a*MUL_OK(b, c + d); // a*((b)*(c + d)) = a*b*c + a*b*d <= richtig
#undef
löscht die Belegung einer Textsubstitution/Makro, z.B.:
Bitte denken Sie bei allen Ideen, auf die Sie nun kommen, dass Sie fast immer eine Alternative zur Makroprogrammierung haben. Verwenden Sie diese Makros vor allem als Zustandsspeicher für den Präprozessordurchlauf an sich und nicht um Funktionalität Ihres Programms zu erweitern.
Einer statischen, konstanten Definition oder Templatefunktionen ist immer der Vorzug zu geben.
#
[Bearbeiten]Der #
-Ausdruck erlaubt es, den einem Makro übergebenen Parameter als Zeichenkette zu interpretieren:
Das obige Beispiel gibt also den Text "Test" auf die Standard-Ausgabe aus.
##
[Bearbeiten]Der ##
-Ausdruck erlaubt es, in Makros definierte Strings innerhalb des Präprozessorlaufs miteinander zu kombinieren, z.B.:
Als Resultat beinhaltet die Konstante A_UND_B die Zeichenkette "HalloWelt". Der ##
-Ausdruck verbindet die Namen der symbolischen Konstanten, nicht deren Werte. Ein weiteres Anwendungsbeispiel:
#define MAKE_CLASS( NAME ) \
class NAME \
{ \
public: \
static void Init##NAME() {}; \
};
...
MAKE_CLASS( MyClass )
...
MyClass::InitMyClass();
#if
, #ifdef
, #ifndef
, #else
, #elif
und #endif
[Bearbeiten]Direktiven zur bedingten Übersetzung, d.h. Programmteile werden entweder übersetzt oder ignoriert.
#if ''Ausdruck1''
// Programmteil 1
#elif ''Ausdruck2''
// Programmteil 2
/*
...
*/
#else
// Programmteil sonst
#endif
Die Ausdrücke hinter #if
bzw. #elif
werden der Reihe nach bewertet, bis einer von ihnen einen von 0 verschiedenen Wert liefert. Dann wird der zugehörige Programmteil wie üblich verarbeitet, die restlichen werden ignoriert. Ergeben alle Ausdrücke 0, wird der Programmteil nach #else
verarbeitet, sofern vorhanden.
Als Bedingungen sind nur konstante Ausdrücke erlaubt, d.h. solche, die der Präprozessor tatsächlich auswerten kann. Definierte Makros werden dabei expandiert, verbleibende Namen durch 0L ersetzt. Insbesondere dürfen keine Zuweisungen, Funktionsaufrufe, Inkrement- und Dekrementoperatoren vorkommen. Das spezielle Konstrukt
wird durch 1L bzw. 0L ersetzt, je nachdem, ob das Makro Name definiert ist oder nicht.
#ifdef
ist eine Abkürzung für #if defined
.
#ifndef
ist eine Abkürzung für #if ! defined
.
Beispiel:
Nehmen wir an, dass Sie Daten in zusammengesetzten Strukturen immer an 4-Byte Grenzen ausrichten wollen. Verschiedene Compiler bieten hierfür verschiedene compilerspezifische Pragma-Direktiven. Da diese Konfiguration nicht im C++-Standard definiert ist, kann nicht sichergestellt werden, dass es eine allgemeingültige Anweisung hierfür gibt. Sie definieren daher mit #define
in einer Headerdatei, die überall eingebunden ist, welchen Compiler Sie verwenden. Z.B. mit #define FLAG_XY_COMPILER_SUITE
in einer Datei namens "compiler-config-all.hpp". Das gibt Ihnen die Möglichkeit, an Stellen, an denen Sie compilerspezifisches Verhalten verwenden wollen, dieses auch auszuwählen.
#include "compiler-config-all.hpp"
#ifdef WIN32
#pragma pack(4)
#elif USING_GCC
#pragma align=4
#elif FLAG_XY_COMPILER_SUITE
#pragma ausrichtung(byte4)
#endif
#error
und #warning
[Bearbeiten]#error
gibt eine Fehlermeldung während des Compilerlaufs aus und bricht den Übersetzungsvorgang ab, z.B.:
#warning
ist ähnlich wie #error
, mit dem Unterschied, dass das Kompilieren nicht abgebrochen wird. Es ist allerdings nicht Teil von ISO-C++, auch wenn die meisten Compiler es unterstützen. Meistens wird es zum Debuggen eingesetzt:
#line
[Bearbeiten]Setzt den Compiler-internen Zeilenzähler auf den angegebenen Wert, z.B.:
#pragma
[Bearbeiten]Das #pragma
-Kommando ist vorgesehen, um eine Reihe von Compiler-spezifischen Anweisungen zu implementieren, z.B.:
Kennt ein bestimmter Compiler eine Pragma-Anweisung nicht, so gibt er üblicherweise eine Warnung aus, ignoriert diese nicht für ihn vorgesehene Anweisung aber ansonsten.
Um z.B. bei MS VisualC++ eine "störende" Warnung zu unterdrücken, gibt man folgendes an:
Vordefinierte Präprozessor-Variablen
[Bearbeiten]__LINE__
: Zeilennummer__FILE__
: Dateiname__DATE__
: Datum des Präprozessoraufrufs im Format Monat/Tag/Jahr__TIME__
: Zeit des Präprozessoraufrufs im Format Stunden:Minuten:Sekunden__cplusplus
: Ist nur definiert, wenn ein C++-Programm verarbeitet wird