Programmieren in C++: Einführung


Kapitel 8: Präprozessor


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


8.1 Allgemeines

Code-Bearbeitung vor der Kompilierung

Der Präprozessor ist ein Programm, das vor dem Compiler-Aufruf automatisch gestartet wird und Quellcode ähnlich wie in einer Textverarbeitung bearbeitet. Mit Hilfe ganz bestimmter Befehle kann beispielsweise Quellcode aus anderen Dateien in die aktuelle Datei eingefügt werden. Es kann jedoch auch Quellcode aus der aktuellen Datei gelöscht werden, so dass der Compiler den entsprechenden Quellcode nicht sieht und auch nicht mitübersetzt.

Der Präprozessor wird für gewöhnlich nur von C- und C++-Compilern verwendet. Java und andere Programmiersprachen kennen keinen Präprozessor. Für die Programmierung in C und C++ ist der Präprozessor jedoch ganz entscheidend, da er die Entwicklung sehr vereinfachen kann.


8.2 Symbolische Konstanten

#define und #undefine

Symbolische Konstanten können über den Präprozessor-Befehl #define definiert werden. Über #undef kann die Definiton einer symbolischen Konstanten wieder aufgehoben werden.

Beachten Sie, dass beide Befehle mit dem #-Zeichen beginnen. Daran können Sie ganz leicht Präprozessor-Befehle erkennen - sie beginnen alle mit einem Hash-Zeichen.

#define DEBUG

Im obigen Code-Beispiel wird eine symbolische Konstante namens DEBUG definiert. Man hat sich angewöhnt, symbolische Konstanten durchgehend groß zu schreiben, um sie schnell erkennen zu können.

Beachten Sie, dass die #define-Anweisung nicht mit einem Semikolon abgeschlossen wird.

Um die Definition einer symbolischen Konstanten wieder aufzuheben, verwenden Sie einfach den #undef-Befehl.

#undef DEBUG

Ist die Definition symbolischer Konstanten eigentlich nur im Zusammenhang mit bedingter Kompilierung sinnvoll, so können symbolische Konstanten jedoch auch als Textersatz definiert werden. Betrachten Sie dazu folgendes Beispiel.

#include <iostream> 

#define MEHRWERTSTEUER 19 

int main() 
{ 
  std::cout << "Aktuelle Mehrwertsteuer: " << MEHRWERTSTEUER << "%" << std::endl; 
} 

Innerhalb des Quellcodes wird direkt die definierte symbolische Konstante MEHRWERTSTEUER angegeben. Wie ist das möglich?

Wenn obiges Code-Beispiel vom Compiler übersetzt wird, wird zuerst der Präprozessor gestartet. Dieses Programm ersetzt nun ganz einfach alle symbolischen Konstanten durch den jeweils definierten Wert. Im Beispiel wird im Quellcode an all den Stellen, an denen MEHRWERTSTEUER steht, die Zahl 19 eingefügt. Erst dann wird die so bearbeitete Quellcode-Datei an den Compiler weitergereicht.

Je nachdem, wie umfangreich der Quellcode ist, kann der Einsatz derartiger symbolischer Konstanten sehr hilfreich sein. Bei einer Änderung des Mehrwertsteuersatzes muss lediglich die symbolische Konstante auf einen anderen Wert gesetzt werden, um danach den Quellcode neu zu übersetzen. Es ist also nicht notwendig, den gesamten Quellcode zu durchsuchen und zu überprüfen, an welchen Stellen nun überall andere Mehrwertsteuersätze angegeben werden müssen.

Mit #define können nicht nur einfache symbolische Konstanten angelegt werden, sondern auch komplexe Makros. Sehen Sie sich folgendes Beispiel an.

#include <iostream> 

#define SUB(a, b) ((a) - (b)) 

int main() 
{ 
  std::cout << SUB(10, 5) << std::endl; 
} 

Das Programm definiert ein Makro SUB, das zwei Parameter erwartet. Das Makro wird durch den Präprozessor so aufgelöst, dass zwischen die beiden Parameter das Minus-Zeichen gestellt wird. Wenn Sie nun wie im obigen Beispiel das Makro aufrufen und die Werte 10 und 5 als Parameter übergeben, wird als Ergebnis der Subtraktion der Wert 5 auf den Bildschirm ausgegeben.

