Vorverarbeiterdirektiven in C
Die Eingabe eines C -Übersetzer wird von einem Vorverarbeiter (Präprozessor, preprocessor ) erzeugt, der durch Vorverarbeiterdirektiven gesteuert werden kann.
Eine solche Direktive wird durch ein Nummernzeichen "#" eingeleitet, das entweder das erste Zeichen einer Quelldatei sein muß (dem noch Leeraum ohne Neuzeilenzeichen vorangehen kann) oder auf Leerraum folgen muß, der mindestens ein Neuzeilenzeichen enthält. Eine Direktive wird durch ein Neuzeilenzeichen abgeschlossen. Man kann sich vereinfacht merken, daß eine Direktive mit einem Nummernzeichen "#" beginnen muß und immer alleine auf einer Zeile stehen muß.
Quelltextdefinitionen
Ein Teil des Quelltextes kann durch eine „Ersatzliste“ (vereinfacht gesagt: durch einen bestimmten Text) ersetzt werden. Dazu kann eine define -Steuerzeile 〈control-line 〉 verwendet werden.
- 〈control-line 〉 ::=
- "#" "define" 〈identifier 〉 〈replacement-list 〉 〈new-line 〉.
Solch eine Steuerzeile bindet einen Bezeichner 〈identifier 〉 an eine Ersatzliste 〈replacement-list 〉. Nach dieser Zeile werden bis auf weiteres alle Vorkommen des Bezeichners 〈identifier 〉 durch die Ersatzliste 〈replacement-list 〉 ersetzt.
Die auf diese Weise erzeugte Bindung einer Ersatzliste an einen Bezeichner wird auch Makro genannt.
Das folgende Beispiel könnte von einem Programmierer stammen, der die geschweiften Klammern immer übersieht und deshalb an ihrer statt lieber die Bezeichner "BEGIN" und "END" schreiben will. Außerdem ist er der Meinung, die Hauptfunktion sollte nicht mit dem Kopf "int main( void )" beginnen, sondern mit der Überschrift "PROGRAM". Daher definiert er diese Bezeichner zunächst entsprechend mithilfe von drei #define -Direktiven. Danach kann er eine an seine Vorstellungen angepaßte „eigene Programmiersprache“ benutzen. Allerdings ist das für die Praxis nicht zu empfehlen, weil solche Privatsprachen schlecht zu lesen sind und meistens keine wirkliche Verbesserung darstellen. Hier dient es nur als Beispiel.
hallo.c
#include <stdio.h>
#define PROGRAM int main( void )
#define BEGIN {
#define END ;}
PROGRAM
BEGIN
printf( "Hallo!\n" )
END
Obwohl die Präprozessorbezeichner auch mit kleinen Buchstaben geschrieben werden könnten, hat es sich eingebürgert, sie ganz mit großen Buchstaben zu schreiben, damit sie leicht erkannt werden können und um zur Vermeidung von Namenskollisionen eine Art von speziellen „Namensraum“ für sie vorzubehalten.
Der C -Vorverarbeiter ersetzt das Auftreten der definierten Bezeichner durch ihre Ersatzliste und erstellt so einen Vorverarbeiter-Ersatztext für den Quellcode, den er an den Übersetzer weiterreicht. Der Übersetzer sieht dann weder die #define -Direktiven noch die definierten Bezeichner, sondern nur noch den Vorverarbeiter-Ersatztext.
Vorverarbeiter-Ersatztext fuer die Quelldatei "hallo.c" [Ausschnitt]
int main( void )
{
printf( "Hallo!\n" )
;}stdout
Hallo!
Innerhalb von Textliteralen werden Makros nicht ersetzt.
empty.c
#include <stdio.h>
#define ABC 123
int main( void )
{ printf( "ABC%d\n", ABC ); }Vorverarbeiter-Ersatztext fuer die Quelldatei "empty.c" [Ausschnitt]
int main( void )
{ printf( "ABC%d\n", 123 ); }stdout
ABC123
Auch Literale könnten mit Makrodefinitionen benannt werden, doch gibt es in C dafür besser geeignete Sprachelemente, die in anderen Lektionen behandelt werden. Das Beispielprogramm "pi.c" zeigt auch die Verwendung von Dokumentationskommentaren vor Makrodefinitionen.
pi.c
#include <stdio.h>
/// Kreiszahl Pi
/** Verhaeltnis des Kreisumfanges zum Kreisradius. */
#define PI 3.14159265358979323846264338327950288419716939937510
int main( void )
{ printf( "%g\n", PI ); }Vorverarbeiter-Ersatztext fuer die Quelldatei "pi.c" [Ausschnitt]
int main( void )
{ printf( "%g\n", 3.14159265358979323846264338327950288419716939937510 ); }stdout
3.14159
»NDEBUG«
main.c
#define NDEBUG 1
#include <assert.h>
#include <stdio.h>/** @brief Ergibt den Kehrwert des Argumentwertes
@param i darf nicht 0 sein
@return Kehrwert des Argumentwertes
@pre i ist nicht gleich 0 */int kehrwert( int const i )
{ assert( i ); /* Vorbedingung */
return 1 / i; }int main( void )
{ printf( "%d\n", kehrwert( 1 ));
printf( "%d\n", kehrwert( 0 )); }
Bedingte Weitergabe
Ob ein Teil des Quelltextes vom Vorverarbeiter überhaupt an den Übersetzer weitergeleitet wird, kann ebenfalls noch festgelegt werden. Die ifdef -Steuerzeile kennzeichnet die folgenden Zeilen bis zur nächsten endif -Steuerzeile (unter Berücksichtigung möglicher verschachtelter ifdef -endif -Paare) so, daß diese nur an den Übersetzer weitergeleitet werden, wenn der auf das Vorverarbeiter-Schlüsselwort "ifdef" folgende Bezeichner definiert wurde. Die ifndef -Steuerzeile leitet entsprechend nur weiter, wenn der Bezeichner nicht definiert wurde.
Die Syntax der erwähnten Zeilen sieht—etwas vereinfacht—folgendermaßen aus.
- 〈if-line 〉 ::=
- "#" "ifdef" 〈identifier 〉 〈new-line 〉.
- 〈if-line 〉 ::=
- "#" "ifndef" 〈identifier 〉 〈new-line 〉.
- 〈endif-line 〉 ::=
- "#" "endif" 〈new-line 〉.
In dem folgenden Beispiel wurde ein Program so präpariert, daß es in Englisch oder in Deutsch kommuniziert, je nachdem, ob vor der Übersetzung ein Makro mit dem Namen "ENGLISH" definiert wurde. So kann also die Sprache des Programms schon bei der Übersetzung festgelegt werden. Ist das Makro "ENGLISH" definiert, erfolgt die Ausgabe auf Englisch, sonst auf Deutsch.
hello.c
#include <stdio.h>
#define ENGLISH 1
int main( void )
{ printf(
#ifdef ENGLISH
"Hello world!\n"
#endif
#ifndef ENGLISH
"Hallo, Welt!\n"
#endif
); }Vorverarbeiter-Ersatztext fuer die Quelldatei "hello.c" [Ausschnitt]
int main( void )
{ printf(
"Hello world!\n"
); }stdout
Hello world!
Würde das Programm erst während seines Ablaufes jedesmal die Sprache untersuchen und beachten, würde es dadurch nämlich gebremst werden, und die Programmteile zur Ausgabe der nicht gewünschten Sprache würden unnötig Speicherplatz belegen. Andererseits kann die Sprache bei Verwendung dieser Technik nicht während des Programmablaufs verändert werden, das Programm muß zur Änderung der Sprache neu übersetzt werden.
Mithilfe des Vorverarbeiters können auch Zeilen „auskommentiert“ werden, also Zeilen vor dem Übersetzer „versteckt“ werden. Da Kommentare, die mit "/*" eingeleitet wurden, nicht verschachtelt werden können, ist es nämlich nicht immer möglich, diese Kommentare dafür zu verwenden, wenn in dem auszukommentierenden Programmstück schon solche Kommentare verwendet wurden. Außerdem sind Kommentare ja nicht für diesen Zweck gedacht, sondern für erklärende Anmerkungen zum Programm. Die Folge der Verwendung von Kommentaren zum Verstecken von Programmteilen wäre dann eine irritierende Darstellung des Codes in bestimmten C -Editoren.
Das folgende Beispiel geht von der Annahme aus, daß ein Makro "UNDEFINED_" bisher nicht definiert wurde. Das kann auch durch eine Prüfung sichergestellt werden, die mit der error -Direktive eine Fehlermeldung erzeugt, falls das Makro doch schon definiert sein sollte.
comment.c
#include <stdio.h>
#ifdef UNDEFINED_
#error
#endif
int main( void )
{ printf( "Hallo!\n" );
#ifdef UNDEFINED_
/* frühere Version: */
printf( "Hallo, Welt!\n" ); /* Begruessung */
#endif
}Vorverarbeiter-Ersatztext fuer die Quelldatei "comment.c" [Ausschnitt]
int main( void )
{ printf( "Hallo!\n" );
}stdout
Hallo!
Falls ein Programmteil dauerhaft nicht mehr genutzt wird, ist es aber übersichtlicher, ihn ganz aus dem Programm zu entfernen. Wenn er noch aufbewahrt werden soll, kann er ja auch in einer separaten Datei gespeichert werden.
Ein Bezeichner kann auch an die leere Ersatzliste gebunden werden. Er gilt dann als definiert, obwohl der Ersatztext leer ist.
empty.c
#include <stdio.h>
#define NOTHING
int main( void )
{
#ifdef NOTHING
printf( "Hal" NOTHING "lo!\n" );
#endif
}Vorverarbeiter-Ersatztext fuer die Quelldatei "empty.c" [Ausschnitt]
int main( void )
{
printf( "Hal" "lo!\n" );
}stdout
Hallo!
Einfügung
Eine Einfügedirektive weist den Vorverarbeiter an, die Einfügedirektive durch den Inhalt einer Datei zu ersetzen.
- 〈control-line 〉 ::=
- "#" "include" 〈pp-tokens 〉 〈new-line 〉.
Wenn viele Programme beispielsweise eine Prüfung enthalten sollen, die sicherstellt, daß der Bezeichner "UNDEFINED_" nicht als Makro definiert wurde, dann kann diese Prüfung einmal in eine Kopfdatei "undefined.h" geschrieben und dann bei Bedarf eingefügt werden.
Es ist üblich, daß der Name solch einer Datei durch die Endung ".h" (wie Header ) beendet wird. Solche Dateien werden meistens am Anfang („Kopf“) einer anderen Datei eingefügt.
undefined.h
#ifdef UNDEFINED_
#error
#endifinclude.c
#include <stdio.h>
#include "undefined.h"
int main( void )
{ printf( "Hallo!\n" ); }
Die Kopfdirektiven mit spitzen Klammern, wie beispielsweise "#include <stdio.h>", sehen zwar ähnlich aus wie Einfügedirektiven mit Anführungszeichen, wie beispielsweise '#include "mystream.h"', aber müssen nicht immer gleich implementiert sein. Sie teilen dem Übersetzer mit, daß das Programm bestimmte Teile der Standardbibliothek verwenden will. Wenn ein Programmierer eine Datei einfügen will, schreibt er den Dateinamen (oder einen anderen Pfad) in Anführungszeichen. Um die Verwendung eines Teiles der Standardbibliothek anzuzeigen, wird der Name dieses Teils hingegen in spitze Klammern (das Zeichen Kleiner "<" und das Zeichen Größer ">") eingeschlossen. Diese Schreibweise stellt dann eine Information für den Übersetzer dar und muß nicht unbedingt das Einfügen einer gleichnamigen Datei durch den Vorverarbeiter bedeuten.
Obwohl die Direktiven mit spitzen Klammern also nicht wirklich eine Datei einfügen müssen, stammen sie doch aus einer Zeit, in der das üblich war, und auch heute passiert es noch häufig. Die eingefügten Dateien enthalten dann oft Funktionsdeklarationen (Prototypen) für bestimmte Funktionen der Standardbibliothek, so daß ein Programm diese dann so aufrufen kann, daß die richtige Verwendung auch vom Übersetzer geprüft werden kann. Falls die Direktiven nicht wirklich Dateien einfügen, so hat ihre Verwendung dann aber jedenfalls dieselbe Wirkung, als ob bestimmte Funktionsdeklarationen eingefügt worden seien. Daher ist es notwendig, vor Verwendung bestimmter Teile der Standardbibliothek entsprechende Kopfdirektiven zu verwenden.
Wenn darauf verzichtet wird, eine eventuelle Einfügung aufgrund der Direktiven mit Namen von Teilen der Standardbibliothek und das Ergebnis der ifdef -Direktive darzustellen, dann ergibt sich nach dem Einfügen der Datei "undefined.h" der in der Abbildung dargestellte Ersatztext für die Quelldatei "include.c".
Vorverarbeiter-Ersatztext fuer die Quelldatei "include.c" [Ausschnitt]
#include <stdio.h>
#ifdef UNDEFINED_
#error
#endif
int main( void )
{ printf( "Hallo!\n" ); }stdout
Hallo!
Einfügeschutz
Gelegentlich kann es unerwünscht sein, eine Datei doppelt einzufügen. Das kann aber versehentlich passieren, wenn eine andere Datei eingefügt wird, die ihrerseits eine schon eingefügte Datei erneut einfügt. Um zu verhindern, daß sich mehrfach eingefügte Quelltextteile wiederholt auswirken oder durch unnötige erneute Verarbeitung die Übersetzung bremsen, kann ein einfacher Einfügeschutz (include guard ) mit dem Vorverarbeiter aufgebaut werden. Dazu wird der Inhalt einer Datei nur weitergeleitet, wenn ein bestimmtes Makro nicht definiert ist. Bei der ersten Verarbeitung der Datei wird dieses Makro aber definiert. Dadurch ist dann eine erneute Verarbeitung ausgeschlossen.
Das Beispiel "undefined_.h" zeigt dieses Verfahren. Bei der Ausführung der Direktiven wird der Bezeichner "undefined2_h_INCLUDED" definiert. Dieser Bezeichner wurde direkt dem Dateinamen gebildet, nur der Punkt mußte durch einen Grundstrich ersetzt werden. Deshalb wird hier auch ausnahmsweise von der sonst üblichen Großschreibung abgewichen. Falls dieselbe Datei erneut eingefügt wird, dann werden die Zeilen aber ignoriert, da dann der Bezeichner "undefined2_h_INCLUDED" definiert ist und die erste Zeile somit bewirkt, daß alle Zeilen bis zur letzten Zeile nicht mehr Bestandteil des Vorverarbeiter-Ersatztexts werden.
undefined2.h
#ifndef undefined2_h_INCLUDED
#define undefined2_h_INCLUDED
#ifdef UNDEFINED_
#error
#endif
#endif
Obwohl in dem Quelltext "include2.c" die Datei "undefined2.h" zweimal eingefügt wird, wird ihr Inhalt, wie die Direktive "#ifdef UNDEFINED_" nur noch einmal verarbeitet.
include2.c
#include <stdio.h>
#include "undefined2.h"
#include "undefined2.h"
int main( void )
{ printf( "Hallo!\n" ); }
Wenn nur die beiden letzten Einfügedirektiven und die in ihnen enthaltenen ifndef -Direktiven interpretiert werden, dann ergibt sich also zunächst der in der Abbildung dargestellte Ersatztext für die Quelldatei "include2.c".
Ersatztext fuer die Quelldatei "include2.c" nach teilweiser Ersetzung
#include <stdio.h>
#ifdef UNDEFINED_
#error
#endif
int main( void )
{ printf( "Hallo!\n" ); }
Solch ein Schutz gegen das wiederholte Einfügen soll nicht nur Fehler vermeiden, sondern auch die Übersetzungszeit vermindern. In dieser Hinsicht wird es gelegentlich bemängelt, daß ein Übersetzer trotz des Einfügeschutzes die gesamte Datei bei jeder Einfügung vollständig durchlesen muß (um das "#endif" zu finden). Es gibt auch Compiler, die beim ersten Lesen einer Datei einen Einfügeschutz der hier beschriebenen Art erkennen und dann eine weitere Direktive zum Einfügen derselben Datei ignorieren und so die Bearbeitungszeit verkürzen können.
Für die Ausführung von Programmteilen in Abhängigkeit von bestimmten Bedingungen und auch zur Definition von Namen für Werte sind meistens andere Sprachelemente, die in anderen Lektionen behandelt werden, besser geeignet als Vorübersetzer-Steuerzeilen. Der Gebrauch von Vorübersetzer-Steuerzeilen ist nur in wenigen speziellen Fällen sinnvoll, wie etwa zum vorübergehenden „Auskommentieren“ (Verstecken) von Programmteilen und als Einfügeschutz. Daher sollte ein Anfänger den Einsatz von Vorübersetzer-Steuerzeilen auf diese beiden Anwendungssituationen beschränken.
Vordefinierte Makros
Nach der Verwendung bestimmter Kopfdirektiven können bestimmte Makros definiert worden sein. So wird in einer anderen Lektion beispielsweise der Wert "RAND_MAX" verwendet. Der Bezeichner "RAND_MAX" ist nach der Verwendung der Direktive "#include <stdlib.h>" definiert.
Es gibt auch einige Bezeichner, die sich ähnlich wie Bezeichner für definierte Makros verhalten, aber immer von der Implementation zur Verfügung gestellt werden. Sie werden meistens nur für die Programmanalyse oder zu Dokumentationszwecken verwendet und nur selten zur Implementation bestimmter Anforderungen an ein Programm.
__LINE__
Die Zeile der Quelldatei, in der dieser Bezeichner steht.
__FILE__
Der Name der Quelldatei (aus der Sicht des Übersetzers).
__DATE__
Das Datum der Übersetzung als Text.
__TIME__
Die Zeit der Übersetzung als Text.
__STDC_VERSION__
Der Wert "199901" vom Typ "long" (einem Typ für ganze Zahlen), bei einem Compiler, der der C -Norm "ISO/IEC 9899:1999 (E)" entspricht.
__cplusplus
Es ist garantiert, daß dieser Bezeichner nicht von der Implementation definiert ist.
predefined.c
#include <stdio.h>
int main( void )
{ printf( "__LINE__ = (%ld).\n", ( long )__LINE__ );
printf( "__FILE__ = \"%s\".\n", __FILE__ );
printf( "__DATE__ = \"%s\".\n", __DATE__ );
printf( "__TIME__ = \"%s\".\n", __TIME__ );
printf( "__func__ = \"%s\".\n", __func__ );
printf( "__STDC_VERSION__ = (%ld).\n", __STDC_VERSION__ ); }
#ifdef __cplusplus
#error
#endifstdout
__LINE__ = (4).
__FILE__ = "predefined.c".
__DATE__ = "Dec 18 2000".
__TIME__ = "23:25:32".
__STDC_VERSION__ = (199901).
Funktionen und Vorübersetzerfähigkeiten
Das Programm "kuckuck3.c" enthält die Definition einer Funktion "kehrreim".
kuckuck3.c
#include <stdio.h>
static void kehrreim()
{ printf( "Sim sa la dim, bam ba,\n" );
printf( "Sa la du, sa la dim -\n" ); }
int main( void )
{ { printf( "Auf einem Baum ein Kuckuck, -\n" ); kehrreim();
printf( "Auf einem Baum ein Kuckuck sass.\n" ); }
{ printf( "Da kam ein junger Jaeger, -\n" ); kehrreim();
printf( "Da kam ein junger Jaegersmann.\n" ); }
{ printf( "Der schoss den armen Kuckuck, -\n" ); kehrreim();
printf( "Der schoss den armen Kuckuck tot.\n" ); }}
Es könnte erwogen werden, eine Funktionsdefinition zur Vermeidung der Aufrufkosten durch eine Makrodefinition oder eine Datei zu ersetzen.
Bei der Definition des Makros "KEHRREIM" wird davon Gebrauch gemacht, daß ein Zeilenende in C in den meisten Fällen durch ein vorangestelltes inverses Schrägstrichzeichen sozusagen „aufgehoben“ werden kann. Dadurch gelten die drei Zeilen der Makrodefinition praktisch doch noch als eine Zeile—wie das für eine solche Definition verlangt wird.
kuckuck_macro.c
#include <stdio.h>
#define KEHRREIM \
{ printf( "Sim sa la dim, bam ba,\n" ); \
printf( "Sa la du, sa la dim -\n" ); }
int main( void )
{ { printf( "Auf einem Baum ein Kuckuck, -\n" ); KEHRREIM
printf( "Auf einem Baum ein Kuckuck sass.\n" ); }
{ printf( "Da kam ein junger Jaeger, -\n" ); KEHRREIM
printf( "Da kam ein junger Jaegersmann.\n" ); }
{ printf( "Der schoss den armen Kuckuck, -\n" ); KEHRREIM
printf( "Der schoss den armen Kuckuck tot.\n" ); }}
Zwar werden durch die Definition einer Verbundanweisung als Makro die Aufrufkosten vermieden, doch ist solch eine Definition trotzdem im allgemeinen nicht empfohlen, da sie insgesamt nicht immer zu einer Verbesserung des Laufzeitverhaltens führt, verschiedene Probleme verursachen kann und es in C noch besser angepaßte Sprachelemente für diesen Zweck gibt, die in anderen Lektionen behandelt werden.
Es könnte auch in Erwägung gezogen werden, die Verbundanweisung in ein separate Datei zu schreiben und mit einer Einfügedirektive wiederholt einzulesen.
kehrreim.h
{ printf( "Sim sa la dim, bam ba,\n" );
printf( "Sa la du, sa la dim -\n" ); }kuckuck_include.c
#include <stdio.h>
int main( void )
{ { printf( "Auf einem Baum ein Kuckuck, -\n" );
#include "kehrreim.h"
printf( "Auf einem Baum ein Kuckuck sass.\n" ); }
{ printf( "Da kam ein junger Jaeger, -\n" );
#include "kehrreim.h"
printf( "Da kam ein junger Jaegersmann.\n" ); }
{ printf( "Der schoss den armen Kuckuck, -\n" );
#include "kehrreim.h"
printf( "Der schoss den armen Kuckuck tot.\n" ); }}
Dieses Vorgehen ist jedoch aus ähnlichen Gründen wie im Falle der Makrodefinition nicht zu empfehlen. Hinzu kommt, daß es sich eingebürgert hat, in eingefügte Dateien nur bestimmte Deklarationen und einige andere Quelltexte zu schreiben, jedoch im allgemeinen keine Anweisungen zum mehrfachen Einlesen. Daher wäre solch eine Verwendung einer Datei auch ungewöhnlich und damit weniger verständlich.