5 Der Praeprozessor

Vor dem eigentlichen Kompilieren wird ein C-Quelltext immer durch den sogenannten Praeprozessor geschickt. Der Compiler selbst bekommt den Quelltext nie zu sehen, sondern nur die Ausgabe des Praeprozessors. Den Praeprozessor muß man sich als eigenständiges Programm denken, welches vor dem Compiler aufgerufen wird. In der Praxis sind allerdings beide meistens in einem Programm vereinigt. Prinzipiell ist der Praeprozessor aber nicht an die Sprache C gebunden; man könnte mit Hilfe eines eigenständigen Praeprozessors auch die Entwicklung von Programmen in FORTRAN oder Pascal vereinfachen oder andere Aufgaben der Textmanipulation damit erledigen.

C ist jedoch umgekehrt an den Praeprozessor gebunden, da letzterer sowohl im alten K&R-C als auch im ANSI-C genormter Bestandteil der Sprache ist.

Die gesamte Tätigkeit des Praeprozessors beschränkt sich auf Textersatz.

Praeprozessoranweisungen beginnen mit einem #-Zeichen als erstem Zeichen in der Zeile.

Sofern man nicht mit ausdrücklichen Praeprozessorbefehlen etwas anderes vorgibt, wird der Quelltext unverändert an den Compiler weitergereicht.

Die wichtigsten Praeprozessordirektiven sind:

Mit #define kann man zum einen für ein später verwendetes Symbol einen Namen vereinbaren und zum anderen sogenannte funktionsähnliche Makros definieren. Dann folgt dem Namen des zu vereinbarenden Makros ein Klammerpaar mit einer Liste von Parametern (ohne Typangabe). Zwischen dem Namen und der schließenden Klammer dürfen keine Leerzeichen auftreten. Danach folgt in beiden Varianten die Zeichenfolge, durch die der Makroaufruf in Zukunft ersetzt werden soll.

Die Wirkung von #define ist auf den Rest des aktuellen Quelltextes beschränkt, einschließlich der mit #include eingebundenen Dateien (siehe unten).

Beispiel:

Aus

#define PI   3.1415923564
a = 2.0*PI;
b = 1.0+2*(PI+3);
macht der Praeprozessor den Text
a = 2.0*3.1415923564;
b = 1.0+2*(3.1415923564+3);

Der Quelltext wird dann in dieser Form an den Compiler weitergereicht.

Aus

#define sum(a,b)     (a+b)
/* ... */
erg = sum( 1.0, wert );
macht der Praeprozessor:
/* ... */
erg = (1.0+wert);

Man kann auch einen Namen definieren, dem nichts zugeordnet wird.

#define unfug
c = unfug - 3.0;
wird durch den Praeprozessor zu
c = - 3.0;

Das macht erst mal keinen Sinn, ist aber für #ifdef und #ifndef manchmal ganz nützlich; diese werden gleich beschrieben.

Die funktionsähnlichen Makros werden oft eingesetzt, um kurze Funktionen zu ersetzen. Beispielsweise kann

int max( int a, int b )
{
    return ( a>b ? a : b );
}
/* ... */
    gross = max(i,gross);
/* ... */
ersetzt5 werden durch:
#define max(a,b) ( a>b ? a : b )
/* ... */
gross = max(i,gross);
/* ... */

Der Programmtext wird dann vom Praeprozessor ersetzt durch:

/* ... */
gross = ( i>gross ? i : gross );
/* ... */

Diese Form des Programms ist zum einen schneller als die ursprüngliche mit dem Funktionsaufruf, da zur Laufzeit keine Parameter und Rückgabewerte verwaltet werden müssen (sogenannter inline code). Zum anderen ist das Makro auch flexibler einsetzbar als die eigentliche Funktion, da man es auch mit anderen Zahlentypen als mit int aufrufen kann und entsprechend auch den richtigen Ergebnistyp erhält. max( 1.0, 2.0 ) liefert ein Ergebnis vom Typ double, wenn max als Makro vereinbart ist; dagegen würde die oben definierte Funktion max() immer ein int-Ergebnis liefern.

Funktionsähnliche Makros haben aber ihre Tücken. Die Wirkung im C-Quelltext ist manchmal beileibe nicht dieselbe, wie beim tatsächlichen Aufruf von Funktionen.