Makros sollten grundsätzlich so definiert werden, dass die Parameter einzeln geklammert und der gesamte Ausdruck ebenfalls nochmal geklammert wird - je mehr Klammern, umso besser. Im folgenden Beispiel sind die Klammern extra weggelassen worden, was zu einem unerwarteten Ergebnis führt.

#include <iostream> 

#define SUB(a, b) a - b 

int main() 
{ 
  std::cout << SUB(10, 5) * 2 << std::endl; 
} 

Anstatt 10 und 5 zu subtrahieren und das Ergebnis mit 2 zu multiplizieren wird zuerst 5 mit 2 multipliziert und dann von 10 subtrahiert. Die Lösung, die erwartet wurde, war 10 - angezeigt wird jedoch 0.

Genau derartige Fälle sind der Grund, warum dringend vom Einsatz von Makros abgeraten wird. In der C++-Community im Internet wird vor Makros gewarnt: "Macros are evil". Der Präprozessor ersetzt wie die Suchen/Ersetzen-Funktionen einer Textverarbeitung stupide Makros, ohne weitergehende Überprüfungen durchzuführen. Der Einsatz von Funktionen bietet vor allem, was Makros betrifft, eine viel größere Sicherheit, da hier sogenannte Nebeneffekte, wie es sie bei Makros oft gibt, nicht auftreten können.


8.3 Bedingte Kompilierung

#if, #elif, #else, #endif, #ifdef und #ifndef

Symbolische Konstanten, die lediglich mit #define definiert werden, ohne einen zu ersetzenden Wert zu erhalten, haben bisher noch nicht viel Sinn ergeben. Im Zusammenhang mit den Präprozessorbefehlen #if, #elif, #else, #endif, #ifdef und #ifndef kann jedoch in Abhängigkeit einer Definition unterschiedlicher Code an den Compiler weitergegeben werden. Betrachten Sie dazu folgendes Beispiel.

#include <iostream> 
#include <cstdlib> 

#define DEBUG 0 

int main() 
{ 
  char buffer[20]; 
  int number; 

  std::cout << "Geben Sie eine Zahl ein: " << std::flush; 
  std::cin.get(buffer, sizeof(buffer)); 
#if DEBUG > 0 
  std::cout << "DEBUG: " << buffer << std::endl; 
#endif 
  number = std::atoi(buffer); 
#if DEBUG > 1 
  std::cout << "DEBUG: " << number << std::endl; 
#endif 
} 

Am Anfang des Programms wird eine symbolische Konstante DEBUG definiert. Diese Konstante kann auf verschiedene Werte gesetzt werden. Je höher der Wert ist, umso mehr Debug-Informationen werden vom Programm ausgegeben. Ist DEBUG auf 0 gesetzt, gibt das Programm keine Debug-Informationen aus. Wird DEBUG auf den Wert 1 gesetzt, wird zusätzlicher Output auf den Bildschirm ausgegeben, um das Programm auf eventuelle Fehler überprüfen zu können. Wird DEBUG auf den Wert 2 gesetzt, werden soviel Debug-Informationen wie möglich ausgegeben.

Mit #if kann genauso wie mit dem bekannten C++-Befehl if eine Bedingung überprüft werden. #if kann jedoch nur auf symbolische Konstanten angewandt werden. Wenn die Bedingung wahr ist, wird der nachfolgende Quellcode bis zu einem abschließendem #endif an den Compiler zur Übersetzung weitergereicht. Andernfalls wird der Quellcode entfernt - der Compiler wird ihn nicht zu sehen bekommen.

Hier sehen Sie einen ganz entscheidenden Vorteil von symbolischen Konstanten verglichen mit Variablen: Es ist möglich, Quellcode nicht in einem Programm zu verwenden, indem über entsprechende Präprozessoranweisungen der Quellcode nicht an den Compiler weitergereicht wird. Sie könnten das gleiche Programm auch mit Hilfe einer globalen Variablen schreiben, die auf einen bestimmten Wert gesetzt wird. Hier würde nun jedoch der gesamte Quellcode vom Compiler übersetzt werden, selbst wenn Teile des Codes im Programm niemals verwendet werden würden. Das würde das Programm unnötigerweise aufblähen und mit ungenutzten Code vollstopfen.

Der schematische Aufbau einer bedingten Kompilierung sieht wie folgt aus.

#if BEDINGUNG 
ANWEISUNGEN 
#elif BEDINGUNG 
ANWEISUNGEN 
#else 
ANWEISUNGEN 
#endif 

