In Assemblersprachen nennt man die Präprozessoren häufig auch Makroprozessoren.
Der Präprozessor wird im Normalfall automatisch als erste Stufe der Übersetzung aufgerufen. Die Ausgabe wird dann direkt an den Compiler übergeben.
Die Arbeit des Präprozessors kann man auch in einer Datei ablegen lassen und sich das Ergebnis mit einem Editor ansehen. Dies kann besonders dann nützlich sein, wenn man mit dem Präprozessor eigene Makros geschrieben hat und nachprüfen will, ob sie auch wie gedacht expandiert werden.
Die Präprozessoranweisung zur Definition von Makros heißt: "#define". Die einfachste Definition eines Makros besteht nur aus einem Makronamen und einer textlichen Ersetzung. Man sagt auch, daß in diesem Fall eine symbolische Konstante definiert wird.
Die häufigsten Anwendungsgebiete solcher symbolischer Konstanten sind die Definition eines Makros für jede Informationsdatei, die Angabe von Feldgrößen oder für mehrfach benutzte numerische Spezialwerte wie PI.
Bild 11-1: Arbeiten mit symbolischen Konstanten
Im Bild 1 wird eine symbolische Konstante GROESSE mit dem Ersetzungswert 10 definiert. Es ist üblich, solche Konstanten groß zu schreiben, um sie von Variablen unterscheiden zu können. Dies ist zwar nicht für den Präprozessor erforderlich, hilft aber dem Leser eines Programmes.
Beim Übersetzen liest der Präprozessor die Zeile fünf mit der Makrodefinition und merkt sich den definierten Namen. Alle Anweisungszeilen für den Präprozessor werden nicht an den Compiler weitergereicht. Bei allen Verwendungen der symbolischen Konstanten erfolgt die Expansion des Makros. Der Makroname wird entfernt und durch den Ersetzungstext ersetzt.
Im Beispiel wird bei jeder Benutzung von GROESSE dieses Wort entfernt und statt dessen "10" zum Compiler gesendet.
Solange eine Definition identisch ist, darf sie in C (aber nicht in C++) wiederholt werden. Trotzdem kann man nur raten, diese Praxis durch bedingte Übersetzungen, die ebenfalls in diesem Kapitel besprochen werden, auszuschließen.
Wird ein existierendes Makro mit einem unterschiedlichen Ersetzungswert erneut definiert, gibt der Compiler eine Warnung aus.
Bild 11-2: Problematische Zweitdefinition
Sehen Sie sich vielleicht einmal die stdio.h an. Ein Beispiel finden Sie bei der bedingten Übersetzung.
Bild 11-3: Makrodefinition mit Parametern
Die Parameter haben eigene Namen in der Schnittstelle, die im Makrokörper verwendet werden. Das Beispiel im Bild zwei definiert ein Makro mit Namen max. Beachten Sie, daß die Klammer ohne Leerzeichen an den Namen anschließt. Die beiden definierten Makroparameter sind a und b.
Im Makrokörper steht eine Formel zur Berechnung des Maximums mit Hilfe eines bedingten Ausdrucks. Bei der Übersetzung wird der Compiler den bedingten Ausdruck auswerten und als Ergebnis bei Wahrheit den Operanden vor dem Doppelpunkt liefern, im Fehlerfalle den Operanden dahinter.
Beim Aufruf eines Makros wird der Präprozessor als Parameter a den ersten Text, als b den zweiten Text in seinen Köper einsetzen. Ob dies das gewünschte Ergebnis liefert, wird erst die nachfolgende Übersetzung zeigen.
Erst jetzt enthält der Text keine Makros mehr und kann an den Compiler übergeben werden.
Die wiederholte Überprüfung und Expandierung kann zu schwer verständlichen Programmen führen. Der Programmierer ist daher gut beraten, wenn er Makroverschachtelungen nur sparsam verwendet.
Bild 11-4: Fehlerhafte Expansion wegen fehlender Klammern
Im Bild drei wurde ein Makro definiert, um das Quadrat einer beliebigen Zahl zu bilden. Im Normalfall wird das Makro sicher gut funktionieren. Im verwendeten Aufruf wurde jedoch ein Ausdruck übergeben. Das Ergebnis der Quadrierung ist hier "23". Ob das mit der Mathematik übereinstimmt? Die Klammern um jeden Parameter hätten dies verhindert.
Der Operator # teilt dem Präprozessor mit, daß er den Makroparameter, vor dem der Operator steht, in Form eines C-Textes einsetzen soll. Im Beispiel (Bild 5) ersetzt man zuerst #wert mit variable. Nun stehen zwei Texte hintereinander. Nach ANSI-Konvention werden zwei Texte, die nur durch Leerzeichen (white space) getrennt, nebeneinander stehen, zu einem verbunden.
Der Aufruf der printf()-Funktion ergibt sich daher folgendermassen.
printf ("variable = %d\n",variable);
Der #-Operator hat beim Aufruf aus dem Variablennamen einen C-Text gemacht. Im Englischen heißt dieser Operator stringizing operator.
Der zweite Operator ## dient zum Zusammenhängen von Texten. Sein englischer Name ist token pasting operator.
Bild 11-5: Ersetzung als Text im Präprozessor
Im Beispiel (Bild 6) werden beim Makro ShowText() zwei Texte als Parameter erwartet und mit Hilfe des ##-Operators zu einem verknüpft. Im ersten Formatparameter der printf()-Funktion taucht deshalb nur ein Formatsymbol "%s" auf.
Bild 11-6: Zusammenhängen von Texten
Im Original-Pascal war insgesamt nur eine Quelldatei erlaubt, da kein Linker zur Verfügung stand und die Sprache keine externen Deklarationen kannte. Um hier trotzdem eine gewisse Strukturierung und Zusammenarbeit verschiedener Programmierer zu ermöglichen, wurden Quelldateien mit Unterprogrammen während der Übersetzung des Hauptprogrammes mit eingelesen und übersetzt.
Dies bedeutete jedoch, daß dabei immer wieder der gesamte Quellcode bei jeder Änderung im Gesamtprogramm neu übersetzt werden mußte. Dies ist sicherlich ein uneffizienter Denkansatz.
In C enthalten die zusätzlich eingelesenen Dateien im Normalfall weder Quellcode, der zu Maschinencode wird, noch Variablendefinitionen. In den Informationsdateien (header files) sind ausschließlich Deklarationen, Struktur- und Typdefinitionen sowie Steueranweisungen an den Präprozessor und den Compiler enthalten.
Die Informationsdateien des Compilers (und der zusätzlichen Anwendungspakete) liegen bei UNIX häufig in den Verzeichnissen /usr/include und /usr/include/sys. Bei den DOS-Compiler hängt der Pfadname vom Compilerhersteller ab. Bei TurboC++ liegen die Informationsdateien im Verzeichnis /tc/include.
Die Informationsdateien erwartet man zumeist im momentanen Arbeitsverzeichnis. Diese Zuordnung ist nicht immer fest eingestellt. Manche Compiler bieten Hilfsmittel an, um eine Liste von Verzeichnissen zu erstellen, die dann durchsucht werden.
Bild 11-7: Einlesen von Informationsdateien
Die #include-Anweisung kennt die folgenden drei Formen.
#include <Dateiname>
#include "Dateiname"
#include Makroname
Die erste Form gibt den Dateinamen, der auch einen Pfad enthalten kann, in einem spitzen Klammerpaar an. Damit wird als Suchbereich der Compiler- und Systembereich angegeben. Alle Informationsdateien, die mit dem Compiler geliefert werden, werden so eingefügt.
Die zweite Form gibt als Suchbereich den Anwenderbereich an. Die Suche wird daher zumeist im momentanen Arbeitsverzeichnis beginnen. Diese Form hat die erste Form als Basisfall. Wird im Anwenderbereich die gewünschte Datei nicht gefunden, dann sucht man im Systembereich weiter.
Bild 11-8: Suche nach Informationsdateien
Eine Informationsdatei, die zum Compiler gehört, würde daher auch dann gefunden, wenn sie in Anführungszeichen angegeben würde.
Die dritte Form erwartet, daß die Expansion eines Makronamens zu einer gültigen Angabe der einzulesenden Datei geführt. das Ergebnis der Expansion muß daher so aussehen, wie in einem der beiden gerade besprochenen Fälle.
Mit dieser Technik kann man z. B. versionsabhängig Makronamen erstellen und an einer anderen Stelle in einer #include-Anweisung benutzen.
Bild 11-9: Suchen mit einem Makronamen
#include-Anweisungen können daher auch innerhalb von Informationsdateien auftreten und so zu einer Verschachtelung führen. Die Compiler haben zumeist Obergrenzen für die Verschachtelungstiefe. Die Grenze liegt meist bei 32 Ebenen.
Im Zusammenhang mit dem verschachtelten Einlesen kann es passieren, daß eine grundlegende Datei in mehreren anderen Dateien eingelesen wird. Es kostet aber unnötige Übersetzungszeit. Daher wird die bedingte Übersetzung in vielen Informationsdateien einen unbeabsichtigten Mehrfacheinschluß verhindern.
Unter einer bedingten Übersetzung versteht man den Einschluß oder Ausschluß von Quelltextzeilen. Die dazu notwendige Entscheidung kann mit Hilfe einer #if-Anweisung sowie booleschen Ausdrücken erreicht werden.
Der Grundaufbau der #if-Anweisung besteht aus einem #if-Teil, der in die Übersetzung aufgenommen wird, wenn der boolesche Ausdruck wahr ergeben hat, einem optionalen #else-Teil für den falschen booleschen Ausdruck und einer schließenden #endif-Anweisung.
Der explizite Abschluß der #if-Anweisung ist notwendig, da der Präprozessor den Quelltext nur einmal liest und dabei das Ende erkennen können muß.
Es gibt vier Formen der #if-Anweisung.
Bild 11-10: Bedingte Übersetzung
Im Fall, daß der Name nicht definiert wurde, werden die gleichen Tätigkeiten mit einer anderen Datei und einem anderen Datentyp durchgeführt. Das angebene Beispiel stammt aus einem UNIX-Programm, das sowohl unter der älteren Version 7 als auch unter dem neueren System V übersetzt werden kann.
Die Umkehrung der Abfrage kann man entweder mit der zweiten Form der #if-Abfrage erreichen oder mit einem Negationsoperator.
Bild 11-11: Frage nach nicht vorhandenen Makro
Im Beispiel (Bild 11) wird die Standard-Informationsdatei stdio.h eingelesen. Sollte darin der Name NULL nicht als Makro definiert worden sein, dann wird zusätzlich die Datei mit den Standarddefinitionen eingelesen.
Um Informationsdateien bedingt einlesen zu können, werden alle Zeilen innerhalb einer #ifdef-Anweisung geschrieben. Als Abfragename dient ein Name, der für diese Datei eindeutig sein sollte. Da es sich hier um eine Systemsteuerung handelt, werden solche Namen oft mit einem Unterstrich eingeleitet. Namen mit einem Unterstrich am Anfang sollte der Programmierer sonst nie verwenden. Sie sind für die Implementierung reserviert.
Wird nun eine Datei zweimal eingelesen, wie es mit der stdio.h im Beispiel (Bild 12 und 13) passiert, dann wird zwar vom Präprozessor auch beim zweiten Mal die Datei geöffnet und gelesen. Die Bearbeitung geht aber viel schneller, da nur die schließende #endif-Anweisung gesucht wird.
Bild 11-12 Aufbau einer Informationsdatei mit Kennung
Will man überhaupt ein mögliches zweites Einlesen verhindern, muß die zugehörige #include-Anweisung ihrerseits in einer Abfrage stehen. Leider findet sich hier eine Schwierigkeit. Die Idendifizierungsnamen für die Informationsdateien sind nicht standardisiert. Sollte das Programm auf eine andere Maschine portiert oder ein anderer Compiler benutzt werden, muß man solche Abfragen anpassen.
Bild 11-13: Bedingtes Einlesen mit Makrokennung
Als Konstante dienen Zahlen, Makronamen oder der Rückgabewert des speziellen, unären Operators defined sowie der Negationsoperator !.
Im Bild 14 wird in der Zeile 4 getestet, ob der Name __STDC__ definiert wurde. Unter einem ANSI-Compiler ist dieser Makroname automatisch definiert und mit dem Ersetzungswert "1" vorbelegt. Nehmen wir einmal an, daß die Quelle mit einem K&R-Compiler übersetzt wird. Dann existiert __STDC__ nicht und der Operator defined ermittelt den numerischen Wert "0". Wäre der Name definiert würde defined eine "1" liefern.
Wie üblich bedeutet "1" wahr und "0" falsch. Mit der Negation wird das Ergebnis von defined invertiert. Die #if-Abfrage findet danach als Ergebnis eine "1" und geht zur #error-Anweisung, die den Übersetzungsvorgang abbricht und eine entsprechende Fehlermeldung liefert. Je nach Compiler kann dies eine Meldung über den Standard-Fehlerkanal sein oder ein Hinweis im Message-Fenster bei TurboC++.
Bild 11-14: Abfrage auf ANSI-Compiler (Version 1)
Will man mit einer #if-Abfrage herausbekommen, ob der verwendete Compiler auch wirklich ANSI-konform ist, dann sollte man sich nicht auf das Beispiel im Bild 14 verlassen.
Bild 11-15: Abfrage auf ANSI-Compiler (Version 2)
ANSI legt fest, daß der Makroname __STDC__ existiert und mit "1" vorbesetzt sein muß während einer konformen Übersetzung. Nun definieren aber manche K&R-Compiler _STDC__ mit einem Ersetzungswert von "0". Als Ausweg bleibt die einfache #if-Abfrage, die nur dann wahr wird, wenn sowohl der Name definiert wurde und der Ersetzungswert nicht "0" ergibt.
Neben der einfachen "#if"-Abfrage kann man mit #elif Abfrageketten aufbauen. Als Beispiel wollen wir den Versionsstand des ANSI-Compilers abfragen. Momentan gibt es die Version 1. Daher wird der Ersetzungswert für __STDC__ zu "1" definiert. In späteren Versionen soll dieser Wert erhöht werden.
In der ersten Abfrage im Bild 16 wird ein boolescher Ausdruck ausgewertet der prüft, ob die Versionsnummer "2" ist. In den relationalen Ausdrücken des Präprozessors sind die gleichen Operatoren wie in C selbst erlaubt. Wird der boolesche Ausdruck als wahr erkannt, wird die Gruppe aus Zeilen bis zur nächsten Anweisung eingeschlosssen. In unserem Fall dürfte die erste Abfrage falsch ergeben.
Bild 11-16: Abfrage der ANSI-Version
Die Abfragekette wird danach mit #elif (else -if) fortgesetzt. Die erste Abfrage, die wahr ergibt, bewirkt, daß die zugehörigen Zeilen in die Übersetzung übernommen werden. Alle weiteren Abfragen werden in diesem Fall übersprungen.
Bei der Berechnung der booleschen Ausdrücke sind auch mathematische Operationen mit einer long / unsigned long Arithmetik erlaubt, sodaß die Werte recht groß werden können.
Bild 11-17: Auswertung numerischer Ausdrücke
Dies funktioniert zwar auch durch eine einfache Neudefinition, führt aber zu einer Compilerwarnung.
Bild 11-18: Korrekte Neudefinition eines Makronamens
__LINE__ Die dezimale Zeilennummer innerhalb
der Quelldatei
__FILE__ der Name der Datei, die gerade übersetzt
wird
__DATE__ das Datum, entspricht der asctime()-Funktion
__TIME__ die Übersetzungszeit, wie bei
asctime()
__STDC__ 1 bei einer ANSI konformen Übersetzung
Diese Makronamen können weder mir #define noch mit #undef beeinflußt werden. Der Programmierer kann mit ihrer Hilfe Testinformationen ausgeben. Für den Compiler bieten die internen Makros die Möglichkeit, Testinformationen in die Objektdatei zu legen, die dann von einem symbolischen Debugger verwendet werden. Für jede Zeile wird beispielsweise eine eigene Marke definiert, um im Debugger den Befehl Laufe bis zur Zeile 100 möglich zu machen.
Als Beispiel für eine Fehlermeldung kann der Test auf den ANSI-Standard sein. Wenn beim Einlesen einer Informationsdatei festgestellt wird, daß die Übersetzung nicht mit einem ANSI-Compiler erfolgt, dann soll eine entsprechende Meldung ausgegeben werden. In vielen Fällen bricht der Präprozessor auch die weitere Übersetzung ab.
Im Standard wird aber der Abbruch nicht spezifiziert.
Bild 11-19: Test auf ANSI-Übersetzung
Die Grundidee ist, daß man mit Hilfe des assert()-Makros Testpunkte im Programm festlegt. An diesen Testpunkten wird ein boolescher Ausdruck ausgewertet. Dabei wird die Zusicherung (assertion) gegeben, daß der Ausdruck sich als wahr erweist. Ist dies nicht der Fall, wird mit einem Ausgabebefehl der Testpunkt und die Testbedingung angezeigt. Insbesondere unter UNIX wird zusätzlich das Programm beendet.
Bild 11-20: Definition des assert-Makros
Mit Hilfe der bedingten Übersetzung fügt man nun die zusätzlichen Abfragen und Ausgaben ein oder unterläßt es. Die Entscheidung wird mit Hilfe des Makronamens NDEBUG getroffen. Sind assert()-Makros eingefügt, werden normalerweise die Testanweisungen eingefügt. Eventuelle Laufzeitfehler werden so hoffentlich gefunden. Am Ende des Projektes definiert man dann nur noch den Makronamen NDEBUG und übersetzt neu. Dabei werden alle zusätzlichen Testpunkte entfernt.
Wurde NDEBUG nicht definiert, wird das Makro (in diesem Fall) möglicherweise zu einer speziellen printf()-Anweisung expandiert. Zuerst wird der Parameter als boolescher Ausdruck verstanden und in einer bedingten Anweisung als Testausdruck verwendet. Ergibt der Testausdruck wahr, dann erfolgt die gleiche Expandierung zu einer leeren Anweisung wie im Fall eines undefinierten NDEBUG-Makronamens.
Bild 11-21: Testpunkte mit assert()-Makro
Im Fehlerfall erhalten wir die printf()-Anweisung, die den übergebenen Ausdruck sowie Dateinamen und Zeile ausgibt. Beachten Sie den #-Operator, der den Ausdruck zu einem Text wandelt.
Es gibt wieder drei Formen.
#line 1234
#line 1234 "Dateiname"
#line Makroname
Steht nach der Anweisung ein Zahlenwert, dann wird die interne Zeilennummer neu gesetzt. Dies mag bisweilen sinnvoll sein, um bestimmte Stellen für das assert()-Makro zu kennzeichnen.
Die zweite Form legt neben einer neuen Zeilennummer auch einen neuen Dateinamen fest. Dies bewirkt keine Einschließeung, wie bei #include, sondern nur eine Information, die zu Testzwecken ausgewertet werden kann.
Steht hinter der #line-Anweisung ein Makroname, dann muß seine Expansion zu einer der beiden zuerst genannten Formen führen.
Im Gegensatz zu den anderen Präprozessoranweisungen wird #line bei manchen Compilern auch an den eigentlichen Compilerteil weitergereicht. Der Compiler erzeugt dann die Debuginformation in der Objektdatei. Bei UNIX-Compilern werden oft am Beginn jeder neuen #include-Datei der Dateiname und die Zeilennummer mit #line bekanntgegeben und an den Compiler weitergereicht.
Ein Beispiel dazu befindet sich im folgenden Abschnitt über den Standalone-Präprozessor.
Die hinter #pragma stehenden Schlüsselworte sind compilerspezifisch und werden von anderen Compilern, die diese Schlüsselworte nicht verstehen, schlicht überlesen.
Die Folge ist, daß immer Funktionen bereitgestellt werden. Gibt es zusätzlich Makros, haben sie den Vorrang, da sie der Präprozessor bereits durch einen Ersetzungstext austauscht.
Im Beispiel können wir auf unterschiuedliche Arten die Verwendung einer Funktion erzwingen. Die einfachste ist sicher, den vorhandenen Makronamen mit #undef zu entfernen. Danach bleibt nur noch die gleichnamige Funktion übrig.
Bild 11-22: Erzwungene Funktionsauswahl
Eine andere Möglichkeit ist es, den Funktionsnamen in ein rundes Klammerpaar einzuschließen. Nach einem Makronamen mit Parametern muß eine sich öffnende runde Klammer folgen, um die Expansion einzuleiten. In unserem Fall folgt die schließende Klammer und verhindert so die Makroexpansion. Wieder bleibt nur die Funktion.
Funktionen werden immer dann verwendet, wenn der Programmeirer die Adresse einer solchen Funktion benötigt, die auch unter dem gleichen Namen als Makro definiert wurde, oder eine eigene Funktion geschrieben hat, die die Bibliotheksfunktion ersetzen soll.
cpp ratio1.c
Der Aufruf des Präprozessors erfolgt üblicherweise mit der angegebenen Kommandozeile. Die verschiedenen CPP-Hersteller haben verschiede Optionen hinzugefügt, die bei Bedarf im jeweiligen Handbuch zu finden sind.
Zwei übliche Optionen sollen trotzdem hier herausgegriffen werden, da sie für die Benutzung von Bedeutung sind. Bei Aufruf kann man Namen definieren oder löschen.
cpp -DV7 -ULINE ratio1.c
Die definierten Namen sollten innerhalb des Quelltextes ausgewertet werden. Ein häufiger Fall ist die Übersetzung unterschiedlicher Versionen. Die Unterscheidung, welche Version im momentanen Übersetzungslauf erzeugt werden soll, geschieht dann durch die Definition des entsprechenden Makronamens.
Wurde das Beispiel mit dem definierten Makronamen V7 aufgerufen, dann wird die eine Version übersetzt und ohne diesen Namen wird standardmäßig die zweite Version übersetzt. Das Beispiel ist ein Auszug aus einem Programm, das auf einem älteren COHERENT-System erstellt wurde, aber auch auf einem UNIX System V übersetzt werden kann.
Bild 11-23: Auszug aus einer Präprozessordatei
Die Ausgabe des Präprozessors wird normalerweise direkt in den weiteren Phasen des Compilers weiterverarbeitet. Die Ausgabedatei ist dann eine sogenannte Objektdatei. Sie enthält den Maschinencode, Bindeinformationen und Debuginformationen.
Der Maschinencode besteht aus den Bitmustern, die die verwendete Maschine erkennen kann. Der Maschinencode ist aber noch nicht ablauffähig. Es fehlen ihm noch die richtigen Adressen. Ein Programm, daß die Funktion printf() aufruft, hat den Code von printf() nicht zur Verfügung. Er wurde ja von einem ganz anderen Programmierer geschrieben. An den Stellen im eigenen Programm, an denen printf() aufgerufen wird, hat der Compiler einfach die Adresse "0" eingetragen. Dies ist praktisch ein Loch im erzeugten Code. Es wird die Aufgabe des Linkers sein, mit Hilfe der Bindeinformationen im Objektmodul die richtigen Adressen nachzutragen.
In der Ausgabedatei des Präprozessors findet sich bei TurboC++ vor jeder Zeile die Angabe aus welcher Datei die Zeile stammt und welche Zeile innerhalb der gerade bearbeiteten Datei dies ist. Damit kann dann der Compiler Fehlermeldungen erstellen, die Datei und Zeile des gefundenen Fehlers angeben. Die Information wird vom Compiler auch benutzt, um in der Objektdatei die Informationen abzulegen, die ein symbolischer Debugger benötigt, um beim Testen Namen und Inhalte anzeigen zu können.
Unter UNIX sieht die Ausgabe etwas anderes aus. Sie enthält zusätzliche Präprozessoranweisungen, die ebenfalls dazu dienen, Zeilennummern und Dateien anzugeben. Die Zeilennummern werden mit der Anweisung #line eingestellt. Zusätzlich gibt der hier verwendete Präprozessor (COHERENT 3.2) immer dann den Dateinamen bei der #line-Anweisung an, wenn eine neue Datei eingelesen wird. Dies entspricht auch dem ANSI-Standard.
Sollten Sie mit UNIX-Umgebungen experimentieren wollen, eignet sich natürlich insbesondere Linux. Ab der Version 2.1 steht hier ein POSIX-kompatibles Programmierssystem zur Verfügung.
Bild 11-24: CPP-Ausgabe unter UNIX