Die erste Falle besteht in der Auswertungsreihenfolge von Ausdrücken. Funktionsaufrufe werden immer vor allen Berechnungen ausgeführt.

int summe( int a, int b )
{
    return a+b;
}
/* ... */
    c = 5*summe( 1, 3 );

berechnet die Summe von 1 und 3. Danach erst wird das Ergebnis mit 5 multipliziert und der resultierende Wert 20 wird an c zugewiesen.

Mit dem Makro

#define summe(a,b)   a+b
/* ... */
    c = 5*summe( 1, 3 );
glaubt man auf den ersten Blick vielleicht, die gleiche Berechnung durchzuführen. In Wirklichkeit aber wird vom Praeprozessor folgender Text an den Compiler weitergereicht:
/* ... */
    c = 5*1+3;

Dies ist mitnichten gleichwertig, da durch die Bedingung ,,Punkt- vor Strichrechnung`` erst 5 mit 1 multipliziert wird, dann wird 3 addiert. Das Ergebnis (8) wird an c zugewiesen.

Dieses Problem ist aber durch Klammerung leicht zu lösen:

#define summe(a,b)   (a+b)
/* ... */
    c = 5*summe( 1, 3 );
wird vom Praeprozessor zu
/* ... */
    c = 5*(1+3);
expandiert und die Welt ist wieder in Ordnung.

Ein ähnlicher Fehler kann auftreten, wenn die Argumente in der Ersatzzeichenfolge nicht geklammert werden:

#define produkt(a,b)   (a*b)
/* ... */
    a = 4;
    b = 5;
    c = produkt( a, b+1 );

Das wird expandiert zu:

/* ... */
    a = 4;
    b = 5;
    c = ( a*b+1 );

Durch die Punkt- vor Strichbedingung wird erst (4*5) berechnet, dann wird 1 dazugezählt.

Beabsichtigt war wohl eher 4*(5+1); dies kann entweder durch Klammerung beim Aufruf:

#define produkt(a,b)   (a*b)
/* ... */
    a = 4;
    b = 5;
    c = produkt( a, (b+1) );

oder durch Klammerung der Argumente im Makro erreicht werden:

#define produkt(a,b)   ((a)*(b))
/* ... */
    a = 4;
    b = 5;
    c = produkt( a, b+1 );

Der zweite Weg ist wohl der sicherere.

Funktionsähnliche Makros sollte man also im Zweifelsfall immer fleißig klammern.

Eine weitere Tücke liegt darin, daß Makros einen Parameter eventuell mehrfach auswerten. Wird ein Parameter beim Aufruf manipuliert, beispielsweise durch den Operator ++, dann wird diese Operation möglicherweise mehrfach durchgeführt.

int max( int a, int b )
{
    return ( a>b ? a : b );
}
/* ... */
    gross = max(++i,gross);
/* ... */
erhöht i um eins und weist an gross dann den Funktionswert von max() zu.

Mit einem Makro sieht das Programmstück so aus:

#define max(a,b) ( a>b ? a : b )
/* ... */
    gross = max(++i,gross);
/* ... */

Der Programmtext wird dann vom Praeprozessor ersetzt durch:

/* ... */
    gross = ( ++i>gross ? ++i : gross );
/* ... */

Dabei wird ebenfalls erst i um eins erhöht und mit dem Wert von gross verglichen. Ist der Vergleich auf größer erfüllt, dann wird ++i an gross zugewiesen, also i nochmals erhöht. Ist die Bedingung dagegen nicht erfüllt, dann verhält sich das Makro wie der Funktionsaufruf.

Verhindern kann man den Fehler nur, indem man die Parameter nicht beim Aufruf manipuliert, sondern gegebenenfalls vorher oder nachher:

#define max(a,b) ( a>b ? a : b )
/* ... */
    ++i;
    gross = max(i,gross);
/* ... */

Reicht eine Zeile nicht aus, um ein Makro zu definieren, dann kann man an das Ende der ersten Zeile das Zeichen \ stellen und die Definition in der nächsten Zeile fortsetzen; reicht diese auch nicht, dann kann man diese auch wieder fortsetzen etc.; man muß nur aufpassen, daß man hinter die letzte Zeile der Definition kein \ setzt.

