Präprozessor
C-Präprozessor
Als C entwickelt wurde, waren Hauptspeicher und Festplattenplatz knapp, was zu einem spartanischen Sprachumfang von C geführt hat. Beispielsweise war C nicht in der Lage, Dateien beim Kompilieren zu inkludieren, sodass schnell der Wunsch aufkam die Sprache dahingehend zu erweitern, damit Konstrukte wie wir sie heute von den Header-Dateien kennen überhaupt möglich wurden. Aus heute nicht mehr nachvollziehbaren Gründen wurden diese neuen Aufgaben aber an ein getrenntes Programm ausgelagert, den Präprozessor.
- Präprozessor (preprocessor)
cpp
löst Makros auf, bevor der Code an den Compiler geht
#include <stdio.h>
#define PI 3.141592653589793
#define area(r) ((r) * (r) * PI)
int main(int argc, char** argv) {
printf("%f", area(10));
}
$ cpp < main.c
...
int main(int argc, char** argv) {
printf("%f", ((10) * (10) * 3.141592653589793));
}
Der C-Präprozessor verarbeitet den Quelltext und die Makros bevor der Compiler mit der Arbeit beginnt. Korrekterweise muss man allerdings anmerken, dass der Präprozessor heute Bestandteil des Compilers geworden ist, um die Geschwindigkeit zu erhöhen. Er bleibt aber technisch vom eigentlichen Compiler getrennt und hat seine eigene Syntax. Über das Kommando cpp
kann man den Präprozessor direkt aufrufen.
Der Präprozessor weiß auch nicht, welche Sprache er verarbeitet, wie das folgende Beispiel zeigt:
#define BEWERTUNG super
#define NOTE 1
Die Vorlesung PR3 ist BEWERTUNG. Ich gebe ihr eine NOTE.
$ cpp non_c.txt
Die Vorlesung PR3 ist super. Ich gebe ihr eine 1 .
Syntax des Präprozessors
Losgelöst von der Sprache C entwickelt, hat der Präprozessor seine eigene Syntax, die sich durch die #
-Zeichen am Anfang jeder Direktive zeigt.
- Der Präprozessor wird mit Direktiven gesteuert, die immer mit
#
am Zeilenanfang beginnen - Direktiven stehen meistens am Anfang der Datei
- Aufgaben
- Dateien mit
#include
laden - Konstanten mit
#define
definieren - Macros mit
#define
definieren - Bedingte Compilierung mit
#ifdef
,#ifndef
und#endif
- Compiler-Option
-E
zeigt die Ausgaben des Präprozessors an - Präprozessor muss nicht mit dem Compiler benutzt werden (Programm
cpp
)
Wie wir später sehen werden, sollte man in modernem C-Code versuchen, den Präprozessor weitgehend zu vermeiden. Die Direktive, die man allerdings nicht umgehen kann, ist die #include
-Direktive, die auch 53 Jahre nach der Erfindung von C die einzige Möglichkeit bleibt, Dateien zu inkludieren.
#include <DATEINAME>
: Inkludiert einen Standardheader
z. B.#include <stdio.h>
- normalerweise ausgehend von
/user/include
- Suchpfad kann mit der Compileroption
-I
geändert werden - enthalten keine relativen Pfade (
..
) #include "DATEINAME"
: Inkludiert einen Header
z. B.#include "gretter.h"
- relativ zum aktuellen Verzeichnis
- üblicherweise für selbstgeschriebene Header
- enthalten häufig relative Pfade (
..
)
Als Daumenregel gilt:
- Von Ihnen geschriebene Header-Dateien →
#include "DATEINAME"
- Header-Dateien von Bibliotheken, die Sie benutzen →
#include <DATEINAME>
Der C-Präprozessor erlaubt es Makroersetzungen durchzuführen. Hierbei werden mit #define
definierte Ausdrücke im Text gesucht und ersetzt. Die Makros können einfache Konstanten aber auch Ausdrücke mit Variablen sein.
#define NAME [KONSTANTE]
: Definiert eine neue Konstante
z. B.#define PI 3.141592653589793
- Name wird textuell mit Wert im Programm ersetzt
- wird benutzt um globale Konstanten zu definieren
- Wert ist optional (im Zusammenhang mit
#ifdef
) - Name ist im Debugger nicht zu sehen!
#define NAME(p1, p2) AUSDRUCK
: Definiert ein Macro
z. B.#define area(r) ((r) * (r) * PI)
- kann für typunabhängigen Code verwendet werden
- ergibt inline Funktionen (heute oft unnötig)
#undef NAME
: Entfernt eine Konstantendefinition
Beachten Sie als erstes, dass es kein Gleichheitszeichen oder ähnliches zwischen dem Namen und dem Wert des Macros gibt. Name und Wert werden einfach durch ein Leerzeichen getrennt.
Im Anschluss an den Namen kann man Parameter (in Klammern) angeben, die im Macro verwendet werden. Der Präprozessor setzt dann die Parameter in das Macro ein, wenn er es im Text ersetzt. Wichtig ist allerdings zu verstehen, dass der Präprozessor hier keinerlei Intelligenz walten lässt und einfach stupide Ersetzungen durchführt. Angenommen, dass Macro sei #define square ((x)*(x))
und Sie schreiben printf("%d", square("Hallo"));
dann macht der Präprozessor daraus printf("%d", (("Hallo")*("Hallo")));
.
Wenn man eine Macro verwendet, um globale Konstanten zu definieren, z. B. #define MAX_VALUE 42
, dann erfolgt die Ersetzung vor dem Compilieren. D. h. in dem generierten Objekt-File sind alle Informationen zu dem Macro-Namen (MAX_VALUE
) entfernt und dort steht nur noch der eigentliche Wert von 42
. Dies kann das Debuggen von C-Programmen erschwerten.
Die – auf den ersten Blick unnötigen – Klammern im Macro #define area(r) ((r) * (r) * PI)
sind wichtig, damit es bei der Expansion des Makros nicht zu fehlerhaftem C-Code kommt.
Das folgende Beispiel zeig das Problem. Der Code
#define PI 3.141592653589793
#define area(r) (r * r * PI)
int main(int argc, char** argv) {
double r = area(3.0 + 1.0)
}
wird expandiert zu
int main(int argc, char** argv) {
double r = (3.0 + 1.0 * 3.0 + 1.0 * 3.141592653589793);
}
was auf jeden Fall falsch ist.
Für Macros gibt es zwei Argumente:
- Sie sind schneller als Funktionen: Da das Macro vor dem Compilieren ersetzt wird, finde an der Stelle kein Funktionsaufruf statt, sondern der Ausdruck wird direkt eingesetzt. Das Macro verhält sich somit wie eine Inline-Funktion. Moderne Compiler führen aber ohnehin bei der Optimierung des Codes ein Inlining von kurzen Funktionen durch, sodass dieses Argument heute nur begrenzt gültig ist.
- Man kann damit typunabhängigen Code erzeugen: Der Präprozessor weiß nichts von C-Datentypen, sodass man dasselbe Macro unabhängig vom Typ der Variablen einsetzen kann. Der C-Compiler würde dies nicht akzeptieren, da er eine strenge Typprüfung durchführt und für jeden Parameter einer Funktion ein Typ angegeben werden muss. Als Entwickler muss man sich aber darüber im Klaren sein, dass die Typsicherheit von C ein Feature ist, das man nicht leichtfertig unterlaufen sollte.
Beispiel: Typunabhängiger Code
#include <stdio.h>
#define min(x, y) ((x) < (y) ? x : y)
#define max(x, y) ((x) > (y) ? x : y)
int main(int argc, char** argv) {
printf("%d\n", max(4, 5));
printf("%f\n", max(4.3, 5.7));
printf("%ld\n", min(4L, 7L));
}
Dieses Beispiel zeigt, wie man ein Macro verwenden kann, um max
und min
so zu definieren, dass es für alle numerischen Datentypen verwendet werden kann. Allerdings kann man sich fragen, ob dies wirklich nötig ist: Die Entwicklerin kennt beim Aufruf der min
- oder max
-Funktion den Datentyp und könnte auch problemlos eine passende C-Funktion aufrufen.
#include <stdio.h>
int int_min(int x, int y) { return (x < y ? x : y); }
long long_min(long x, long y) { return (x < y ? x : y); }
double double_min(double x, double y) { return (x < y ? x : y); }
int main(int argc, char** argv) {
printf("%d\n", int_min(4, 5));
printf("%f\n", double_min(4.3, 5.7));
printf("%ld\n", long_min(4L, 7L));
}
Der Aufwand ist nur geringfügig höher und Macros werden vollständig vermieden.
Ein Grund Macros zu vermeiden liegt darin, dass Macros keine Funktionen sind.
Macros sind keine Funktionen
Einer der gängigen Fehler bei der Verwendung von Macros ist, sie sich als C-Funktionen vorzustellen. Wegen der identischen Syntax bei der Verwendung (area(20)
) kann dies leicht passieren. Sie sind aber keine Funktionen, sondern rein textuelle Ersetzungen.
- Macros sind keine Funktionen, sondern textuelle Ersetzungen!
#include <stdio.h>
#define valid(x) ((x) > 0 && (x) < 20)
int main(int argc, char** argv) {
int x = 0;
if (valid(x++)) { /* tu was */ }
printf("%d\n", x);
x = 1;
if (valid(x++)) { /* tu was */ }
printf("%d\n", x);
}
1
3
Die Ausgabe versteht man, wenn man das Macro textuell ersetzt. Dann sieht der Code wie folgt aus:
int main(int argc, char** argv) {
int x = 0;
if (((x++) > 0 && (x++) < 20)) { }
printf("%d\n", x);
x = 1;
if (((x++) > 0 && (x++) < 20)) { }
printf("%d\n", x);
}
Wenn x > 0 ist, werden beide Bedingungen geprüft und die Variable wird zweimal inkrementiert. Dies liegt daran, dass das Macro kein Funktionsaufruf ist, bei dem die Parameter auf den Stack geschrieben werden, sondern eine textuelle Ersetzung, bei der x++
einfach in das Macro eingesetzt wird.
Bedingte Compilierung
Mithilfe des C-Präprozessor kann man eine Bedingte Compilierung durchführen, d. h. abhängig von dem Wert eines Ausdrucks werden einzelne Teile des Codes ausgeblendet oder eben nicht.
#if AUSDRUCK
/* code 1 */
#elif AUSDRUCK2
/* code 2 */
#else
/* code 3 */
#endif
- Präprozessor prüft
AUSDRUCK
im#if
- wenn der Ausdruck wahr ist, wird der erste Code-Bereich ausgegeben
- wenn er falsch ist, werden die nächsten
#elif
-Ausdrücke geprüft - wenn kein Ausdruck wahr ist, wird der
#else
-Bereich ausgegeben
Welche Arten von Ausdrücken sind in einem #if
erlaubt? Es sind alle C-Ausdrücke erlaubt, die sich als arithmetischer Ausdruck auffassen lassen, also zu 0
oder einem anderen Zahlen-Wert ausgewertet werden:
- Zeichenvergleiche (keine Anführungszeichen!)
#if OS == linux
- Numerische Vergleiche
#if VERSION == 5
#if VERSION > 5
#if VERSION + 2 > 5
- Logische Operationen
#if VERSION == 5 && OS == linux
#if !(VERSION == 5)
- Prüfung auf Existenz eines Macros mit
defined
(besser#ifdef
und#ifndef
)#if defined OS
Die Macros werden vor dem Vergleich einfach wieder textuell ersetzt, d. h. aus
#define OS linux
#define VERSION 5
#if OS == linux && VERSION == 5
wird
#if linux == linux && 5 == 5
Da die Macros vom C-Präprozessor und nicht vom C-Compiler ausgewertet werden, können keine Ausdrücke verwendet werden, die sich auf dem C-Typsystem abstützen, z. B. ist kein sizeof()
-Operator verfügbar.
Wenn ein Macro nicht gesetzt ist, also undefiniert, dann wird in numerischen Vergleichen einfach angenommen, dass es den Wert 0
hat. Somit kann man#if defined BUFSIZE && BUFSIZE >= 1024
vereinfachen zu #if BUFSIZE >= 1024
.
Will man Code einfach von der Compilierung ausnehmen aber keinen Kommentar verwenden (z. B. weil Kommentare nicht geschachtelt werden können), kann man einfach #if 0
verwenden:
#if 0
/* wird nicht ausgewertet */
#endif
Die bedingte Compilierung wird häufig benutzt, um plattformspezifischen Code zu schreiben.
Das folgende Beispiel zeigt, wie man die bedingte Compilierung verwendet, um Code für verschiedene Betriebssysteme zu schreiben.
#define OS linux
#if OS == linux
/* Linux Code */
#else
/* Was anderes als Linux */
#endif /* OS == linux */
Das Macro OS
würde in der Praxis nicht im Quelltext definiert, sondern von außen über den Compiler mit einem entsprechenden Schalter gesetzt. Der C-Compiler setzt bereits eine Reihe von Macros, um plattformabhängigen Code einfach zu ermöglichen, z. B. gibt es ein Macro __cplusplus
, das anzeigt, ob ein C++ oder „nur“ ein C-Compiler im Einsatz sind.
Da man bei Präprozessor-Makros keine Schachtelung durch Einrückung darstellen kann, hat es sich eingebürgert, beim #endif
zu notieren, zu welchem #if
es gehört.
Will man nur auf die Existenz eines Macros prüfen, dann bietet sich mit #ifdef
und #ifndef
ein einfacher Mechanismus an.
#ifdef NAME
/* code 1 */
#else
/* code 2 */
#endif
- Präprozessor prüft, ob
NAME
mit#define NAME
definiert wurde und gibt abhängig den ersten oder zweiten Codeblock aus - Für den umgekehrten Fall gibt es noch
#ifndef
Der Code aus dem Beispiel ist identisch mit:
#if defined NAME
/* code 1 */
#else
/* code 2 */
#endif
Man kann anstatt #if defined NAME
auch etwas eleganter #if defined(NAME)
schreiben. So fügt sich die Konstruktion etwas besser in den Stil des umgebenden C-Programms ein.
Schutz vor doppelter Inklusion
- Doppelte Definitionen sind in C problematisch
- Header-Dateien werden oft mehrfach (direkt und indirekt) inkludiert ⇒ Fehler
- Lösung: Header-File mit Präprozessor vor doppelter Inklusion schützen
/* Konvention: DATEINAME */
#ifndef MYHEADER_H
#define MYHEADER_H
/* Deklarationen */
#endif /* MYHEADER_H */
Die Gründe für dieses Vorgehen wurden schon weiter oben detailliert besprochen, sodass hier auf die Details verzichtet wird.
Vordefinierte Variablen
Der C-Standard erfordert, dass bestimmte, vordefinierte Macros vom Compiler automatisch gesetzt werden und vom C-Quelltext verwendet werden können. Einige der wichtigsten sind:
__FILE__
: Name der aktuellen Quelltextdatei__LINE__
: laufende Zeile der aktuellen Quelltextdatei__DATE__
: aktuelles Datum__TIME__
: aktuelle Uhrzeit__cplusplus
: gesetzt, wenn der Compiler C++ unterstützt- …
Verwendung beispielsweise für Testausgaben von Variablen:
printf("file %s, line %d, Wert: %ld\n", __FILE__, __LINE__, wert);
There will be Dragons
Der Präprozessor ist ein Werkzeug, mit dem man viel machen, aber auch viel falsch machen kann.
- Der Präprozessor stammt aus der Computer-Steinzeit
- Der Präprozessor ist ein steter Quell von Problemen
- Man sollte Präprozessor-Direktiven nur sehr sparsam einsetzen
- Es gibt für fast alle Anwendungszwecke Alternativen direkt in C
#define INT16 ...
→typedef
#define MAXLEN 256
→const
#define max(a,b) ...
→ Funktionen- Auskommentieren von Code → Versionsverwaltung
Eine per Macro durchgeführte Typdefinition kann man einfach durch ein typedef
ersetzen:
#define INT16 short
void f(INT16 param) {
/* tu was */
}
kann man auch schreiben als:
typedef short int16;
void f(int16 param) {
/* tu was */
}
Konstanten lassen sich durch C-Konstanten ersetzen:
#include <unistd.h>
#define MAXLEN 256
int main(int argc, char** argv) {
char buffer[MAXLEN];
read(0, buffer, MAXLEN);
}
wird zu
#include <unistd.h>
const int MAXLEN = 256;
int main(int argc, char** argv) {
char buffer[MAXLEN];
read(0, buffer, MAXLEN);
}
Bei der bedingten Compilierung ist der Präprozessor aber bis heute nicht wegzudenken. Wenn man ein Programm auf verschiedenen Plattformen compilieren möchte, kann man die plattformabhängigen Teile durch entsprechende Präprozessor-Anweisungen ein- bzw. ausblenden. Da man die Präprozessor-Makros beim Compilieren mit -D
setzen kann, ist es möglich denselben Quelltext passend zu compilieren.