www.mikrocontroller.net

C-Präprozessor

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.

Inhaltsverzeichnis

[Bearbeiten] #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.

[Bearbeiten] #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.

[Bearbeiten] #if, #ifdef

[Bearbeiten] 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.

webmaster@mikrocontroller.netImpressumNutzungsbedingungenWerbung auf Mikrocontroller.net