ZurückZum InhaltVorwärts

Präprozessor

Vor der eigentlichen Übersetzung durch den Compiler wird der Quelltext zuerst durch einen Präprozessor bearbeitet. Präprozessoren bearbeiten die Quelldatei ähnlich einem spezialisierten Editor. Die wichtigsten Aufgaben des Präprozessors sind bedingte Übersetzungen durch Ein- oder Ausschluß von Quellcode, Makroverarbeitung, Fehlerüberprüfung sowie die Handhabung von Besonderheiten eines speziellen Compilers.

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.

Definition von Makros

Es gibt zwei Arten von Makros in C: symbolische Konstante und parametrisierte Makros. Die einen können wie Text- oder numerische Konstante verwendet werden und die anderen verhalten sich beim Aufruf ähnlich den Funktionen.

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.

Mehrfachdefinition des gleichen Makronamens

Wird ein Makroname wiederholt definiert, können sich Probleme ergeben. In C ist die mehrfache, identische Definition korrekt. Dieser Fall kommt innerhalb der Informationsdateien des ANSI-Standards vor, z. B. bei der Definition von NULL. Die zentrale Stelle ist die Datei stddef.h. Die Definition wird aber auch in der stdio.h wiederholt.

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

Makrodefinition ohne Ersetzung

Einem Spezialfall der Makrodefinition begegnet man öfter in Informationsdateien. Pro Informationsdatei wird ein eindeutiger Name definiert, der diese Datei kennzeichnet. Diesen Namen verwendet man im Zusammenspiel mit der bedingten Übersetzung. Ist der Name noch nicht bekannt, dann kann die Datei eingelesen und verarbeitet werden; ist er bekannt, ist dies nicht mehr notwendig.

Sehen Sie sich vielleicht einmal die stdio.h an. Ein Beispiel finden Sie bei der bedingten Übersetzung.

Parametrisierte Makros

Komplexere Makros werden mit Hilfe eines Klammerpaares eingeführt. Folgt dem Makronamen unmittelbar eine sich öffnende runde Klammer, dann handelt es sich um ein Makro mit Parametern.

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.

Widerholung der Makroexpansion

Im Beispiel wurde das Makro zweimal verwendet. Im ersten Aufruf werden Feldvariable übergeben, im zweiten auch ein weiteres Makro. Der Präprozessor wird die Überprüfung des Ersetzungstextes in einer internen Schleife wiederholen, bis keine Ersetzung mehr möglich ist. Im Beispiel erfolgen beim zweiten Aufruf zwei Ersetzungen. Zuerst wird das Makro max mit seinen Parametern expandiert. Dabei erhalten wir aber nach der Expansion erneut einen Makronamen: GROESSE. Der gerade erzeugte Ersetzungstext wird erneut geprüft und die symbolische Konstante mit ihren Ersetzungswert expandiert.

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.

Klammern vermeiden Fehlerquellen

Bei jeder Expansion des Makros stehen die Parameter geklammert. Der Grund dafür ist, daß der Präprozessor nur Texte ersetzt, ohne ihren Aufbau zu verstehen. Besteht nun ein Text aus mehreren Operanden, dann würde in manchen Fällen die Expansion zu völlig unerwünschten Resultaten führen.

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.

Neue Eigenschaften des Präprozessors

In ANSI-C wurden für den Präprozessor zwei neue Operatoren definiert: Textersetzung und Textverbindung.

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

Einlesen anderer Dateien

Bei der Übersetzung einer Datei mit Quellcode ist es oft sinnvoll, eine andere Datei als Teil der eigenen Übersetzung einzulesen. Es gibt dabei zwei unterschiedliche Zielrichtungen.

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.

Ablageorte der Informationsdateien

Die Informationsdateien können im Prinzip in einem beliebigen Verzeichnis stehen. Zumeist unterscheidet man aber zwei Ablagebereiche: beim Compiler und beim Anwender.

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

Einlesen mit "include"

Die Präprozessoranweisung zum Einlesen anderer Dateien heißt #include. Bei der Bearbeitung der Anweisung wird die Anweisungszeile entfernt und durch den Inhalt der hinter #include angegebenen Datei ersetzt.

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

Verschachtelung von #include-Anweisungen