Die Argumente in einem Makroaufruf dürfen übrigens wieder Makroaufrufe enthalten; die enthaltenen Aufrufe werden zuerst ausgewertet und dann erst werden die schon expandierten Argumente in dem äußeren Makro verwendet.

Wenn man Makros mit Argumenten definiert, dann kann man vor jeder Verwendung eines Arguments in der Definition das Zeichen # oder die Zeichen ## vorsetzen. Mit # erreicht man, daß das folgende Argument als String expandiert wird: Das Zeichen # wird beim Expandieren entfernt und das Argument vor dem Einsetzen mit Anführungsstrichen " versehen.

Beispiel:

/* pr2str.c 30. 7.95 kw
 */

#define printint(a)   printf( #a " ist %d\n", a )

main()
{
  printint( 2+3 );
}
wird vom Praeprozessor zu folgendem expandiert:
# 1 "pr2str.c"
 




main()
{
  printf( "2+3" " ist %d\n",   2+3   ) ;
}

Die Ausgabe davon lautet:

2+3 ist 5

Mit ## kann man ein Argument mit dem vorhergehenden oder dem nachfolgenden Token zu einem verketten.

/* prcat.c 30. 7.95 kw
 */

#define var(a) variable ## a

main()
{
  int
    variable1,
    variablexy;

  variable1  = 1;
  variablexy = 25;
  printf( "%d, %d\n", var(1), var(xy) );
}

gibt die beiden Variablen variable1 und variablexy aus.

Die (falsche) Version

#define var(a) variable a
macht aus dem Aufruf
printf( "%d, %d\n",  var(1),  var(xy) );
den Text
printf( "%d, %d\n",  variable 1,  variable xy );
und läßt also die einzelnen Token getrennt stehen; mit
#define var(a) variable ## a
dagegen erhält man aus
printf( "%d, %d\n",  var(1),  var(xy) );
nach dem Praeprozessor
printf( "%d, %d\n",  variable1,  variablexy );
und gibt tatsächlich den Wert der beiden Variablen aus.

#undef ist das Gegenstück zu #define. Mit #undef kann man einen bereits vereinbarten Namen oder ein Makro für den Rest des Quelltextes wieder vergessen lassen; sei es, weil man den Namen nur kurz brauchte oder für einen anderen Zweck neu vergeben will.

Das erneute Vergeben von Namen sollte man aber nur wohlüberlegt einsetzen, da der Quelltext in der Regel unübersichtlich wird, wenn ein und derselbe Name mit verschiedenen Werten und Bedeutungen belegt wird.

Wenn man mit #undef eine Definition löscht, die es gar nicht gibt, dann passiert nichts; dies zählt nicht als Fehler. Etwas zu definieren, was bereits definiert ist, ist dagegen ein Fehler. Wenn man also einen Namen definieren möchte, und sich nicht nicht sicher ist, ob er bereits existiert, dann kann man ihn vorher einfach mit #undef löschen, und dann mit #define neu definieren.

#include fügt an der aktuellen Stelle den gesamten Inhalt einer anderen Datei ein. Deren Text durchläuft wiederum den Praeprozessor, bevor er an den Compiler weitergereicht wird. Dadurch kann die eingefügte Datei wieder mit #include eine weitere einlesen lassen.

Der Name der einzulesenden Datei muß entweder in ein Paar von spitzen Klammern (<...>) oder in Gänsefüßchen ("...") eingeschlossen werden.

Gibt man beim Dateinamen den vollen Pfadnamen an, dann sind die beiden Formen gleich. Steht vor dem Dateinamen aber kein Pfad, dann wird bei #include "dateiname" zuerst da nach der Datei gesucht, wo auch der Quelltext herstammt. Für die Dateien, die mit #include <dateiname> eingebunden werden, existiert immer ein vordefinierter Pfad, mit dem diese Dateien gesucht werden.

Bei kommandozeilenorientierten Umgebungen muß man den Pfad meist in einer sogenannten environment-Variablen namens INCLUDE hinterlegen. Unter MSDOS erreicht man das mit dem Kommando SET INCLUDE=pfad. Arbeitet man mit Unix, dann muß man meistens ebenfalls mit set INCLUDE=pfad die Variable setzen, aber eventuell (je nach verwendeter Shell) auch noch exportieren mit export INCLUDE. In Editor-/Compilerumgebungen wie Turbo-C oder Prospero-C wird dieser Pfad als Option irgendwo im Menü eingestellt.

