C-Präprozessor

aus Wikipedia, der freien Enzyklopädie
Wechseln zu: Navigation, Suche

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 Einfügen 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.

Hintergrund[Bearbeiten]

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.[1][2]

In den Anfangsjahren war der Präprozessor ein eigenständiges Programm, das sein Zwischenergebnis an den eigentlichen Compiler übergab, der es dann übersetzte. Heute werden die Präprozessor-Anweisungen von den Compilern für C++ und C in einem einzigen Arbeitsgang mitberücksichtigt. Auf Wunsch kann von ihnen zusätzlich oder ausschließlich das Resultat ausgegeben werden, das ein Präprozessor geliefert hätte.

Der C-Präprozessor als Textersetzer[Bearbeiten]

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.

Phasen[Bearbeiten]

Der C-Standard definiert unter anderem die nachfolgenden vier (von insgesamt acht) Übersetzungsphasen. Diese vier werden vom C-Präprozessor durchgeführt:

  1. Ersetzung von Trigraph-Zeichen durch das korrespondierende einzelne Zeichen.
  2. Zusammenführung von Zeilen, die durch den umgekehrten Schrägstrich (\) am Zeilenende aufgeteilt wurden.
  3. 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.
  4. 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[Bearbeiten]

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 Funktion printf() 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äprozessor 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 Dateinamenserweiterung .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[Bearbeiten]

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 Makro WIN32 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 Funktion printf beibehalten, ansonsten wird dieser Funktionsaufruf entfernt.

Definition und Ersetzung von Makros[Bearbeiten]

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 Parameter t) 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[Bearbeiten]

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

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

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

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

Standardisierte Makros[Bearbeiten]

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

  • 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 -- sowie Funktionen und Zuweisungen als Argumente 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[Bearbeiten]

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

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

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. */

Literatur[Bearbeiten]

Einzelnachweise[Bearbeiten]

  1. Dennis M. Ritchie: The Development of the C Language. Abgerufen am 12. September 2010 (englisch).
  2. Rationale for International Standard - Programming Languages - C. S. 15 (Abschnitt 5.1.1.2), abgerufen am 12. September 2010 (PDF; 898 kB, englisch).
  3. The C Preprocessor - Concatenation. Abgerufen am 25. Juli 2014 (englisch).