Bei einer Wertzuweisung wie y=3*f(x)-7 verbirgt sich hinter f(x)
in den meisten Programmiersprachen ein Funktionsaufruf. Bei
Ausführung des Aufrufs muß die Kontrolle dann an die Stelle
gelenkt werden, an der die Funktion definiert ist. Die Argumente
sowie die Adresse, an der der Aufruf stattfand, müssen auf dem
Stack abgelegt werden. Nach Abarbeitung der Funktion kann die
Adresse wiedergeholt werden, um an der auf den Aufruf folgenden
Stelle weiterzumachen.
Das alles sind Aktionen, die bei oftmaligem Vorkommen ein Programm
stark verlangsamen können. Die Programmiersprache C bietet daher
die Möglichkeit, anstelle von Funktionen Makros zu benutzen. Zur
Laufzeit des Programms entfällt der Sprung zum Unterprogramm, da
der auszuführende Code direkt an diejenige Stelle geschrieben
wird, an der sonst der Aufruf des Unterprogramms gestanden hätte.
Auch der Wert der Parameter wird direkt dort eingesetzt. Der
Nachteil dieser Methode ist sofort ersichtlich: Wird ein Makro an
zehn verschiedenen Stellen im Programm benutzt, wird auch derselbe
Code zehnmal eingefügt. Das kann ein Programm nicht unwesentlich
aufblähen.
Definition von Makros
Wie werden Makros nun definiert? Dazu dient der Befehl #define.
Dahinter muß der Makroname mit Parameterliste folgen, zwischen dem
Namen und der öffnenden Klammer darf kein Leerzeichen stehen. Nach
dem Ende der Parameterliste und einem Leerzeichen erfolgt die
Erklärung des Makros, also der Programmtext, für den der Makroname
stehen soll. Hier der Vergleich einer Funktion und
eines Makros, die beide demselben Zweck dienen:
f(x)
int x;
{
return (4*x+3);
}
#define F(x) (4*(x)+3)
|
Verarbeitung von Makros
Zur Auswertung von Makros existiert ein Präprozessor, der mit dem
Compiler nichts zu tun hat. Vielmehr tritt er bereits vor dem
Compiler in Aktion. Dieser verarbeitet dann das Resultat des
Präprozessors. Wie arbeitet der Präprozessor? Nun, die #define-
Anweisung leistet einen reinen Textersatz. Rufen wir unser Makro
F(x) aus Listing 1 in der Form y=3*F(x)-7 auf, so verändert der
Präprozessor lediglich den Quelltext und expandiert den Ausdruck
zu y=3*(4*(x)+3)-7. Der Compiler bekommt den ersten Ausdruck also
gar nicht zu Gesicht, sondern nur den zweiten.
Diese Arbeitsweise sollte immer beachtet werden, da ein
Unverständnis Anlaß zu Fehlern geben kann. Betrachten wir dazu die
Makrodefinition #define G(x) 4*x+3. Der Unterschied zu dem Makro
aus Listing 1 besteht nur in den fehlenden Klammern. Das Makro
kann nun in folgenden Formen aufgerufen werden: y=G(b)*2 oder
y=2*G(b). Beides sollte dasselbe Resultat liefern. Sehen wir uns
also an, was der Präprozessor daraus macht.
Zu beachten!!
Im ersten Fall entsteht die Anweisung y=4*b+3*2, im zweiten Fall
y=2*4*b+3. Die Ausdrücke sind nicht identisch, wie man sofort
sieht. Bei Benutzung der äußeren Klammer wäre das nicht passiert.
Die dann entstehenden Anweisungen y=(4*b+3)*2 und y=2*(4*b+3)
würden für jeden Wert von b dasselbe Ergebnis liefern. Und dieses
Ergebnis wäre wohl auch das gewünschte.
Doch auch die innere Klammer ist wichtig. Das sieht man an den
Aufrufen y=G(b+5) und y=G(5+b). Wieder sollte dasselbe Resultat
erscheinen. Im ersten Fall liefert der Präprozessor jedoch
y=4*b+5+3, im zweiten Fall y=4*5+b+3, wieder sind beide nicht
identisch. Wir merken uns also: Bei Definition von Makros wird
jeder Parameter und der Gesamtausdruck in Klammern gesetzt.
Ein oft benutztes Strukturmittel bei Makros ist der bedingte
Ausdruck. Dies ist ein Ausdruck, der in Abhängigkeit einer
Bedingung einen von zwei möglichen Werten erhält. Wollen wir z.B.
der Variablen y den Wert 0 zuweisen, falls x<=0 ist, und 1, falls
x>0 ist, so könnte die Zuweisung unter Benutzung eines bedingten
Ausdrucks die Form y=(x<=0?0:1); haben.
Dabei wird zunächst die Bedingung x<=0 ausgewertet. Falls diese
wahr ist, wird der Ausdruck zwischen dem Fragezeichen und dem
Doppelpunkt ausgewertet und an y zugewiesen. Im anderen Fall
erhält y den Wert des Ausdrucks nach dem Doppelpunkt. Die
Definition eines Makros mit einem bedingten Ausdruck würde dann so
aussehen: #define H(x) ((x)<=0?0:1).
Einfach wird nun ein Makro für die Berechnung des Maximums zweier
Zahlen: #define MAX(x,y) ((x)<(y)?(y):(x)). Ganz klar: Ist (x)
kleiner als (y), ist (y) das Maximum, ansonsten (x). Es gibt hier
allerdings einen versteckten Fallstrick, der nicht einfach durch
Klammern beseitigt werden kann. Was passiert bei dem Aufruf
MAX(a++,b++) ? (Für Nicht-C-Experten sei erklärt: Der Inkrement-
Operator ++ erhöht den Wert der vorstehenden Variablen nach der
Benutzung ihres Werts um 1.)
Und noch ein Fallstrick
Der Ausdruck wird ja zu ((a++)<(b++)?(b++):(a++)) expandiert. Je
nach Wert von a und b wird eine von beiden Variablen zweimal
bewertet. Durch die Inkrement-Operatoren wird also entweder a oder
b zweimal inkrementiert, die andere Variable nur einmal. Ähnlich
verhält sich #define SQUARE(x) ((x)*(x)). Hier wird x auf jeden
Fall zweimal bewertet. Der Aufruf SQUARE(b+=10) ergibt dann wieder
Probleme. Für Funktionsaufrufe in Makro-Argumenten gilt dasselbe,
da diese Funktionen ja statische Variablen enthalten können, deren
Wert durch jeden Aufruf geändert wird.
Wer programmiert denn überhaupt so einen Unsinn, könnte man
fragen. Nun, erlaubt ist es in C jedenfalls. Und dasselbe als
Funktion programmiert würde zu keinerlei Schwierigkeiten führen.
Wie bereits erwähnt, kann man derlei Probleme bei Makros nicht
umgehen. Hier zeigt es sich vorteilhaft, daß man Makros durch
Großschreibung deutlich von Funktionen abhebt. Denn dadurch zeigt
sich deutlich: Hier handelt es sich um einen Makro-Aufruf, der
keine Zuweisungs-, Inkrement- oder Dekrementoperatoren enthalten
sollte.
Makros für alle Typen
Jetzt wollen wir aber wieder einen Vorteil von Makros gegenüber
Funktionen erwähnen. Nehmen wir dazu unser eben definiertes Makro
MAX. Es ist für zwei Parameter erklärt. Über den Datentyp dieser
Parameter ist aber keine Aussage getroffen. Und in der Tat, MAX
kann gleichermaßen für Integer-, Real-, Short- oder Long-Werte
benutzt werden, ganz beliebig. Bedingung ist nur, daß der
Vergleichsoperator < für die Parametertypen zulässig ist. MAX muß
also nur einmal definiert werden. Eine Funktion max hingegen würde
eine neue Definition für jeden einzelnen Datentyp benötigen.
Zusätzlich müßte für jeden Typ auch noch ein eigener Name vergeben
werden.
Makros können natürlich auch außerhalb von arithmetischen
Ausdrücken benutzt werden. #define PRINTI(x) printf("%d",x)
definiert ein Makro für die dezimale Ausgabe einer Integer-Zahl.
Und schließlich, da ja nur Textersatz geleistet wird, kann ein
Makro auch ganze Anweisungsfolgen enthalten. So wird durch das
Makro #define XTOY(x,y) y=x;x=0 der Wert von x in y gespeichert
und x auf 0 gesetzt.
Und nochmal zu beachten!!
Die Definition von XTOY ist allerdings sehr unschön. Was passiert,
wenn es in einer if-Anweisung aufgerufen wird, z.B. if (y==0)
XTOY(x,y); ? Der Präprozessor macht daraus if (y==0) y=x; x=0; .
Das bedeutet, falls y null ist, wird x nach y gespeichert, aber
auf jeden Fall x=0 gesetzt. Wieder fehlen also Klammern, diesmal
geschweifte. Durch #define XTOY(x,y) {y=x;x=0;} wird durch das
Makro ein eigener Block definiert. Damit funktioniert unsere if-
Anweisung wie gewünscht.
In jedem Block dürfen bekanntermaßen neue lokale Variablen
definiert werden, also auch in einem Makro. Dies können wir uns
zunutze machen, um die Werte von zwei Variablen zu vertauschen.
Denn das Makro #define SWAP(x,y) {double h;h=x;x=y;y=h;} definiert
zunächst eine Hilfsvariable h, die zum Vertauschen benötigt wird.
Hier wird der Compiler jedoch je nach Datentyp der Argumente
Warnungen ausgeben, da dann Typenumwandlungen durchgeführt werden
müssen.
Sollen größere Blöcke als Makro definiert werden, kann der Platz
in einer Zeile knapp werden. Der Backslash \ teilt dann dem
Präprozessor mit, daß die Definition in der nächsten Zeile
weitergeht. Inwieweit es dann allerdings noch sinnvoll ist, den
Block als Makro und nicht als Funktion zu definieren, ist
fraglich.
Im Einzelfall muß somit jeder selbst abwägen, ob eine Definition
als Makro oder als Funktion größere Vorteile bringt. Hier sind Vor-
und Nachteile beider Methoden noch einmal zusammengestellt:
Kriterium | Makros | Funktionen |
Speicherbedarf | größer | kleiner |
Ausführungszeit | schneller | langsamer |
Zuweisungen, Inkrement, Dekrement, Funktionsaufruf | problematisch | unproblematisch |
Definition für unterschiedliche Datentypen | möglich | bedingt möglich |
Definition lokaler Variabler | möglich | möglich |
Nützliche Makros
Und hier eine Sammlung von nützlichen Makros für verschiedene Zwecke:
#define ABS(x) ((x)<0?-(x):(x))
#define MAKEPOS(x) (x=ABS(x))
#define SGN(x) ((x)==0?0:((x)>0?1:-1))
#define MAX(x,y) ((x)<(y)?(y):(x))
#define MIN(x,y) ((x)<(y)?(x):(y))
#define SETBIT(x,n) ((x)|=1L<<(n))
#define CLEARBIT(x,n) ((x)&=~(1L<<(n)))
#define GETBIT(x,n) ((x)&1L<<(n))
#define ODD(x) ((x)&1)
#define EVEN(x) (!((x)&1))
|
Diese beginnt mit einem Makro zur Berechnung des Absolutbetrags,
das durch einen bedingten Ausdruck realisiert ist. Durch MAKEPOS
wird einer Variablen ihr Absolutwert gleich zugewiesen, sie wird
also positiv gemacht. Das Vorzeichen wird durch SGN erhalten.
Hierbei handelt es sich um einen verschachtelten bedingten
Ausdruck. In allen Fällen wird das Argument mehrfach bewertet, so
daß Vorsicht geboten ist. Gleiches gilt auch für MAX und MIN. MAX
wurde ja bereits angesprochen.
Es geht weiter mit Bitoperationen. SETBIT(x,n) setzt in x das n-te
Bit, CLEARBIT(x,n) löscht es. Durch GETBIT(x,n) erfährt man, ob
das n-te Bit gesetzt ist oder nicht. Wie funktioniert das? Nun, es
wird immer eine Maske erzeugt, in der nur das n-te Bit gesetzt
ist, alle anderen Bits sind null. Dies geschieht einfach durch
Links-Shiften eines Bits um n Stellen. Eine bitweise Oder-
Verknüpfung setzt nun das betreffende Bit, das kann daher für
SETBIT verwendet werden.
Eine bitweise Und-Verknüpfung löscht alle Bits außer dem einen,
was somit für GETBIT nützlich ist. Ist das fragliche Bit nicht
gesetzt, ist das Resultat natürlich 0. Ist es hingegen gesetzt,
kommt nicht 1 heraus, sondern der Wert von 1L<<(n). Das macht aber
nichts, da GETBIT nur einen Wahrheitswert liefern soll. Und alle
Werte außer 0 werden ja als TRUE interpretiert. Ein Einsatz in
einem logischen Ausdruck ist also problemlos möglich.
Für CLEARBIT muß die Maske invertiert werden. Durch eine bitweise
Und-Verknüpfung hiervon mit x erreichen wir das Gewünschte. In
allen Fällen wird übrigens die Long-Zahl 1L benutzt, damit die
benötigte Maske auch als Long-Zahl angelegt wird. Damit ist klar,
daß die Makros auch für Long-Zahlen funktionieren. Für normale
Int-Werte macht das nichts, dann wandelt der Compiler eben um.
Eine entsprechende Warnung muß man in Kauf nehmen.
Die letzten zwei Makros testen, ob das Argument ungerade oder
gerade ist. Dies ist jeweils am letzten Bit abzulesen. Ist es
gesetzt, ist die Zahl ungerade, ansonsten gerade. Dies hätte auch
durch GETBIT realisiert werden können. So wie hier angegeben, geht
es aber schneller, da keine Shiftoperation durchgeführt werden
muß. Daher muß auch nicht mit einer Long-Zahl gearbeitet werden.
Zu beachten ist bei allen Makros, daß der Typ des Ergebnisses
nicht immer klar ist, da ja auch der Typ der Argumente variabel
ist. Im Zweifelsfall sollte man daher immer eine Cast-Operation
voranstellen. Unbedingt nötig ist dies, falls die Makros als
Argumente von printf auftreten.