Mit #include <...> fügt man vor allem sogenannte header-Dateien von dritter Seite ein. Das sind Dateien, die beispielsweise vom Compilerhersteller mitgeliefert werden und Vereinbarungen und Makros zur Verwendung der Standardbibliotheken enthalten. Dadurch muß man nicht alle Funktionsdeklarationen und #define-Anweisungen selbst schreiben, sondern fügt einfach die entsprechenden Dateien mit #include ein.

Mit #include "..." dagegen werden meist eigene Vereinbarungen eingefügt. Dies ist in der Regel günstiger, als die Vereinbarungen direkt in den Quelltext einzustreuen, da man bei mittleren und größeren Programmen den ganzen Quelltext irgendwann auf mehrere Dateien verteilt. Ändert sich irgendeine Vereinbarung dann, müßte man diese in allen Quelltexten korrigieren. Steht anstatt der Vereinbarungen in jedem Quelltext dagegen nur #include "sowieso", dann kommen alle Vereinbarungen nur einmal vor und brauchen auch nur an einer Stelle geändert werden. Dadurch spart man letztlich viel Arbeit und hat wesentlich weniger Fehlerquellen. Siehe dazu die Ausführungen in Aufteilung auf mehrere Quelltexte.

#if und seine Anverwandten (#elif, #else, #endif, #ifdef, #ifndef) kann man für bedingte Kompilierung nutzen.

Als Bedingung darf ein C-ähnlicher logischer Ausdruck verwendet werden, der allerdings nur aus Konstanten, Praeprozessormakros und den Operatoren bestehen darf.

Außerdem kann man in der #if-Bedingung mit defined feststellen, ob ein Name als Makro vereinbart ist.

Bei

#define MEIER
#if defined MEIER
    tuwas
#endif
wird tuwas an den Compiler gereicht, bei
#if defined MEIER
    tuwas
