C-Präprozessor

Aus der Mikrocontroller.net Artikelsammlung, mit Beiträgen verschiedener Autoren (siehe Versionsgeschichte)
Wechseln zu: Navigation, Suche

Der erste Verarbeitungsschritt bei der Kompilierung eines C/C++-Programmes erfolgt durch den Präprozessor. Dieser verändert den Quelltext, den die späteren Verarbeitungsphasen erhalten, in folgender Hinsicht:

  • Einbeziehen zusätzlicher Dateien (Include-Files)
  • Ersetzen von (parametrisierbaren) Makros
  • Entfernen einzelner Abschnitte (= bedingte Kompilierung)

Im Grunde kann man sich den Präprozessor als eine Art Texteditor vorstellen, der die Anweisungen, was er zu tun hat, dem Text entnimmt, den er bearbeitet. In jedem Texteditor gibt es zb. die Funktion 'Suchen und Ersetzen'. Auch im Präprozessor gibt es sie, nur heißt sie dort #define. Alle Anweisungen an den Präprozessor beginnen grundsätzlich damit, daß das Zeichen '#' das erste Zeichen in einer Textzeile darstellt. Und umgekehrt: Ist das erste Zeichen in einer Textzeile ein '#', so handelt es sich um eine Präprozessor-Anweisung.

#include

Die #include weist den Präprozessor an, den Inhalt der angegebenen Datei anstelle der #include Anweisung einzusetzen. Weiter passiert nichts. Bei der Angabe des Dateinamens der einzusetzenden Datei gibt es 2 Formen

#include "Datei1.xyz"
#include <Datei2.abc>

Der Unterschied zwischen beiden Formen besteht rein im Aufsuchpfad, den der Präprozessor benutzt, um die Datei zu finden. Per Konvention wird die < >-Form benutzt, um systemweite Includes durchzuführen. Alle mit dem Compiler mitgelieferten Header Files sind zb. solche systemweite-Includes. Bei der Installation des Compilers wurde im System hinterlassen, auf welchem Pfad sie gefunden werden können. Durch Verwendung der < >-Form wird dem Präprozessor mitgeteilt, dass diese damals vereinbarten Pfadangaben zur Aufsuche dieser Datei benutzt werden soll.

#define

Mittels #define wird eine Textersetzung vereinbart.

#define ABC xyz

weist den Präprozessor an, im weiteren Quelltext alle Vorkommen von 'ABC' durch den Text 'xyz' zu ersetzen. Der Präprozessor macht dies überall, solange

  • es sich an der zu ersetzenden Stelle um keinen String handelt. Mit obigem #define würde also in "Dies ist ABC" keine Textersetzung stattfinden.
  • er den Ursprungstext als 'Wort' im Sinne eines C-Wortes handelt. Mit obigem #define würde also in cdABCef = 5; keine Textersetzung stattfinden.

Wichtig ist: Der Präprozessor führt eine reine Textersetzung durch! Ob sich durch diese Ersetzung eine Logikänderung im Programm ergibt, interessiert den Präprozessor nicht.

#define NR 5

int Werte[NR];

...

  for( i = 0; i < NR; i++ )
    printf( "%d", Werte[i] );

Bevor der eigentliche Compiler den Quelltext zu Gesicht bekommt, wird er zunächst vom Präprozessor bearbeitet. Dieser führt die Textersetzung durch, indem er alle Vorkommen von NR durch den Text 5 ersetzt. Erst dieses Ergebnis

int Werte[5];

...

  for( i = 0; i < 5; i++ )
    printf( "%d", Werte[i] );

wird dann dem eigentlichen Compiler zur Übersetzung vorgelegt. In diesem Beispiel hat man durch den Einsatz des Präprozessors erreicht, dass die Anzahl der Arrayelemente immer mit dem Maximalwert in der for-Schleife übereinstimmt. Ein Fehler, dass beispielweise die Arraygröße verändert wird, ohne das die for-Schleife angepasst würde, ist durch den Einsatz des Präprozessors wirkungsvoll verhindert worden.

Aber auch hier wieder: Der Präprozessors macht nur eine Textersetzung! Für den Präprozessors ist es völlig unerheblich, ob sich dadurch die Logik des Programms aus Sicht des Programmierers verändert

#define PART    3+5

...

  y = 4*PART;

In diesem Beispiel mag es schon so sein, dass der Programmierer im Sinn hatte, den Ausdruck 4 mal 8 berechnen zu lassen, wobei sich die 8 durch eine Addition von 3 und 5 ergeben. Das interessiert aber den Präprozessor nicht. Der macht eine reine textuelle Ersetzung, indem er den Text 'PART' durch den Text '3+5' austauscht, wodurch dieses Ergebnis entsteht

#define PART    3+5

...

  y = 4*3+5;

Dies ist aber etwas anderes, nämlich '(4*3)+5', also ursprünglich beabsichtigt war, nämlich '4*(3+5)'.

