21. Präprozessor

Der Präprozessor von C hat, wie gesagt, mit der eigentlichen Sprache wenig zu tun – er ist aber mittlerweile Teil des C-Standards. Seine Funktion ist, etwas veredelte Suchen-und-Ersetzen-Operationen auf den Quelltext des Programms laufen zu lassen, bevor der Compiler überhaupt etwas sieht. Wenn ihr verstanden habt, dass der Präprozessor wirklich kaum mehr als Suchen-und-Ersetzen macht, seid ihr gegen 80% der klassischen Fehler mit dem C-Präprozessor gefeit.

Symbolische Konstanten

Konstanten außer 0, 1 und (manchmal) 2 sollten nie im Quelltext vorkommen. In Ausnahmefällen kommen globale Variablen in Frage, normalerweise aber Präprozessor-Konstanten mit aussagekräftigen Namen in Großbuchstaben:

#define MAX_LINE_LENGTH 80
#define CONFIG_FILE_NAME ".progrc"

Anweisungen an den Präprozessor – Namen werden “als Text” ersetzt, deshalb kein Strichpunkt.

Eine semantische Einheit – ein Name! Nur weil irgendetwas für euch gerade den gleichen Wert hat, sollte es noch lange nicht von einer gemeinsamen Präprozessor-Konstante kontrolliert werden.

Damit ist gemeint, dass, wenn etwa zusätzlich noch

#define MAX_FNAME_LEN 80

wäre, die maximale Länge einer Zeile also mit der eines Dateinamens übereinstimmen würde, es trotzdem schlecht wäre, ein Symbol für beide Limits zu verwenden. Was, wenn MAX_FNAME_LEN aus irgendwelchen Gründen plötzlich kleiner werden müsste?

Schlecht außerdem:

#define FIVE 5

Der Makroname FIVE trägt keine Information, die über das Literal 5 hinausgeht. Wirklich schlimm wird das dann, wenn irgendwer auf die schlaue Idee kommt, dass dort, wo bisher die Fünf stand, jetzt besser eine Neun sein sollte – eine Definition

#define FIVE 9

ist weit schlimmer als gar keine Makros.

Makros

Der Präprozessor kann auch Makros mit Parametern, was ähnlich wie bei Funktionen aussieht:

#define SQR(a) ((a)*(a))
SQR(8);SQR(x+x);SQR(i++);

Dabei ist wichtig, dass zwischen dem Makronamen und der öffnenden Klammer kein Blank ist, denn der Präprozessor erkennt dadurch den Unterschied zwischen

#define PAR_OPEN (

und

#define NO_SQR(a) a*a

Der Präprozessor macht nur Textersetzungen. Das bedeutet, dass, wann immer irgendwo NO_SQR(text) steht, der Präprozessor daraus text*text macht, unabhängig davon, was text wohl sei, und zwar, noch bevor der Compiler überhaupt zum Code kommt. Das ist beispielsweise beim Ausdruck NO_SQR(a+a) ziemlich sicher nicht das, was man haben wollte.

Der Compiler sieht davon:

((8)*(8));((x+x)*(x+x));((i++)*(i++));

Also: Lieber mehr als weniger Klammern, Namen von Makros in Großbuchstaben als Warnung, dass es sich um Makros handelt, Makros nicht übertreiben und im Zweifelsfall lieber Funktionen nehmen.

Warum will man Makros dann eigentlich haben? Früher war das Hauptargument, dass Makros “schneller” sind als Funktionen (durch die Textersetzung erübrigt sich ein Funktionsaufruf); heute ist das aus einer Reihe von Gründen nicht mehr so interessant. Nach wie vor kann der gezielte Einsatz von Makros die Les- und Portierbarkeit von C-Programmen aber erheblich verbessern. Es gibt auch einige eher arkane Probleme, die den Compiler vor unlösbare Aufgaben stellen würden, die mit dem Präprozessor aber einfach zu lösen sind.

Bedingte Compilierung

Wir haben schon #if 0 zum Auskommentieren gesehen. Allgemeiner kann man prüfen, ob bestimmte Konstanten gesetzt sind oder nicht (#ifdef FOO bzw. #ifndef BAR) oder ob sie einen bestimmten Wert haben (#if BAZ==0). Wiederum ist das vor allem wichtig, um Programme portabel zu machen.

So definiert etwa GNU C von selbst das Makro __GNUC__; Code, der nur unter GNU C compiliert, kann so vor anderen Compilern versteckt werden. Compiler, die ANSI C versehen, definieren __STDC__ usf.

#include <stdio.h>

int main(void)
{
#ifdef __STDC__
  printf("ANSI-C\n");
#endif
#ifdef __GNUC__
  printf("GNU C\n");
#endif
  return 0;
}

Man kann ähnliches auch mit eigenen Konstanten machen, etwa

#define SPECTACULAR_SHOW 1  /* undefine to skip flashy intro */

....

#ifdef SPECTACULAR_SHOW
  playIntro()
#endif

Übungen zu diesem Abschnitt

Ihr solltet euch wenigstens an den rötlich unterlegten Aufgaben versuchen

(1)

Ihr erinnert euch vielleicht, dass ihr euch mit gcc -E die Ausgabe des C-Präprozessors ansehen könnt. Seht euch an, was der Präprozessor aus folgendem Text macht (der natürlich kein C-Programm darstellt und auch keine korrekte C-Syntax ausspuckt):

#define NO_SQR(a) a*a
#define SQR(a) ((a)*(a))
#define ASSIGN(a,b) a = b

Hier ein bisschen Murks:
NO_SQR(8)  Tjaja...  NO_SQR(x+x)    Wird nicht besser...   SQR(i++);

#define Murks besserer Kram

Hier ein bisschen Murks:
SQR(8)  Oho.  NO_SQR(x+x)    Passt scho.   SQR(i++);

#if 0
Das hier könnt ihr gar nicht lesen
#endif

ASSIGN(1,2)

Erklärt die Ausgabe des Präprozessors. Fällt euch noch etwas auf?


Markus Demleitner

Copyright Notice