#endif
(ohne vorheriges #define MEIER) dagegen nicht.

#if defined ... kann zu #ifdef ... abgekürzt werden, hier also bei dem obigen Beispiel:

#define MEIER
#ifdef MEIER
    tuwas
#endif

Analog wird der Teil, der hinter #ifndef ... steht, genau dann ausgeführt, wenn der angegebene Name nicht definiert ist.

Der Praeprozessor wird oft dazu benutzt, um verschiedene Versionen eines Programms in einem Quelltext halten zu können und mit Änderung einer #define-Anweisung von einer Version zu einer anderen wechseln zu können. Beispielsweise kann man Unterschiede zwischen verschiedenen Rechnersystemen etwa so berücksichtigen:

#define TURBOC     1
#define PROSPEROC  2
#define VMSC       3
#define WELCHESC   TURBOC
/* ... */
#if WELCHESC==TURBOC
     teil1
#elif WELCHESC==PROSPEROC
     teil2
#elif WELCHESC==VMSC
     teil3
#else
     teil4
#endif
/* ... */
Da hier WELCHESC als TURBOC definiert ist, wird nur teil1 an den Compiler weitergereicht. Die Programmstücke teil2, teil3 und teil4 werden dem Compiler vom Praeprozessor vorenthalten und folglich nicht mitübersetzt. So kann man mit Hilfe von Praeprozessordirektiven in einem Quelltext verschiedene Versionen eines Programms halten (beispielsweise für verschiedene Rechnersysteme oder verschiedene Kunden) und die jeweilige Version nur an einer Stelle mit einem #define auswählen.

Oft wird ein Programm während der Entwicklung mit ausführlicheren Fehlermeldungen versehen, die mit

#ifdef DEBUG
    /* ... */
#endif
geklammert sind. Will man die Fehlermeldungen für Testzwecke haben, schreibt man an den Anfang des Quelltextes #define DEBUG und hat nach dem Übersetzen die entsprechenden Programmteile mitübersetzt. Ist das Programm fertig ausgetestet, dann kann man #define DEBUG wieder entfernen und hat nach dem Übersetzen das entsprechend kleinere Programm zur Verfügung. Treten Fehler auf: kein Problem, #define DEBUG wieder einsetzen und schon kann man weiter austesten!

Es hat sich als sehr zweckmässig erwiesen, während der Programmentwicklung die Anweisung #define DEBUG an Beginn eines jeden Quelltextes beziehungsweise einer überall eingebundenen Headerdatei zu stellen und nach Entwicklungsende für die Produktionsversion die Zeile gar nicht erst zu entfernen, sondern in #define NDEBUG umzuändern. Zum einen kann man so zwischen Test- und Produktionsversion jeweils mit einem Tastendruck wechseln, zum anderen hat man dadurch in der Produktionsversion gleichzeitig alle assert()-Aufrufe stillgelegt (siehe assert() beziehungsweise Fehlersuche mit Standard-C).

#line manipuliert die Zeilennumerierung des Compilers.

Angenommen, in der Zeile 25 steht die Direktive

#line 2000
, dann zählt der Compiler ab dieser Stelle die Zeilennummern weiter, als ob die Zeile mit der #line-Anweisung die Nummer 2000 gehabt hätte. Die folgende Zeile wird 2001, und so fort.

#error gibt während der Übersetzung eine Fehlermeldung auf die Standardfehlerausgabe (inklusive der Token, die der #error-Anweisung folgen) und beendet die Kompilation.

Beispiel:

#error Mist, alles Muell!
gibt den Text Mist, alles Muell aus und beendet die Übersetzung.

Diese Anweisung ist beispielsweise sinnvoll, wenn man mehrere Versionen eines Programms in einem Quelltext hält und mit Praeprozessordirektiven dazwischen umschalten kann. Dann kann es vorkommen, daß man bei Erweiterungen der Variationen an einer Stelle die neue Variante vergißt. Dies kann man von vornherein unterbinden, wenn man in alle #if - #elif - #endif-Entscheidungen noch einen #else-Zweig mit einer #error-Anweisung einsetzt.

Beispiel:

#define     ATARI   1
#define     MSDOOF  2
#define     BSD_V   3

#define     RECHNER     ATARI   /* ATARI, MSDOOF, MSDOOF */

/* ... */

#if RECHNER==ATARI
    /* Atari-spezifisches
     */
    /* ... */
#elif RECHNER==MSDOOF
    /* Pech gehabt!
     */
    /* ... */
#elif RECHNER==BSD_V
    /* Unix-spezifisches
     */
    /* ... */
#else
    #error RECHNER ist kein gueltiger Rechner!
#endif
/* ... */

Wird dann zu einem späteren Zeitpunkt ein weiterer Rechnertyp angefügt (zum Beispiel mit #define SUN 4) und mit #define RECHNER SUN auch ausprobiert, dann vergißt man natürlich leicht, eine der #if-Entscheidungen zu ergänzen. Dann kommt die #error-Anweisung zum Zuge und die Kompilation wird mit Angabe der Fehlerstelle und dem Text:

4 ist kein gueltiger Rechner!
abgebrochen.

#pragma ist vorgesehen, um bestimmten Compilern Mitteilungen zu machen. Ein Compiler darf jede #pragma-Direktive übergehen, wenn er will, oder ihren Inhalt nicht versteht. Die Verwendung ist also systemabhängig und hier nicht von Interesse.

Eine mögliche Anwendung ist, einen bestimmten Compiler irgendwo im Quelltext zu bestimmten Optimierungen zu ermuntern oder davon abzuhalten.

Vordefinierte Makros sind __LINE__, __FILE__, __DATE__ und __TIME__. Sie sind während der Übersetzung schon vordefiniert und enthalten die aktuelle Zeilennummer (als ganze Zahl), den Namen der gerade übersetzten Datei (als Stringkonstante), das aktuelle Datum (als Stringkonstante, z.B. "Jan 01 1991") bzw. die aktuelle Uhrzeit (ebenfalls als Stringkonstante).

__STDC__ ist mit 1 definiert, wenn der verwendete Compiler den ANSI-Standard unterstützt, ansonsten nicht.

Diese Makros dürfen nur verwendet werden, aber nicht manipuliert!

Je nach Compilersystem können noch weitere Namen definiert sein.

AnyWare@Wachtler.de