Die Computerseite von Eckart Winkler
Makros in C

 

Dieser Artikel ist im DMV-Verlag in der Zeitschrift "toolbox", Ausgabe 11/1989 erschienen.
 



Funktionsaufrufe und Makros

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:

KriteriumMakrosFunktionen
Speicherbedarfgrößerkleiner
Ausführungszeitschnellerlangsamer
Zuweisungen, Inkrement, Dekrement, Funktionsaufrufproblematischunproblematisch
Definition für unterschiedliche Datentypenmöglichbedingt möglich
Definition lokaler Variablermöglichmö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.

 

Übersicht Programmiersprache C Index