Wenn die Projekte größer werden und damit die #include-Dateien zahlreicher, möchte man oft mit dem Einlesen einer logisch höheren Informationsdatei alle weiteren dazu benötigten gleich mit einlesen.

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

Bedingte Übersetzung

In den bisherigen Themen dieses Kapitels klang wiederholt das Problem der bedingten Übersetzung an.

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.

Beginnen wir mit der Abfrage vorhandener Makronamen. Im nächsten Beispiel soll für den Fall, daß der Makroname V7 bekannt ist, eine spezielle Informationsdatei eingelesen, sowie zwei Strukturvariable mit einem, darin definierten, Datentyp angelegt werden.

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.

Bedingter Einschluß von Informationsdateien

Mit Hilfe des Test, ob ein Name schon bekannt ist, werden oft auch Informationsdateien bedingt eingelesen. Dies spart u. U. erheblich an Übersetzungszeit. Außerdem ist es nicht sehr sinnvoll, die gleichen Makronamen mehrfach zu definieren.

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

Auswertung von numerischen Ausdrücken

Damit bleiben noch die dritte und vierte Form der #if-Anweisung zu besprechen, die einen Ausdruck bestehend aus Konstanten auswerten.

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

Entfernen definierter Makronamen

Ein bereits definierter Makroname kann mit der #undef-Anweisung entfernt werden. Dies kann in Fällen nützlich sein, wenn man einem Makronamen einen neuen Wert zuweisen will.

Dies funktioniert zwar auch durch eine einfache Neudefinition, führt aber zu einer Compilerwarnung.

Bild 11-18: Korrekte Neudefinition eines Makronamens

Vordefinierte Makronamen

Während einer Übersetzung werden die folgenden Makronamen automatisch vordefiniert.

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

Die Fehleranweisung #error

In bestimmten Sonderfällen ist die Übersetzung einer Datei nicht sinnvoll. Wenn dies bereits der Präprozessor erkennen kann, dann kann er mit Hilfe der #error-Anweisung eine diagnostische Meldung veranlassen.

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

Fehlerüberprüfung mit dem assert-Makro

In der Informationsdatei assert.h wird ein spezielles Makro definiert, daß bei der Fehlersuche unterstützen soll und dies mit den vorgestellten Standardmakros tut.

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.

Der Aufbau des Makros

Das Makro testet die Existenz von NDEBUG (nicht debuggen). Im Fall, daß NDEBUG nicht existiert, expandiert das Makro zu der Zahlenkonstanten "0", die als void * typgewandelt wird. Da dem Makro bei der Verwendung ein Strichpunkt folgt, ist das Ergebnis hier eine leere Anweisung, die der Compiler wegoptimieren kann.

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.

Explizite Angabe von Zeilen und Dateien

Innerhalb einer Übersetzung werden, wie besprochen, die aktuelle Zeilennumer mit dem Makro __LINE__ und die gerade übersetzte Datei mit dem Makro __FILE__ beschrieben. Der Programmierer kann nun innerhalb einer Datei bestimmen, welche Nummern und Dateinamen in diesen Makros stehen sollen. Dazu dient die Anweisung #line.

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.

Compilerspezialitäten

Die meisten Compiler haben eine ganze Menge an Optionen. Um dem Programmierer die Möglichkeit zu geben, die speziellen Steueranweisungen für den verwendeten Compiler mit in die Quelldatei aufnehmen zu können, wurde eine eigene Anweisung geschaffen. Sie heißt #pragma.

Die hinter #pragma stehenden Schlüsselworte sind compilerspezifisch und werden von anderen Compilern, die diese Schlüsselworte nicht verstehen, schlicht überlesen.

Funktionsaufruf oder Makroexpansion

Innerhalb der Standardbibliothek werden eine Reihe von Namen definiert, die als Makro geschrieben wurden. Viele kleine Funktionen, wie putchar() oder getchar() sind historisch immer Makros gewesen. Im Standard wird nun aus Gründen der Einheitlichkeit gefordert, daß man von allen funktionsähnlichen Namen auch die Adresse ermitteln können soll.

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.

Expliziter Aufruf des Präprozessors

Der Präprozessor wird im Normalfall automatisch als erster Teil des Übersetzungslaufes aufgerufen. Bei den meisten Compilern gibt es ihn aber auch als getrenntes Programm.

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


Zum Inhalt