#define
-Direktive
Mittels der #define
-Direktive wird einem Namen ein Makro zugewiesen, welches im gesamten darauf folgenden Code den Namen ersetzt. Makros können Parameter besitzen, und somit im Code ähnlich wie Funktionen aufgerufen
werden. Der Gebrauch solcher Makros ist jedoch heimtückisch.
PI = 3.140000
I love ManderC
Hypothenuse = 5.00
Details
Ein Makro ist grundsätzlich nichts anderes als ein Text-Schnipsel, welcher beim Compilieren in einem Vorverarbeitungs-Schritt (Pre-processing) mittels einfacher Ersetzung an die Stellen kopiert wird, wo der Name des Makros auftritt. Mit Ausnahme von bestimmten Preprozessor-Operatoren und eventuellen Makro-Parametern werden einfach jegliche Zeichen (inklusive Whitespaces) des bei der #define
-Direktive angegebenen Texts eins-zu-eins kopiert und daraufhin vom eigentlichen Compiler abgearbeitet.
Makros sind sehr gut geeignet für allgemeingültige Konstanten wie Pi, e, Wurzel-2, die Erdanziehungskraft g und ähnliche. Solche unveränderbare konstante Werte sind insofern bevorzugt als Makros zu definieren, da sie nicht wie eine Variable während dem Programmablauf erst aus dem Speicher gelesen werden müssen, sondern direkt im Programmcode compiliert sind.
Makros können das Schreiben von repetitivem Programmcode vereinfachen, da sie an fast jeder beliebigen Stelle eingesetzt werden können und somit mühselige Schreibarbeit verhindern.
Des weiteren werden Makros oftmals zur bedingten Compilierung verwendet, beispielsweise mit Flags wie #define USE_ACCURATE_METHOD 1
. Wenn der Name eines Makros nicht definiert ist, so wird jegliche Nutzung in einer Preprozessor-Bedingung zu 0
auswerten. Wenn ein nicht-definiertes Makro im zu übersetzenden Code verwendet wird, so meldet der Compiler einen Fehler.
Makros werden immer erst dann ausgewertet, wenn sie gebraucht werden, jedes Mal von neuem:
Makros werden üblicherweise mit GROSSBUCHSTABEN geschrieben. Des weiteren wird empfohlen, möglichst kleine, überschaubare Makros zu schreiben. Ein Makro muss in einer einzelnen Zeile definiert werden. Mehrzeilige Makros können mittels einem Backslash \ am Ende der Zeile erreicht werden.
Makros sind nicht unproblematisch! Weiter unten werden einige Beispiele gegeben, wo Fehlerquellen liegen und wie Makros aufgrund dessen möglichst sicher geschrieben werden können.
Parametrisierte Makros
Makros können ähnlich wie Funktionen Parameter definieren. Hierzu muss direkt hinter dem Makro-Namen eine durch Komma ,
getrennte Liste von Parameternamen in runden Klammern ()
folgen. Zwischen dem Makro-Namen und der öffnenden runden Klammer darf kein Whitespace sein! Parametrisierte Makros können mit den Auslassungspunkten ...
auch beliebig viele Parameter besitzen.
Bei Auftreten eines parametrisierten Makros wird der Preprozessor den Makro-Text wie oben beschrieben an die aufgetretene Stelle kopieren, daraufhin jedoch noch zusätzlich sämtliche Parameternamen durch die entsprechenden übergebenen Argumente ersetzen. Im folgenden Beispiel steht links die Ausgabe des Preprozessors:
Es ist zu beachten, dass ein Makro-Argument nicht wie bei einer Funktion einen Wert darstellt, sondern ein beliebiger Text sein kann. Der Preprozessor wird somit blind die Parameter durch die Argumente übersetzen und übergibt das Resultat schlussendlich dem Compiler.
Wie soeben in diesem Beispiel gezeigt, können aufgrund der blinden Ersetzung von Makros und deren Parameter viele Probleme auftreten. Weiter unten werden einige wichtige Fehlerquellen erläutert.
Makro-Argumente mit Nebeneffekten
Der Gebrauch von Makros mit Argumenten gilt im allgemeinen als unschön und ist entsprechend nicht überall gleich beliebt. Oftmals geht vergessen, dass Makros nicht wie Variablen oder Funktionen ausgelesen oder aufgerufen werden, sondern durch den Preprozessor Zeichen für Zeichen direkt in den Programmcode hineingeschrieben werden. Folgendes Programm verdeutlicht die Problematik:
520932930, 520932930
28925691, 822784415
Während bei der ersten Variante zweimal derselbe Wert ausgegeben wird, werden bei der Makro-Variante zwei verschiedene Werte ausgegeben, obschon das Makro und die Funktion denselben Code definieren. Der Unterschied besteht darin, dass bei der ersten Variante zuerst die Funktion rand()
aufgerufen wird und das Resultat (Eine Zufallszahl) als Argument an die Funktion übergeben wird. Bei der Makro-Definition hingegen wird der Code rand()
selbst übergeben, was der Preprozessor schlussendlich in folgende Zeile umwandelt:
Dies bedeutet, dass rand()
zweimal aufgerufen wird. Die rand
-Funktion hat jedoch einen Nebeneffekt: Bei jedem Aufruf wird eine versteckte Variable verändert. So verändert sich auch das ausgegebene Resultat.
Wann immer somit Makro-Parameter mehrfach im Makro-Text vorkommen, so werden auch die entsprechenden Nebeneffekte eines Ausdruck mehrfach ausgeführt. Dummerweise ist es genauso schwierig, keine Argumente mit Nebeneffekt zu verwenden, wie Makros zu schreiben, bei welchen die Parameter garantiert nicht mehrfach im Makro-Text vorkommen. Glücklicherweise treten entsprechende Fehler eher selten auf. Die Empfehlung des Autors ist, komplizierte Makros spätestens im Falle eines Fehlers durch eine entsprechende Funktion (möglicherweise mit dem inline
-Keyword) zu ersetzen.
Weitere Beispiele für Ausdrücke mit Nebeneffekten sind Zuweisungen oder die Inkrement- und Dekrement-Operatoren. Weitere Erläuterungen dazu können bei den Sequenz-Punkten nachgelesen werden.
Direktiven als Makros
Makros können keine weiteren Direktiven enthalten. Sowohl das führende #
, als auch der Name der Direktive werden durch den Preprozessor nicht aufgelöst:
expected unqualified-id
before '#' token
Der Fehler, der hierbei entsteht, ist verwirrend. Er entsteht durch den Compiler, nicht durch den Preprozessor. Das obige Beispiel wird in folgende Zeile übersetzt:
Daraufhin jedoch beendet der Preprozessor seinen Dienst und übergibt diesen Code an den eigentlichen Compiler, der mit dem #
nichts anfangen kann.
Das überzählige Semikolon ;
Da der Preprozessor Zeichen für Zeichen umwandelt, ist es nicht notwendig, ein Makro mit einem Semikolon ;
abzuschliessen, wenn der Ausdruck im eigentlichen Code wie eine Funktion aussehen soll.
sqrt(3*3 +4*4);;
sqrt(3*3 +4*4);
Diese Massnahme sieht in diesem Beispiel aus wie eine unwichtige Schönheits-Korrektur. Wenn jedoch ebendieses Makro nicht als alleinstehende Anweisung, sondern als Teil eines Ausdruckes geschrieben wird, so ist das zusätzliche Semikolon sogar äusserst problematisch, da das Semikolon in den Ausdruck mit hineinkopiert wird, was zu einem Syntax-Fehler führt:
Des weiteren gibt es im Zusammenhang mit Makros noch ein Phänomen, welches als Verschluckung des Semikolons
bezeichnet wird. Es hat damit zu tun, dass vor einem else
-Keyword einer einzeiligen if
-Bedingung ein Makro geschrieben wird, welches als ein Code-Block ausprogrammiert ist. Folgendes Beispiel illustriert dies:
Hierbei wurde angenommen, dass das Makro ähnlich wie eine Anweisung stets mit einem Semikolon abgeschlossen werden kann. Durch die schliessende geschweifte Klammer des Makros wurde jedoch der if
-Teil syntaktisch bereits abgeschlossen. Die C-Syntax erwartet nun entweder das else
-Keyword oder aber eine neue Anweisung. Durch die zusätzliche Angabe des Semikolons nimmt der Compiler letzteres an und betrachtet die if
-Anweisung als abgeschlossen. Daraufhin findet er ein else
ohne ein dazugehöriges if
und meldet einen Fehler.
Um den Fehler zu beheben, muss entweder das Semikolon entfernt werden, oder das Makro muss so programmiert werden, dass es das nachfolgende Semikolon verschluckt
. Üblicherweise wird dies durch folgendes Konstrukt gelöst:
Die Implementation des Makros befindet sich dadurch immer noch in einem Block, ist aufgrund der do-while
-Schleife jedoch als Kontrollstruktur realisiert, welche stets ein Semikolon am Ende erwartet. Mit der Abbruchbedingung 0
wird garantiert, dass die Schleife genau ein einziges Mal durchläuft. Der Compiler wird diese Konstruktion schlussendlich herausoptimieren.
Klammerung von Berechnungen
Für den Preprozessor oder Compiler gibt es feste Regeln, in welcher Reihenfolge Operanden umgesetzt werden. Diese Reihenfolge kann unter anderem mit Klammerung gesteuert werden. Bei Makros wird empfohlen, um Berechnungen herum stets Klammern zu setzen. Folgendes Beispiel zeigt, was passiert, wenn eine Berechnung nicht korrekt geklammert wird:
1 / PI_HALF: 0.159236
1 / PI_HALF: 0.636943
Der Preprozessor übersetzt den Code in die folgenden Zeilen:
In der ersten Zeile wird zuerst 1 / PI
gerechnet und danach das Resultat durch 2
geteilt. Korrekt wäre jedoch, dass wie in der zweiten Zeile zuerst PI / 2
berechnet wird, und vom Resultat dann der Kehrwert genommen wird.
Klammerung von Casts
Oftmals werden in Makros explizite Casts gemacht. In den allermeisten Fällen werden damit keine Probleme auftreten, allerdings zeigt folgendes Programm ein einfaches Beispiel dessen, was passieren könnte:
Int of 1.5 + 1.5 is 2.50
Int of 1.5 + 1.5 is 3.00
Die beiden Zeilen mit der Berechnung werden vom Preprozessor folgendermassen umgewandelt:
Damit ist klar, dass bei der ersten Zeile nur a
gecastet wird, wohingegen auf der zweiten Zeile die gesamte Berechnung a + 1.5
in einen Integer umgewandelt wird.
Die Fehler bei vergessener Klammerung von Casts können noch schlimmer ausfallen, sobald sie im Zusammenhang mit Pointer-Umwandlungen gebraucht werden, das Programm kann hierbei sogar abstürzen. es ist zu empfehlen, um das gesamte Makro zusätzliche Klammern hinzuzufügen. Folgendes Beispiel versucht, ein zweidimensionales Array als eindimensionales zu casten:
Number: -1073743256
Number: -1073743256
Number: 2
Die Zeilen werden vom Preprozessor folgendermassen umgewandelt:
Während in den ersten beiden Zeilen strickt nach der von der Programmiersprache vorgegebenen Operatorenreihenfolge zuerst das Element mit Index [1]
angesprochen und danach fälschlicherweise dereferenziert wird (was zu einer Zahl im Nirvana des unbenutzten Speichers führt), wird erst in der dritten Zeile korrekterweise das Array zuerst als eindimensionales Array gecastet und danach das Element mit Index [1]
angesprochen.
Klammerung von Dereferenzen
Folgendes Beispiel zeigt, was passiert, wenn eine Dereferenzierung nicht korrekt geklammert wird:
Der Preprozessor übersetzt den Code in die folgenden Zeilen:
Während bei der ersten Zeile zuerst das erste Dreier-Tupel {1,2,3}
dereferenziert wird und danach das Element 2
mit Index [1]
ausgewählt wird, wird bei der zweiten Zeile zuerst das Dreier-Tupel {4,5,6}
mit Index [1]
ausgewählt und dann das erste Element 4
dereferenziert.