C-Präprozessor

Programmiersprache C

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 dem 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.

Beispielsweise wird für die Kompilierung von Ressource-Dateien auch der C-Präprozessor verwendet. Dieser erlaubt es C-Headerdateien einzubetten und ermöglicht somit Werte, zum Beispiel mit #define definierte Zahlwerte oder Zeichenketten, zwischen C-Code und Ressource-Code zu teilen. Wichtig ist dabei, dass der Ressource-Compiler keinen komplexen C-Code verarbeiten kann. Der C-Code, wie Funktions-Deklarationen oder Struktur-Definitionen, kann dabei mit einem #if oder #ifdef bedingt für den Ressource-Compiler ausgeblendet werden, wobei bestimmte Makros vom Ressource-Compiler definiert werden (RC_INVOKED), die beim C-Compiler nicht definiert sind. Dies nutzt auch die Header-Datei windows.h aus, die somit sowohl in Programm-Code wie auch in Ressource-Code (teilweise) genutzt werden kann[3].

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 (gedacht etwa für lange Zeichenketten, bei Lochkarten oder Magnetbändern mit fester Record-Länge).
  3. 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,z. B.

#ifdef WIN32
    #include <windows.h>
#else
    #include <unistd.h>
#endif

In diesem Beispiel prüft der C-Präprozessor, 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-Compiler), durch eine Aufrufoption des C-Präprozessors oder durch eine Anweisung mittels #define bekannt gemacht werden.

Im folgenden Beispiel wird der Aufruf von printf nur beibehalten, sofern das Makro VERBOSE an dieser Stelle einen numerischen Wert von 2 oder mehr aufweist:

#if VERBOSE >=2
    printf("Kontrollausgabe\n");
#endif

Falls in diesem Beispiel VERBOSE nicht definiert ist, expandiert es am Vergleichsoperator zu 0.

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__ (VA steht für „variable argument list“) 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, __VA_ARGS__)

Zum Beispiel wird:

int i = 6, j = 9;
MELDUNG("DEBUG: 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.

In älteren, C90-kompatiblen Quelltexten findent man dafür den folgenden Workaround:

#define MELDUNG(x) printf x
MELDUNG(("DEBUG: i = %d, j = %d\n", i, j));

Das heißt, der Makroaufruf muss mit doppelten runden Klammern erfolgen, um so schließlich ein Argument zu übergeben. Das Makro wiederum muss eine Funktion aufrufen, die genau die übergebene Parameterliste erwartet. Notfalls muss man dafür eine passende (Ausgabe-)Funktion schreiben, da der Präprozessor keine Stringbearbeitung kennt, um die Parameterliste zu ändern oder zu erweitern.

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

Standardisierte Makros

Bearbeiten

Zwei vordefinierte Makros sind __FILE__ (aktueller Dateiname) und __LINE__ (aktuelle Zeile innerhalb der Datei):

#include <stdio.h>
#include <stdlib.h>

#define MELDUNG(text) fprintf(stderr, \
    "Datei [%s], Zeile %d: %s\n", \
    __FILE__, __LINE__, text)

int main(void)
{
    MELDUNG("Kapitaler Fehler. Programmende.");
    return EXIT_FAILURE;
}

Im Fehlerfall wird so vor dem Programmende folgender Text ausgegeben:

Datei [beispiel.c], Zeile 10: Kapitaler Fehler. Programmende.

Gefahren von Makros

Bearbeiten
  • Wichtig ist, dass bei der Deklaration von Makros mit Berechnungen jeder Parameter geklammert werden muss, 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.
  • Makros sind global und lassen sich nicht in Namensräume (C++-Konstrukt) einsperren. Einmal vergebene Bezeichner dürfen weder als lokale Variablen noch als Strukturmember auftauchen, da hier der C-typische Verdeckungseffekt nicht greift. Daher müssen diese für das gesamte Projekt eineindeutig sein, was bei der Namensvergabe zu beachten ist.

Weniger fehleranfällige Programmierung, insbesondere in C++, vermeidet Makros wo immer das möglich ist. Beispielsweise lassen sich symbolische Integer-Konstanten in C und C++ mittels enum festlegen, Gleitkomma-Konstanten mit constexpr (C++ ab 2011), bedingte Compilierung mit if (0) { }, und kleine Berechnungen mit Inline-Funktionen, ggf. mit Templates.

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

Erkennung des verwendeten Compilers und dessen Version

Bearbeiten

Die Programmiersprache C bietet selbst keine Möglichkeit den für ein Programm verwendeten Compiler und dessen Version zur Laufzeit des Programms auszugeben. Hierfür haben die meisten C Compilerhersteller Makros definiert[5], die sich mit dem C-Präprozessor auswerten lassen.

int main()
{
  ...
  #if defined ( __clang__ )
    printf("Clang %i.%i.%i\n", __clang_major__, __clang_minor__, __clang_patchlevel__);
  #elif defined( __GNUC__ )
    printf("GCC %i.%i.%i\n", __GNUC__, __GNUC_MINOR__, __GNUC_PATCHLEVEL__);
  #elif defined ( _MSC_VER )
    printf("Microsoft Visual C++ %i\n", _MSC_VER);
  #else
    printf("Unbekannter C-Compiler\n");
  #endif
  ...
}

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. (PDF; 898 kB) S. 15 (Abschnitt 5.1.1.2), abgerufen am 12. September 2010 (englisch).
  3. msdn.microsoft.com
  4. The C Preprocessor – Concatenation. Abgerufen am 25. Juli 2014 (englisch).
  5. https://sourceforge.net/p/predef/wiki/Compilers/ Pre-defined Compiler Macros Wiki