Sie sehen, dass wie die bekannte if-Kontrollstruktur auch die bedingte Kompilierung mehrere Bedingungen überprüfen kann. Dies erfolgt per #elif. Genauso wie in der if-Kontrollstruktur können #elif-Zweige weggelassen werden. Auch das abschließende #else ist optional.

Anstatt hinter #if eine Bedingung zu überprüfen, kann hinter #ifdef eine symbolische Konstante daraufhin überprüft werden, ob sie überhaupt definiert ist.

#include <iostream> 
#include <cstdlib> 

#define DEBUG 

int main() 
{ 
  char buffer[20]; 
  int number; 

  std::cout << "Geben Sie eine Zahl ein: " << std::flush; 
  std::cin.get(buffer, sizeof(buffer)); 
#ifdef DEBUG 
  std::cout << "DEBUG: " << buffer << std::endl; 
#endif 
  number = std::atoi(buffer); 
#ifdef DEBUG 
  std::cout << "DEBUG: " << number << std::endl; 
#endif 
} 

Die symbolische Konstante DEBUG wird nun nicht mehr auf einen Wert gesetzt, sondern einfach nur definiert. Über #ifdef wird nun innerhalb des Quellcodes überprüft, ob es die symbolische Konstante DEBUG gibt. Ist dies der Fall, wird der Quellcode bis zum abschließenden #endif an den Compiler zur Übersetzung weitergereicht. Um nun zu verhindern, dass die entsprechenden Code-Zeilen kompiliert werden, muss einfach die #define-Anweisung für DEBUG aus dem Programm gelöscht werden.

Während es also mit #if möglich ist, eine symbolische Konstante auf einen bestimmten Wert zu überprüfen, kann mit #ifdef überprüft werden, ob eine symbolische Konstante überhaupt definiert ist.

Als Gegenstück zu #ifdef existiert #ifndef. Mit #ifndef wird überprüft, ob eine symbolische Konstante nicht definiert ist.

#ifdef und #ifndef sind letztendlich nur Abkürzungen für #if defined und #if !defined. Nachdem es für #elif-Zweige keine Abkürzung gibt, muss für weitergehende Überprüfungen in einer #if-Kontrollstruktur mit der langen Schreibweise gearbeitet werden. Betrachten Sie dazu folgendes Beispiel.

#include <iostream> 

int main() 
{ 
#if defined(__linux__) 
  std::cout << "Betriebssystem Linux" << std::endl; 
#elif defined(WIN32) 
  std::cout << "Betriebssystem Microsoft Windows" << std::endl; 
#elif defined(__APPLE__) 
  std::cout << "Betriebssystem Mac OS X" << std::endl; 
#else 
  std::cout << "Unbekanntes Betriebssystem" << std::endl; 
#endif 
} 

Während #if defined auch als #ifdef geschrieben werden könnte, gibt es für die #elif defined-Zweige keine abgekürzte Schreibweise. Daher findet man normalerweise immer nur die lange Form in Quellcodes, um bei einer einheitlichen Schreibweise zu bleiben.

Obiges Beispiel demonstriert auch gleich einen sehr häufigen Einsatz von #if defined-Überprüfungen. Im Beispiel wird überprüft, ob eine der symbolischen Konstanten __linux__, WIN32 oder __APPLE__ definiert ist. Symbolische Konstanten, die mit zwei Unterstrichen beginnen und enden, werden von Compilern definiert. C++-Compiler auf dem Betriebssystem Linux definieren hierbei eine symbolische Konstante __linux__, C++-Compiler unter Microsoft Windows WIN32 und C++-Compiler unter Mac OS X __APPLE__. Auf diese Weise ist es möglich, betriebssystemspezifischen Quellcode zu schreiben und ihn nur dann an den Compiler weiterzugeben und übersetzen zu lassen, wenn der Quellcode auf dem richtigen Betriebssystem kompiliert wird.


8.4 Fehlermeldung

#error

Im Zusammenhang mit der bedingten Kompilierung ist es manchmal sinnvoll, eine Fehlermeldung ausgeben zu können, mit der gleichzeitig die Kompilierung abgebrochen wird. Sehen Sie sich dazu folgendes Beispiel an.

#include <iostream> 

int main() 
{ 
#if defined(WIN32) 
  std::cout << "Betriebssystem Microsoft Windows" << std::endl; 
#else 
#error Das Programm muss unter Microsoft Windows kompiliert werden! 
#endif 
} 

