Der Präprozessor ist Teil eines jeden C oder C++ Compilers und bezeichnet in etwa die erste Stufe, die im Rahmen der Übersetzung eines Quelltextes durchlaufen wird. Tatsächlich ist der Präprozessor im C++ Standard nicht klar vom Rest des Compilers abgegrenzt, im alltäglichen Sprachgebrauch bezeichnet er in etwa all die Schritte der Übersetzung, die einer einfachen Textersetzung gleichkommen. Präprozessordirektiven lassen sich deshalb auch stets an dem vorangestellten '
#'-Zeichen erkennen, das es dem Übersetzer sehr leicht macht, die Direktiven von anderen Symbolen im Quelltext zu unterscheiden.
Eine Direktive, die du mit Sicherheit kennst, ist
#include "SomeHeader.h". Diese Direktive weist den Präprozessor einfach nur an, den gesamten Inhalt der angegebenen Datei an Stelle des
#includes in den Quelltext zu kopieren. Deshalb sind Fehlermeldungen, die durch fehlerhafte Includes hervorgerufen werden, mitunter auch so schwer zu lokalisieren. Moderne Compiler geben zwar ihr bestes, den Ursprung von Fehlern auch nach dieser simplen Textersetzung so genau wie möglich zu lokalisieren, manche Fehler (z.B. fehlende Semikola am Ende) lassen sich jedoch nur dann wirklich verstehen, wenn man sich dieser einfachen Funktionsweise von #include bewusst ist.
Eine weitere gebräuchliche Direktive ist
#define. So definiert
#define MY_PI 3.14 zum Beispiel eine Konstante
MY_PI, mit der Folge, dass der Präprozessor bei der Übersetzung in deinem Quelltext alle nachfolgenden Vorkommen von
MY_PI durch
3.14 ersetzen wird. Aber Vorsicht, auch hier handelt es sich um eine einfache Textersetzung. Diese Art von Konstanten stammen noch aus Zeiten, in denen es keine andere Möglichkeit der Definition von Konstanten gab. Genau wie du bei der manuellen Bearbeitung von Quelltext mit Textersetzungstools sehr leicht Fehler machen kannst, so kann dir das auch mit solchen Konstanten passieren. Hier liegt auch der Grund, warum solche Konstanten in der Regel
GROSSGESCHRIEBEN werden: Würde sich der Namen irgendeiner Konstante mit dem Namen irgendeines anderen Symbols deines Quelltexts, zum Beispiel einer Funktion oder Variablen, überschneiden, so würde jedes Vorkommen dieser Funktion oder Variable ebenfalls ersetzt. Hierin liegt auch der Grund, warum man derartige Definitionen von Konstanten mit Hilfe des Präprozessors heute nach Möglichkeit immer vermeidet. Ein großes Ärgernis bis heute sind zum Beispiel die Windows API Headers, die zur automatischen Umbenennung von Funktionen solche Dinge wie
#define CreateFont CreateFontA enthalten, was dazu führt, dass auch deine eigene Funktion auf einmal
CreateFontA heißen, wenn du sie eigentlich
CreateFont nennen willst. Zur Definition von Konstanten deshalb immer
const verwenden, in obigem Fall
float const MyPI = 3.14f.
In manchen Fällen ist der Präprozessor leider bis heute unvermeidbar, so zum Beispiel in deinem Include-Guard-Beispiel.
#ifndef QUIZ_HEADER ist eine weitere Präprozessordirektive, die prüft, ob eine Präprozessorkonstante mit dem Namen
QUIZ_HEADER existiert. Existiert eine solche Konstante noch nicht, dann wird sie im Anschluss mit
#define QUIZ_HEADER definiert. Existiert sie hingegen schon, so wird aller Quelltext bis zur nächsten schließenden
#endif-Direktive übersprungen. Dein Beispiel ist hier etwas unvollständig, in der Regel sieht das vollständige Szenario wie folgt aus:
|
C-/C++-Quelltext
|
1
2
3
4
5
6
|
// Quiz.h
#ifndef QUIZ_HEADER
#define QUIZ_HEADER
// Quelltext des Headers, z.B.:
float const MyPI = 3.14f;
#endif
|
Benötigt werden solche Include Guards genau weil der Präprozessor so ein primitives Programm ist, das Includes wie eingangs beschrieben nur per Textersetzung durchführt. Schreibst du nun zweimal hintereinander
#include "Quiz.h", dann ersetzt der Präprozessor einfach zweimal das jeweilige
#include mit dem Inhalt deines Quiz-Headers. Das wiederum würde ohne Include-Guard zu einem Fehler führen, weil nun
MyPI in deiner Übersetzungseinheit (= Cpp-Datei) zweimal hintereinander definiert würde. In diesem Fall entsteht jedoch kein solcher Fehler, weil der Präprozessor nach der Ersetzung der ersten
#include-Direktive, bei der
QUIZ_HEADER noch nicht definiert ist,
QUIZ_HEADER mit dem nachfolgenden
#define in die Liste der definierten Konstanten aufnimmt. Bei Ersetzung der zweiten
#include-Direktive ist deshalb
QUIZ_HEADER nun schon definiert worden, was den Präprozessor bei der zweiten Kopie des
#ifdef QUIZ_HEADER dazu veranlasst, diesmal den Quelltext bis zum
#endif zu überspringen, eine Doppeldefinition von
MyPI findet somit nicht mehr statt.
In diesem Rahmen mag ein solcher Include Guard nicht allzu sinnvoll erscheinen, wenn du viele Headers hast, die sich gegenseitig einbinden, passiert es jedoch sehr schnell, dass einige Headers auf Umwegen mehrfach in deine Übersetzungseinheiten eingefügt werden. Deshalb ist es gängige Praxis, alle Headers mit derartigen Include Guards zu versehen.
Es sei noch angemerkt, dass fast alle modernen Compiler eine weitere Direktive
#pragma once anbieten, die nur einmal an den Anfang eines jeden Headers geschrieben werden muss, um dasselbe Verhalten ohne umständliches
#ifdef ... #define ... #endif zu erhalten. Im Standard ist diese Direktive leider immer noch nicht enthalten, wodurch du streng genommen mit
#pragma once keinen portablen C++-Code mehr schreibst. Dafür umgehst du jedoch Fehler, die entstehen, wenn versehentlich mehrere Headers denselben Namen für ihre Include Guards nutzen, mit
#pragma once automatisch, was die Nutzung von
#pragma once durchaus empfehlenswert macht. In diesem Fall sähe obiges Beispiel so aus:
|
C-/C++-Quelltext
|
1
2
3
4
|
// Quiz.h
#pragma once
// Quelltext des Headers, z.B.:
float const MyPI = 3.14f;
|