DerC-Präprozessor (cpp, auchCPrecompiler) ist derPräprozessor derProgrammiersprache C. In vielen Implementierungen ist er ein eigenständigesComputerprogramm, das durch denCompiler als erster Schritt der Übersetzung aufgerufen wird. Der Präprozessor bearbeitet Anweisungen zum Einfügen vonQuelltext (#include), zum Ersetzen vonMakros (#define), und für bedingte Ü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 andererDateitypen verwendet werden.
Die Programmiersprache C verfügte in ihren frühesten Versionen über keinen Präprozessor. Er wurde unter anderem auf Betreiben vonAlan Snyder (siehe auch:Portable C Compiler) eingeführt, vor allem aber, um in C das Einfügen anderer Quelltextdateien wie inBCPL (einer Vorgängersprache von C) zu erlauben und das Ersetzen einfacher parameterloser Makros zu ermöglichen. Erweitert vonMike Lesk undJohn 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.
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 vonRessource-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-Dateiwindows.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:
Die häufigste Nutzung des Präprozessors besteht im Einschleusen anderer Dateiinhalte:
#include<stdio.h>intmain(void){printf("Hello, world!\n");return0;}
Der Präprozessor ersetzt die Zeile#include <stdio.h> mit dem Inhalt derHeader-Dateistdio.h, in der unter anderem dieFunktionprintf() deklariert wird. Die Dateistdio.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 imDateisystem 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 nachinclude-Dateien gesucht werden soll.
Eine allgemein übliche Konvention legt fest, dassinclude-Dateien dieDateinamenserweiterung.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.
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 namensWIN32 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-Compiler), durch eine Aufrufoption des C-Präprozessors oder durch eine Anweisung mittels#define bekannt gemacht werden.
Im folgenden Beispiel wird der Aufruf vonprintf nur beibehalten, sofern das MakroVERBOSE an dieser Stelle einen numerischen Wert von 2 oder mehr aufweist:
#if VERBOSE >=2printf("Kontrollausgabe\n");#endif
Falls in diesem BeispielVERBOSE nicht definiert ist, expandiert es am Vergleichsoperator zu0.
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 (guterProgrammierstil). EineEllipse („...“) 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.14159Ein Beispiel für ein Makro mit Parametern ist:
#define CELSIUS_ZU_FAHRENHEIT(t) ((t) * 1.8 + 32)Das MakroCELSIUS_ZU_FAHRENHEIT beschreibt die Umrechnung einer Temperatur (angegeben als Parametert) aus derCelsius- in dieFahrenheit-Skala. Auch ein Makro mit Parametern wird im Quelltext ersetzt:
intfahrenheit,celsius=10;fahrenheit=CELSIUS_ZU_FAHRENHEIT(celsius+5);
wird durch den C-Präprozessor ersetzt zu:
intfahrenheit,celsius=10;fahrenheit=((celsius+5)*1.8+32);
Makros mit einer variablen Anzahl von Parametern bieten sich an, um Argumente an einevariadische Funktion zu übergeben:
#define MELDUNG(...) fprintf(stderr, __VA_ARGS__)Zum Beispiel wird:
inti=6,j=9;MELDUNG("DEBUG: i = %d, j = %d\n",i,j);
durch den C-Präprozessor ersetzt zu:
inti=6,j=9;fprintf(stderr,"DEBUG: i = %d, j = %d\n",i,j);
Da in C aufeinanderfolgendeZeichenkettenliterale während der Übersetzung zusammengefasst werden, ergibt sich hieraus ein gültiger Aufruf der Bibliotheksfunktionfprintf.
In älteren, C90-kompatiblen Quelltexten findent man dafür den folgenden Workaround:
#define MELDUNG(x) printf xMELDUNG(("DEBUG: i = %d, j = %d\n",i,j));
Das heißt, der Makroaufrufmuss mit doppelten runden Klammern erfolgen, um so schließlichein Argument zu übergeben. Das Makro wiederummuss 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.
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.
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 */
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 gibtstring aus, nichthallo:
#include<stdio.h>#define STR(X) #Xintmain(void){charstring[]="hallo";puts(STR(string));return0;}
Der Verkettungsoperator## erlaubt es, zwei Makroparameter zu einem zu verschmelzen(englisch: token pasting). Das folgende Beispielprogramm gibt die Zahl234 aus:
#include<stdio.h>#define GLUE(X, Y) X ## Yintmain(void){printf("%d\n",GLUE(2,34));return0;}
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]
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)intmain(void){MELDUNG("Kapitaler Fehler. Programmende.");returnEXIT_FAILURE;}
Im Fehlerfall wird so vor dem Programmende folgender Text ausgegeben:
Datei [beispiel.c], Zeile 10: Kapitaler Fehler. Programmende.
t im Ersatztext nicht erfolgt, so wäre als Ersetzung das (mathematisch falsche und nicht gewünschte) Ergebnis(celsius + 5 * 1.8 + 32) entstanden.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++ mittelsenum festlegen, Gleitkomma-Konstanten mitconstexpr (C++ ab 2011), bedingte Compilierung mitif (0) { }, und kleine Berechnungen mit Inline-Funktionen, ggf. mit Templates.
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
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 beispielsweiselex oderyacc, um im erzeugten C-Code auf die entsprechende Stelle der Ursprungsdatei zu verweisen. Dadurch wird die Fehlersuche stark vereinfacht.
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 aufStatus- oder Modusregister der Fließkommaeinheit nimmt. */
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.
intmain(){...#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);#elseprintf("Unbekannter C-Compiler\n");#endif...}