Schlussfolgerung: Es ist bei komplexeren Makros nicht ungewöhnlich, dass sich in Makros relativ viele Klammern wiederfinden, deren Zweck gerade für einen Neuling nicht auf den ersten Blick zu durchschauen ist.

#define PART    (3+5)

...
  x = PART;
  y = 4*PART;

Während die Klammern im Makro bei der Zuweisung an x keine Funktion erfüllen (aber auch nicht störend sind), sind sie bei der Zuweisung an y lebenswichtig um der ganzen Anweisung nach der Textersetzung die beabsichtigte Bedeutung zu geben.

#define für eine Codesequenz

Wird ein #define nicht für einen Ausdruck verwendet, sondern für eine Codesequenz wie in

#define F(n) { a = n; b = n; }

dann entsteht ein Problem bei

if (...) F(1); else F(2);

da hier für den Compiler letztlich

if (...) { ... }; else { ... };

stehen bleibt, und damit ein überzähliges Semikolon vor dem else. Nun kann man dies natürlich mit

if (...) { F(1); } else { F(2); }

oder

if (...) F(1) else F(2)

abhandeln. Gelegentlich findet man deshalb aber auch seltsam anmutende Definitionen wie

#define F(n) do{ a = n; b = n; }while(0)

#if, #ifdef

Die Statements #if und #elsif sind die einzigen Präprozessor-Statements, in denen er selbst Berechnungen durchführt. In allen anderen Fällen findet eine reine Textersetzung statt und es rechnet erst der Compiler oder der Prozessor. Da der Präprozessor aber nach eigenen Regeln rechnet, wird Code wie

#define N (1000000*1000000)
#if N==1000000000000
   printf("%Ld\n", (long long)N);
#endif

oft zur Ausgabe von -727379968 führen. Wenn der Präprozessor mit 64 Bits rechnet und der Compiler mit 32 Bits.

mögliche Probleme beim Einsatz des Präprozessors

Eine am C-Präprozessor häufig geäußerte Kritik ist, dass er (nahezu) ohne Berücksichtigung der eigentlichen Sprachsyntax arbeitet ("The C-Preprocessor doesn't know about C"). Die Tatsache, dass Makros beispielsweise auf der Basis von Textersatz arbeiten, kann zu Überaschungen führen. So wird in

#define cub(a) a*a*a
...
int x, y;
y = 4;
x = cub(y+1);
...

in x nicht etwa der Wert 125 (5 hoch 3) stehen, sondern der Wert 13, da nach Ersetzen des Makros der folgende Quelltext kompiliert wird ...

x = y+1*y+1*y+1;

... und durch die arithmetischen Vorrangregeln, wird dieser Ausdruck so ausgewertet:

x = y + (1*y) + (1*y) + 1;

Deshalb sollte man jeden Parameter eines Makros bei jeder Verwendung klammern. Damit werden viele Probleme mit Makros gelöst und man erhält für obiges Beispiel folgende Form und damit auch eine korrekte Berechnung:

#define cub(a) ((a)*(a)*(a))
...
int x, y;
y = 4;
x = cub(y+1);
...

Ein weiteres Problem besteht jedoch, wenn ein Makro-Parameter im Ersatztext doppelt verwendet wird:

#define max(a, b) ((a>b) ? (a) : (b))
...
int x, y;
...
x = max(y, 10);   /* OK */
x = max(++y, 10); /* ?? */

Im zweiten Fall wird die Variable y u.U. zweimal inkrementiert - was ohne Kenntnis der Makro-Definition keineswegs offensichtlich ist (max könnte auch eine echte Funktion sein).

Die Tatsache, dass der C-Präprozessor die Syntax von C/C++ nicht wirklich berücksichtigt, ist allerdings auch nützlich. So lassen sich mit dem C-Präprozessor Datentypen parametrisieren, um systematische Programmteile zu vereinfachen:

/* GSAWP:
   generiert Funktionsdefinition zum Vertauschen des Inhalts von zwei Variablen
*/
#define GSWAP(n, T)s\
        void n(T *xp, T *yp) { T tmp = *xp; *xp = *yp; *yp = tmp; }
...
GSWAP(iswap, int)
GSWAP(dswap, double)
GSWAP(swap_s, struct s)
...
int a, b;
double c, d;
struct s e, f;
...
iswap(&a, &b);
dswap(&c, &d);
swap_s(&e. &f);

Viele typische Anwendungsfälle des Präprozessors lassen sich allerdings bereits mit Standard-C-Bordmitteln erfolgreich erschlagen:

  • Statt #define besser mit const vereinbarte Variablen benutzen. Bei jedem besseren Compiler ergeben sich dank Optimierung keinerlei Nachteile.
  • enum für Konstantenfelder benutzen. Dabei sollte man allerdings sauber casten, da enum-Werte in der Regel als int interpretiert werden.
  • Neuere Versionen des GCC unterstützen inline, wodurch Makros für Einzeiler überflüssig werden.


In C++ wurden ergänzend zum C-Präprozessor weitere Mechanismen eingeführt. So erlauben Templates generische Programmierung, womit viele der "Makro-Tricks" wie die obigen überflüssig werden.