Der Präprozessor überprüft, ob eine symbolische Konstante WIN32 definiert ist. Dies ist nur dann der Fall, wenn der Quellcode mit einem Compiler unter Microsoft Windows kompiliert wird. Wird das Programm auf einem anderen Betriebssystem kompiliert, wird mit #error eine Fehlermeldung auf den Bildschirm ausgegeben und die Kompilierung abgebrochen. Dies ist beispielsweise sinnvoll, wenn ein Programm für ein bestimmtes Betriebssystem noch nicht angepaßt wurde und daher eine Kompilierung keinen Sinn ergeben würde.


8.5 Dateien einfügen

#include

Die Präprozessor-Anweisung #include haben Sie bereits in vielen Beispielprogrammen selbst verwendet gehabt. Hinter #include wird ein Dateiname angegeben. Der gesamte Inhalt der angegebenen Datei wird vom Präprozessor daraufhin gelesen und in die aktuelle Quellcode-Datei kopiert - und zwar genau an die Stelle gesetzt, an der sich die #include-Anweisung befindet.

#include bietet zwei Schreibweisen an.

#include <iostream> 

Der Dateiname wird in spitzen Klammern angegeben, wenn die Datei in den Standard-Include-Verzeichnissen liegt. Compiler lassen sich so konfigurieren, dass sie eine Reihe von Verzeichnissen kennen, in denen wichtige Include-Dateien liegen. Normalerweise werden diese Dateien Header-Dateien genannt. Während vor dem offiziellen C++-Standard die Endung für Header-Dateien gewöhnlich .h war, ist im Standard neu festgelegt worden, dass Header-Dateien gar keine Endung mehr besitzen. Dies betrifft jedoch nur die Header-Dateien innerhalb des C++-Standards. Wie Sie Ihre eigenen Header-Dateien nennen spielt keine Rolle. Die Endung .h wie auch jede andere beliebige Endung ist erlaubt.

#include "mein_datentyp.h" 

Während der Zugriff auf offizielle Header-Dateien normalerweise über spitze Klammern erfolgt, findet der Zugriff auf eigene Header-Dateien, die von Ihnen entwickelt wurden und zum aktuellen Projekt gehören, über Anführungszeichen statt. Derart angegebene Dateien werden im aktuellen Verzeichnis gesucht und nicht in den Standard-Include-Verzeichnissen.

Während Sie mit #include eigentlich jede beliebige Datei in eine andere Datei einfügen können, werden diese Präprozessor-Anweisungen in C++ hauptsächlich in Verbindung mit Klassen-Definitionen verwendet. Wann immer Sie auf eine Klasse zugreifen müssen, müssen Sie diese vorher bekannt machen - erst Definition, dann Verwendung. Indem Sie die entsprechende Header-Datei einbinden, fügen Sie die Definition der Klasse in Ihre eigene Datei ein. Daraufhin können Sie diese Klasse innerhalb Ihres Quellcodes verwenden. So sind Sie zum Beispiel immer vorgegangen, wenn Sie in Ihrem Code die Klasse std::string einsetzen wollten - diese mussten Sie erst bekanntmachen. Dies taten Sie immer, indem Sie die Header-Datei string mit #include eingebunden haben.

Das Erstellen von Klassen-Definitionen und Header-Dateien wird im Buch Programmieren in C++: Aufbau behandelt.


8.6 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Entwickeln Sie eine C++-Anwendung, die den Anwender zur Eingabe zweier Ganzzahlen auffordert. Die beiden eingegebenen Zahlen sollen mit Hilfe einer Funktion addiert werden, die gleichzeitig die Summe auf den Bildschirm ausgibt. Das Programm soll auf den drei fiktiven Betriebssystemen Kuhnix BSE, Luxus und Hasta La Vista lauffähig sein. Kuhnix BSE bietet jedoch nur den Datentyp long an, um Ganzzahlen zu speichern. Luxus hingegen kennt nur den Datentyp char. Und Hasta La Vista kann Ganzzahlen nur in Variablen vom Typ short speichern. Verwenden Sie Präprozessor-Anweisungen in der Art, dass das Programm mit einer entsprechenden #define Anweisung jeweils so übersetzt wird, dass es unter dem jeweiligen Betriebssystem funktioniert. Falls das Programm unter dem nicht unterstützten Betriebssystem Banana Mac kompiliert wird, soll eine Fehlermeldung ausgegeben und die Kompilierung abgebrochen werden.