Weitere wichtige Präprozessor Direktiven
Neben den grundlegenden Präprozessor-Direktiven wie bedingter Kompilierung und Makrodefinitionen existieren weitere Anweisungen, die spezielle Aufgaben im Kompilierprozess übernehmen. Sie ermöglichen eine gezielte Steuerung des Builds, die Anpassung von Diagnosemeldungen oder die Integration compiler-spezifischer Funktionen. Im Folgenden werden wichtige Beispiele vorgestellt.
Fehlermeldungen und Warnungen (#error und #warning)
Normalerweise beschränkt sich der C-Präprozessor darauf, einfache Textersetzungen durchzuführen oder zusätzliche Dateien einzubinden. Er arbeitet also rein vorbereitend, bevor der eigentliche Compiler startet. In manchen Situationen reicht das jedoch nicht aus: Man möchte verhindern, dass bestimmte fehlerhafte oder nicht unterstützte Konfigurationen überhaupt bis zum Compiler gelangen. Genau hier kommen spezielle Präprozessor-Direktiven ins Spiel, mit denen sich gezielt Fehler oder Warnungen auslösen lassen. So kann man den Übersetzungsvorgang entweder sofort abbrechen oder zumindest einen deutlichen Hinweis ausgeben.
Für diesen Zweck existieren zwei verschiedene Mechanismen:
-
#errorist standardisiert und sorgt dafür, dass eine Fehlermeldung erzeugt wird, woraufhin die Kompilierung sofort beendet wird. Ein typisches Einsatzfeld: Eine Plattform ist nicht vorgesehen oder eine Makrokombination widerspricht sich. (Quelle: ISO/IEC 9899:2024, § 6.10.5 Error directive) -
#warningist nicht standardisiert, wird aber von vielen Compilern (z. B. GCC, Clang) unterstützt. Damit kann man Hinweise ausgeben, ohne den Build zu stoppen. Typischer Einsatz: auf veraltete Funktionen hinweisen oder Migrationsempfehlungen geben.
Ein Beispiel verdeutlicht den Unterschied zwischen beiden Direktiven:
#include <stdio.h>
// #define UNSUPPORTED_PLATFORM
#define DEPRECATED_FEATURE
int main() {
#ifdef UNSUPPORTED_PLATFORM
#error Diese Plattform wird nicht unterstützt
#endif
#ifdef DEPRECATED_FEATURE
#warning Diese Funktion ist veraltet, bitte aktualisieren
#endif
puts("Programm läuft.");
}
Ist das Makro UNSUPPORTED_PLATFORM definiert, bricht die Kompilierung sofort mit einer klaren Fehlermeldung ab. Wird hingegen nur DEPRECATED_FEATURE gesetzt, erscheint eine Warnung, die das Programmieren nicht blockiert.
#error sollte nur in bedingten Blöcken genutzt werden, da ansonsten jede Kompilierung fehlschlägt. Die Meldungen sollten dabei so formuliert sein, dass sie konkrete Hilfestellung bieten. #warning wiederum ist nicht überall verfügbar und wird von manchen Compilern schlicht ignoriert, weshalb sein Einsatz überlegt erfolgen sollte.
Zeilen- und Dateiinformationen mit #line
Die Direktive #line wird in erster Linie in automatisch generiertem Code verwendet.
(Quelle: ISO/IEC 9899:2024, § 6.10.4 Line control)
Sie legt die Zeilennummer und optional den Dateinamen fest, die der Compiler in Fehler- und Warnmeldungen angibt. Im Abschnitt \ab{hash_line} wurde diese Direktive bereits erwähnt, als der vom Präprozessor erzeugte Code betrachtet wurde.
#line wird nur selten manuell in Quelltext eingefügt. Typischerweise erzeugen Werkzeuge die Direktive automatisch, zum Beispiel:
- der C-Präprozessor selbst
- Parser-Generatoren (yacc, bison, ANTLR)
- Template-Engines
- weitere Präprozessoren für domänenspezifische Sprachen
- Build-Tools, die C-Code aus anderen Formaten generieren
Angenommen, es existiert eine Template-Datei:
Willkommen im Programm!
Dies ist Zeile 2 des Templates.
Hier steht ein Fehler: ${1/0}
Ein Generator könnte daraus automatisch C-Code erzeugen, der jede Template-Zeile in puts()- oder printf()-Aufrufe umwandelt. Damit der Bezug zu den ursprünglichen Template-Zeilen erhalten bleibt, fügt der Generator #line-Direktiven ein:
.generated_output.c (automatisch generiert)
/*1*/ #include <stdio.h>
/*2*/
/*3*/ int main() {
/*4*/ #line 1 "template.txt"
/*5*/ puts("Willkommen im Programm!");
/*6*/ #line 2 "template.txt"
/*7*/ puts("Dies ist Zeile 2 des Templates.");
/*8*/ #line 3 "template.txt"
/*9*/ printf("Hier steht ein Fehler: %d\n", 1/0);
/*0*/}
Die Angabe in einer #line-Direktive wirkt ab der folgenden Quelltextzeile und bleibt gültig, bis eine neue Direktive gesetzt wird. Meldet der Compiler hier also einen Fehler in Zeile 9, verweist er auf Zeile 3 in template.txt:
template.txt: In function 'main':
template.txt:3:44: warning: division by zero [-Wdiv-by-zero]
Die Ausgabe zeigt, dass nicht die generierte Datei, sondern template.txt als Quelle des Problems genannt wird. Genau dafür existieren die #line-Direktiven.
Beim C-Präprozessor ist #line zentral: Er setzt beim Einfügen und Erweitern von Quelltext ständig neue Zeilenangaben. Ohne diese Informationen könnten Compiler- und Debugger-Meldungen nur noch auf den vom Präprozessor erzeugten Zwischentext verweisen. Da dieser durch zahlreiche Einfügeoperationen oft stark erweitert und unübersichtlich wird, wäre eine Fehlersuche dort kaum möglich. Erst durch die eingefügten #line-Direktiven lassen sich Diagnosen korrekt auf die Ursprungsdateien zurückführen.
Der Compiler nutzt #line für die Ausgabe von Fehlermeldungen, der Debugger für die Anzeige von Quelltext während des Debuggens. Die Direktive beeinflusst ausschließlich die Diagnostik und hat keine Auswirkungen auf den erzeugten Maschinencode.
Compiler-spezifische Anpassungen (#pragma, _Pragma)
Die Direktive #pragma ist das zentrale Werkzeug für Compiler-spezifische Erweiterungen. Sie erlaubt es, Anweisungen an den Compiler weiterzugeben, die von nicht unterstützenden Compilern standardkonform ignoriert werden.
(Quelle: ISO/IEC 9899:2024, § 6.10.6 Pragma directive)
#pragma message("Präprozessor beginnt")
#include <stdio.h>
int main() {
#pragma GCC diagnostic ignored "-Wunused-variable"
int unused_var = 42; // Keine Warnung trotz ungenutzter Variable
puts("Programm läuft.");
}
#pragma message("Präprozessor ist fertig")
Das Beispiel zeigt, wie eine Warnung in GCC unterdrückt werden kann. Andere Compiler ignorieren die Anweisung. #pragma ist bewusst so gestaltet, dass Erweiterungen möglich sind, ohne den Standard zu verletzen.
Die Zeilen mit #pragma message werden bereits vom Präprozessor verarbeitet. Sie geben während der Kompilierung Meldungen im Stil gewöhnlicher Compiler-Hinweise aus, etwa:
<source>:1:9: note: '#pragma message: Präprozessor beginnt'
1 | #pragma message("Präprozessor beginnt")
| ^~~~~~~
<source>:11:9: note: '#pragma message: Präprozessor ist fertig'
11 | #pragma message("Präprozessor ist fertig")
| ^~~~~~~
Nicht alle Pragmas sind portabel, und manche können auf anderen Plattformen sogar Fehler verursachen. Eine bewährte Vorgehensweise ist daher, sie in Kombination mit Compiler-Makros zu verwenden, um sicherzustellen, dass sie nur bei unterstützten Compilern aktiv sind.
Hinweis: Der Name pragma kommt aus dem Griechischen und bedeutet ›Handlung‹ oder ›Tatsache‹. Der Begriff wurde bereits in der Programmiersprache ALGOL 68 im Jahr 1968 verwendet, wo Compiler-Direktiven als ›pragmats‹ (von ›pragmatic‹ abgeleitet) bezeichnet wurden. In die Informatik gelangte der Begriff über das Englische als Kurzform von pragmatic information, also ›pragmatische Anweisung‹. Gemeint ist damit, dass es sich nicht um einen Teil der eigentlichen Programmlogik handelt, sondern um eine zweckorientierte Zusatzinformation für den Compiler.
Der _Pragma-Operator
_Pragma (seit C99) dient demselben Zweck wie #pragma, nämlich dem Erteilen von Anweisungen an den Compiler. Der Unterschied besteht darin, dass #pragma nur als Präprozessor-Direktive unmittelbar im Quelltext erscheinen kann, während _Pragma in Ausdrücken verwendet werden darf. Dadurch lässt sich #pragma-Funktionalität in Makros einbauen.
Die allgemeine Syntax ist:
_Pragma ( string-literal )
Das Argument ist stets ein Stringliteral. Der Compiler behandelt den Inhalt so, als stünde dort eine #pragma-Direktive.
Ein Beispiel:
#define IGNORE_UNUSED_RESULT(expr) \
do { \
_Pragma("GCC diagnostic push") \
_Pragma("GCC diagnostic ignored \"-Wunused-result\"") \
expr; \
_Pragma("GCC diagnostic pop") \
} while(0)
[[nodiscard]] int compute() { return 42; }
int main() {
// Kein Warning für ignorierten Rückgabewert
IGNORE_UNUSED_RESULT(compute());
}
Das Makro IGNORE_UNUSED_RESULT ermöglicht es, Compiler-Warnungen gezielt für einen einzelnen Funktionsaufruf zu unterdrücken:
_Pragma("GCC diagnostic push")sichert den aktuellen Zustand der Compiler-Warnungen_Pragma("GCC diagnostic ignored \"-Wunused-result\"")deaktiviert die Warnung für ignorierte Rückgabewerteexpr;führt den übergebenen Ausdruck aus (hier den Aufruf voncompute())_Pragma("GCC diagnostic pop")stellt den ursprünglichen Warnungszustand wieder her
Ohne _Pragma wäre es nicht möglich, diese Compiler-Direktiven innerhalb eines Makros zu platzieren, da #pragma nur auf oberster Ebene funktioniert.
Tipp:
#pragmaund_Pragmalassen sich gezielt einsetzen, um zu steuern, welche Warnungen ein Compiler ausgibt. Viele Compiler besitzen eigene Erweiterungen für Diagnosekontrolle. Ein Beispiel:#ifdef __GNUC__ #pragma GCC diagnostic ignored "-Wunused-parameter" #endifDas nutzt dieselbe Warnoption, die man sonst auf der Kommandozeile über
-Wno-unused-parametersetzen würde, nur eben direkt im Quelltext. Die Diagnose-Pragmas wirken ab der Stelle, an der sie stehen, und zwar bis eine Gegenanweisung kommt oder bis die Übersetzungseinheit endet.Damit wird die Warnung nur unter GCC abgeschaltet. Andere Compiler ignorieren die Zeile oder kennen das Pragma nicht. Solche Anweisungen sind optional und werden nur ausgewertet, wenn der Compiler sie unterstützt.
_Pragmabietet dieselbe Möglichkeit innerhalb von Makros, etwa:#define DISABLE_UNUSED \ _Pragma("GCC diagnostic ignored \"-Wunused-variable\"")Damit kann man Warnungen situationsabhängig steuern, ohne den Quelltext zu verzweigen.
Leere Direktiven
Schließlich gibt es noch die leere Direktive #.
(Quelle: ISO/IEC 9899:2024, § 6.10 Preprocessing directives)
#include <stdio.h>
#define FEATURE_A
#
int main() { }
Die leere Direktive hat keine semantische Wirkung und wird vom Präprozessor ignoriert. Sie wird vereinzelt bei automatisch generiertem Code eingesetzt. Es gibt aber für uns keinen wirklichen Grund, leere Direktiven im Programmcode zu schreiben.
Hinweis: Die leere Direktive muss alleine in der Zeile stehen. Wird versehentlich Inhalt hinter das # gesetzt, führt dies zu Syntaxfehlern.