- C-Präprozessor
-
Der C-Präprozessor (cpp, auch C Precompiler) ist der Präprozessor der Programmiersprache C. In vielen Implementierungen ist er ein eigenständiges Computerprogramm, das durch den Compiler als erster Schritt der Übersetzung aufgerufen wird. Der Präprozessor bearbeitet Anweisungen zum Einschleusen von Quelltext (#include), zum Ersetzen von Makros (#define), und bedingter Übersetzung (#if). Die Sprache der Präprozessor-Anweisungen ist nicht spezifisch zur Grammatik der Sprache C. Deshalb kann der C-Präprozessor auch zur Bearbeitung anderer Dateitypen verwendet werden.
Inhaltsverzeichnis
Phasen
Der C-Standard definiert unter anderem die nachfolgenden vier (von insgesamt acht) Übersetzungsphasen. Diese vier werden vom C-Präprozessor durchgeführt:
- Ersetzung von Trigraph-Zeichen durch das korrespondierende einzelne Zeichen.
- Zusammenführung von Zeilen, die durch den umgekehrten Schrägstrich (\) am Zeilenende aufgeteilt wurden.
- Aufbereitung in Tokens: Der Präprozessor zerlegt die Eingabe in für die nachfolgenden Compiler-Phasen leichter zu verarbeitende Einheiten und Leerräume und ersetzt Kommentare durch Leerräume.
- Ersetzung von Makros und Einschleusen von Dateiinhalten: Präprozessor-Anweisungen zum Einschleusen von Dateiinhalten (zusätzlich zu übersetzender Quelltext) und für bedingte Übersetzungen werden ausgeführt. Gleichzeitig werden Makros expandiert.
Einschleusen von Dateiinhalten
Die häufigste Nutzung des Präprozessors besteht im Einschleusen anderer Dateiinhalte:
#include <stdio.h> int main( void ) { printf( "Hello, world!\n" ); return 0; }
Der Präprozessor ersetzt die Zeile
#include <stdio.h>
mit dem Inhalt der Header-Datei 'stdio.h', in der unter anderem die Funktionprintf()
deklariert wird. Die Datei 'stdio.h' ist Bestandteil jeder C-Entwicklungsumgebung.Die
#include
-Anweisung kann auch mit doppelten Anführungszeichen (#include "stdio.h"
) verwendet werden. Dann wird bei der Suche nach der betroffenen Datei zusätzlich zu den Verzeichnissen des C-Compilers auch das aktuelle Verzeichnis im Dateisystem durchsucht. Durch Optionen für den C-Compiler, der diese wiederum an den C-Präprozessor weiterreicht oder durch Aufrufoptionen für den C-Prärpozessor kann festgelegt werden, in welchen Verzeichnissen nach include-Dateien gesucht werden soll.Eine allgemein übliche Konvention legt fest, dass include-Dateien die Dateinamenserweiterung .h erhalten. Originäre C-Quelldateien erhalten die Dateinamenswerweiterung .c. Das ist jedoch nicht zwingend vorgeschrieben. Auch Inhalte aus Dateien mit anderer Dateinamenserweiterung als .h können auf diese Art eingeschleust werden.
Innerhalb einzuschleusender Dateien wird häufig durch bedingte Ersetzung dafür gesorgt, dass Deklarationen für die nachfolgenden Compiler-Phasen nicht mehrfach wirksam werden, sofern der Dateiinhalt mehrfach durch
#include
eingeschleust wird.Bedingte Ersetzung
Die Anweisungen
#if
,#ifdef
,#ifndef
,#else
,#elif
und#endif
werden für bedingte Ersetzungen des C-Präprozessors verwendet:#ifdef WIN32 # include <windows.h> #else # include <unistd.h> #endif
Der C-Präprozessor prüft, ob ihm ein Makro namens
WIN32
bekannt ist. Ist das der Fall, wird der Dateiinhalt von<windows.h>
eingeschleust, ansonsten der von<unistd.h>
. Das MakroWIN32
kann implizit durch den Übersetzer (z. B. durch alle Windows-32-Bit-Übersetzer), durch eine Aufrufoption des C-Präprozessors oder durch eine Anweisung mittels#define
bekannt gemacht werden.#if VERBOSE >=2 printf( "Kontrollausgabe\n" ); #endif
Sofern das Makro
VERBOSE
während seiner Ersetzung durch den C-Präprozessor den Wert 2 oder größer aufweist, wird der Aufruf der Funktionprintf
beibehalten, ansonsten wird dieser Funktionsaufruf entfernt.Definition und Ersetzung von Makros
In C sind Makros ohne Parameter, mit Parametern und (seit C99) auch mit einer variablen Zahl an Parametern zulässig:
#define <MAKRO_NAME_OHNE_PARAMETER> <Ersatztext> #define <MAKRO_NAME_MIT_PARAMETER>( <Parameterliste> ) <Ersatztext> #define <MAKRO_NAME_MIT_VARIABLEN_PARAMETERN>( <optionale feste Parameterliste,> ... ) <Ersatztext>
Bei Makros mit Parametern ist zwischen dem Makronamen und der öffnenden runden Klammer kein Leerraum zugelassen. Ansonsten wird das Makro inklusive der Parameterliste als reiner Textersatz für den Makronamen verwendet. Zur Unterscheidung von Funktionen bestehen die Namen von Makros üblicherweise ausschließlich aus Großbuchstaben (guter Programmierstil). Eine Ellipse („
...
“) zeigt an, dass das Makro an dieser Stelle ein oder mehrere Argumente akzeptiert. Auf diese kann im Ersatztext des Makros mit dem speziellen Bezeichner__VA_ARGS__
Bezug genommen werden.Makros ohne Parameter werden beim Auftreten des Makronamens im Quelltext durch ihren Ersatztext (der auch leer sein kann) ersetzt. Bei Makros mit Parametern geschieht das nur, wenn nach dem Makronamen eine Parameterliste folgt, die in runde Klammern eingeschlossen ist und in der Parameteranzahl der Deklaration des Makros entspricht. Beim Ersetzen von Makros mit variabler Parameterzahl werden die variablen Argumente inklusive der sie trennenden Kommata zu einem einzigen Argument zusammengefasst und im Ersatztext statt
__VA_ARGS__
eingefügt.Makros ohne Parameter werden häufig für symbolische Namen von Konstanten verwendet:
#define PI 3.14159
Ein Beispiel für ein Makro mit Parametern ist:
#define CELSIUS_ZU_FAHRENHEIT( t ) ( ( t ) * 1.8 + 32 )
Das Makro
CELSIUS_ZU_FAHRENHEIT
beschreibt die Umrechnung einer Temperatur (angegeben als Parametert
) aus der Celsius- in die Fahrenheit-Skala. Auch ein Makro mit Parametern wird im Quelltext ersetzt:int fahrenheit, celsius = 10; fahrenheit = CELSIUS_ZU_FAHRENHEIT( celsius + 5 );
wird durch den C-Präprozessor ersetzt zu:
int fahrenheit, celsius = 10; fahrenheit = ( ( celsius + 5 ) * 1.8 + 32 );
Makros mit einer variablen Anzahl von Parametern bieten sich an, um Argumente an eine variadische Funktion zu übergeben:
#define MELDUNG( ... ) fprintf( stderr, "DEBUG: " __VA_ARGS__ )
Zum Beispiel wird:
int i = 6, j = 9; MELDUNG( "i = %d, j = %d\n", i, j );
durch den C-Präprozessor ersetzt zu:
int i = 6, j = 9; fprintf( stderr, "DEBUG: " "i = %d, j = %d\n", i, j );
Da in C aufeinanderfolgende Zeichenkettenliterale während der Übersetzung zusammengefasst werden, ergibt sich hieraus ein gültiger Aufruf der Bibliotheksfunktion
fprintf
.Makro über mehrere Zeilen
Da in der zweiten Phase des C-Präprozessors durch das Zeichen \ am Zeilenende schon die Zusammenführung auf eine Zeile erfolgt, können Makros durch diesen Mechanismus auf mehreren Zeilen deklariert werden.
Makrodefinition zurücknehmen
Eine vorherige Makrodefinition kann mit
#undef
wieder rückgängig gemacht werden. Das dient dazu, Makros nur in einem begrenzten Codeabschnitt verfügbar zu machen:#undef CELSIUS_ZU_FAHRENHEIT /* Der Geltungsbereich des Makros endet hier */
Umwandlung eines Makroparameters in eine Zeichenkette
Wird einem Parameter im Ersatztext eines Makros ein
#
vorangestellt, so wird bei der Ersetzung das Argument durch Einschließen in doppelte Hochkommata in eine Zeichenkette umgewandelt (stringized). Folgendes Programm gibt string aus, nicht hallo:#include <stdio.h> #define STR(X) #X int main( void ) { char string[] = "hallo"; puts( STR( string ) ); return 0; }
Verkettung von Makroparametern
Der Verkettungsoperator
##
erlaubt es, zwei Makroparameter zu einem zu verschmelzen (englisch: token pasting). Das folgende Beispielprogramm gibt die Zahl 234 aus:#include <stdio.h> #define GLUE(X,Y) X ## Y int main( void ) { printf( "%d\n", GLUE(2, 34) ); return 0; }
Die Operatoren
#
und##
ermöglichen bei geschickter Kombination das halbautomatische Erstellen beziehungsweise Umstellen ganzer Programmteile durch den Präprozessor während der Übersetzung des Programms, was allerdings auch zu schwer durchschaubarem Code führen kann.[1]Standardisierte Makros
Zwei vordefinierte Makros sind
__FILE__
(aktueller Dateiname) und__LINE__
(aktuelle Zeile innerhalb der Datei):#include <stdlib.h> #define MELDUNG(text) fprintf( stderr, \ "Datei [%s], Zeile %d: %s\n" \ __FILE__, __LINE__, text ) if ( fehler ) { MELDUNG( "Kapitaler Fehler. Programmende." ); exit( EXIT_FAILURE ); }
Im Fehlerfall wird so vor dem Programmende folgender Text ausgegeben:
Datei [beispiel.c], Zeile 9: Kapitaler Fehler. Programmende.
Gefahren von Makros
- Wichtig ist, dass bei der Deklaration von Makros mit Berechnungen ausreichend viele Klammern gesetzt werden, damit beim Aufruf des Makros immer das gewünschte Ergebnis erreicht wird. Wäre im Beispiel der Temperaturumrechnung die Klammerung um den Parameter
t
im Ersatztext nicht erfolgt, so wäre als Ersetzung das (mathematisch falsche und nicht gewünschte) Ergebnis( celsius + 5 * 1.8 + 32 )
entstanden. - Bei Makroaufrufen sind Argumente mit den Operatoren ++ und -- zu vermeiden, da diese durch eventuelle Mehrfachauswertung zu unerwünschten Seiteneffekten oder sogar undefiniertem Code führen können.
- Die Verwendung von Semikolon im Ersatztext als Ende einer C-Anweisung oder als Trenner zwischen mehreren im Makroersatz angegebenen C-Anweisungen sollte vermieden werden, da dies Nebeneffekte auf den weiter zu übersetzenden Quelltext bewirken kann.
Gezielter Abbruch der Übersetzung
Mit der Anweisung
#error
kann der Übersetzungsvorgang abgebrochen und eine Meldung ausgegeben werden:#include <limits.h> #if CHAR_BIT != 8 #error "Dieses Programm unterstützt nur Plattformen mit 8bit-Bytes!" #endif
Ändern des Dateinamens und der Zeilennummern
Mittels der Anweisung
#line
ist es möglich, aus Sicht des Compilers die Nummer der darauf folgenden Zeile und auch den für Meldungen verwendeten Namen der aktuellen Quelldatei zu manipulieren. Dies hat Auswirkungen auf etwaige nachfolgende Compilermeldungen:#line 42 /* Diese Zeile hätte in einer Compilermeldung jetzt die Nummer 42. */ #line 58 "scan.l" /* In einer Meldung wäre dies Zeile 58 der Datei ''scan.l'' */
Genutzt wird dieser Mechanismus oft von Codegeneratoren wie beispielsweise lex oder yacc, um im erzeugten C-Code auf die entsprechende Stelle der Ursprungsdatei zu verweisen. Dadurch wird die Fehlersuche stark vereinfacht.
Beeinflussung des Compilers
Die Präprozessoranweisung
#pragma
erlaubt es, den Compiler zu beeinflussen. Derartige Kommandos sind meist compilerspezifisch, einige definiert aber auch der C-Standard (ab C99), z. B.:#include <fenv.h> #pragma STDC FENV_ACCESS ON /* Im folgenden muss der Compiler davon ausgehen, dass das Programm Zugriff auf Status- oder Modusregister der Fließkommaeinheit nimmt. */
Der C-Präprozessor als Textersetzer
Da sich der C-Präprozessor nicht auf die Beschreibung der Sprache C stützt, sondern ausschließlich seine ihm bekannten Anweisungen erkennt und bearbeitet, kann er auch als reiner Textersetzer für andere Zwecke verwendet werden.
Hintergrund
Die Programmiersprache C verfügte in ihren frühesten Versionen über keinen Präprozessor. Er wurde unter anderem auf Betreiben von Alan Snyder (siehe auch: Portable C Compiler) eingeführt, vor allem aber, um in C das Einfügen anderer Quelltextdateien wie in BCPL (einer Vorgängersprache von C) zu erlauben und das Ersetzen einfacher parameterloser Makros zu ermöglichen. Erweitert von Mike Lesk und John Reiser um parameterbehaftete Makros und Konstrukte zur bedingten Übersetzung entwickelte er sich im Laufe der Zeit vom optionalen Zusatzprogramm eines Compilers zu einer standardisierten Komponente der Programmiersprache. Die von der Kernsprache unabhängige Entwicklung erklärt die Diskrepanzen in der Sprachsyntax zwischen C und seinem Präprozessor.[2][3]
Einzelnachweise
- ↑ The C Preprocessor - Concatenation (auf http://theory.uwinnipeg.ca). Abgerufen am 11. September 2010 (englisch).
- ↑ Dennis M. Ritchie: The Development of the C Language. Abgerufen am 12. September 2010 (englisch).
- ↑ Rationale for International Standard - Programming Languages - C. S. 15 (Abschnitt 5.1.1.2), abgerufen am 12. September 2010 (PDF, englisch).
Literatur
- British Standards Institute (Hrsg.): The C Standard - Incorporating TC1 - BS ISO/IEC 9899:1999. John Wiley & Sons, 2003. ISBN 0-470-84573-2. Kapitel 6.10.1 bis 6.10.9
- C99-Standard ISO/IEC 9899:TC3 Draft auf www.open-std.org (PDF, englisch)
Wikimedia Foundation.