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.

Thema wird später näher erläutert…

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.

Tipp

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

#define BEGRUESSUNG "Hallo Welt!\n"
cout << BEGRUESSUNG;

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

#undef BEGRUESSUNG
Buchempfehlung

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.

Der #-Ausdruck erlaubt es, den einem Makro übergebenen Parameter als Zeichenkette zu interpretieren:

#define STRING(s) #s
cout << STRING(Test) << endl;

Das obige Beispiel gibt also den Text "Test" auf die Standard-Ausgabe aus.

Der ##-Ausdruck erlaubt es, in Makros definierte Strings innerhalb des Präprozessorlaufs miteinander zu kombinieren, z.B.:

#define A "Hallo"
#define B "Welt"
#define A_UND_B A##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

#defined ''Name''

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

#error Dieser Quellcode-Abschnitt sollte nicht mit compiliert werden!

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

#warning Dieser Quellcode-Abschnitt muss noch überarbeitet werden.

Setzt den Compiler-internen Zeilenzähler auf den angegebenen Wert, z.B.:

#line 100

Das #pragma-Kommando ist vorgesehen, um eine Reihe von Compiler-spezifischen Anweisungen zu implementieren, z.B.:

#pragma comment(lib, "advapi32.lib")

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:

#pragma warning( disable: 4010 ) // 4010 ist Nummer der Warnung

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