#define

Konstanten mit #define

Mithilfe der Direktive #define lassen sich unter anderem konstante Werte definieren. Das sieht ungefähr so aus:

#include<stdio.h>
 
#define TEXT "Hallo"
#define E 2.718281
#define LANGUAGE 'C'
 
int main()
{
    printf("%s\n",TEXT);
    printf("Programmiersprache: %c\n",LANGUAGE);
    printf("Die Euler'sche Zahl lautet %f",E);
    return 0;
}

Durch #define lassen sich Konstanten von verschiedenen Datentypen definieren, ohne diese explizit zu nennen. In TEXT wird sogar ein binäres Nullzeichen(\0) für Strings eingefügt. Das liegt, wie bei den sonstigen Stringinitialiesierungen, an den doppelten Hochkommata. Die Konstante LANGUAGE gehört zu dem Typ char, wegen der einfachen Hochkommata und E ist vom Typ float, wegen der Dezimalstellen. Die Syntax wirkt ungewohnt. Es wird z.B. kein Gleichheitszeichen benutzt. Zur Initialisierung dient einfach nur ein Leerzeichen. Dass die Namen der Konstanten nur aus Großbuchstaben bestehen, liegt daran, dass es sich bei den meisten Programmierern so eingebürgert hat. Das soll vor allem helfen, Präprozessorkonstanten von gewöhnlichen Konstanten zu unterscheiden. Die Regeln der Namensgebung sind die selben, wie die der Variablen oder Funktionen.

#define vs. const

Eine ähnliche Möglichkeit haben wir bereits durch das Schlüsselwort const kennengelernt. Worin liegt denn der Unterschied?

Oft sieht man const in Funktionsparametern. Das macht Sinn, um die Übersicht der Funktion zu verbessern. So kann man schnell feststellen, dass der konstante Parameter nicht geändert wird und man kann sich auf die anderen Variablen und Parameter konzentrieren.

Ein weiterer Nachteil von #define ist, dass der eine oder andere Compiler nur den Wert 2.718281 und nicht den Namen E „kennt“. Gibt es einen Compilerfehler an der Stelle, an der der Wert E benutzt wird, wird der Compiler in Fehlermeldung nur 2.718281 ausgeben. Hat man ein großes Programm, welches viele Konstanten benutzt, so ist es schwer nachzuvollziehen, wo der Wert 2.718281 auftaucht. Das kann eine lange Fehlersuche zur Folge haben.

In diesem „Duell“ ist also const der klare Sieger. Die Möglichkeit der Präprozessorkonstanten ist veraltet und sollte nicht genutzt werden. Allerdings hat #define noch andere Verwendungszwecke, die hier erwähnt werden.

Makros

Die Anweisung #define hat auch noch andere Fähigkeiten. Man kann damit sogenannte Makros erstellen. Ein Makro ist eine Art Zusammenfassung von Anweisungen, die in einem Befehl ausgeführt werden. Ein Beispiel:

#include<stdio.h>
 
#define SQUARE(base) (base * base)
 
int main()
{
    long base;
    /* Eingabe von base */
    printf("%i zum Quadrat ist %i\n",base,SQUARE(base));
    return 0;
}

Diese Schreibweise ist auch möglich:

#include<stdio.h>
 
#define SQUARE(base) {\
    base*= base;\
    printf("%i",base);\
}
 
int main()
{
    long base;
    /* Eingabe von base */
    SQUARE(base);
    return 0;
}

Wie man sieht, lässt sich ein Makro ähnlich wie eine Funktion deklarieren. Diesen kann man auch Parameter übergeben. Es ist nur zu beachten, dass zwischen dem Namen des Makros und der Parameterliste kein Leerzeichen steht. Sonst würde der Compiler beim Aufruf von SQUARE(base) einen Fehler ausgeben, da er SQUARE(base) als Funktion ansieht, obwohl es in solch einem Fall nur eine Konstante wäre. Ist das Makro mehrzeilig, so muss am Ende jeder Zeile, außer in der letzten, ein Backslash (\) zur Verkettung der Ausdrücke geschrieben werden, da der Präprozessor ansonsten den Rest nicht mehr als Direktive anerkennt.
Dort, wo sich der Wert einer Konstanten befinden würde, steht jetzt ein Ausdruck.
Im ersten Beispiel ist es lediglich eine Multiplikation, im zweiten Beispiel kommt man syntaktisch einer Funktion näher. Genauer gesagt ist das zweite Beispiel eine Prozedur. Makros können keinen Rückgabewert abgeben.
Im Prinzip sind Makros auch Funktionen. Allerdings sind diese beiden Varianten bei genauerer Betrachtungsweise doch unterschiedlich. Funktionen werden beim Aufruf in einen bestimmten Speicherbereich des Hauptspeichers geladen, den Stack. Dort werden sie abgearbeitet und dann verworfen. Makros werden dagegen wie üblich vor dem Kompilieren in den Quelltext eingefügt.

Makrooperationen

Eine veraltete und äußerst selten genutzte Möglichkeit von #define ist die Verkettung von Makros. Der Vollständigkeit halber wird sie hier noch beschrieben. Zuvor noch die zwei Makrooperatoren: # und ##.

#include<stdio.h>
 
#define PRINTVAR(x) printf("Die Variable %s hat den Wert %i.",#x,x);
 
int main()
{
    short x=4;
    PRINTVAR(x);
    return 0;
}

Ausgegeben wird folgendes: „Die Variable x hat den Wert 4.“

Intern ersetzt der Präprozessor für den Aufruf von PRINTVAR folgendes ein:

    printf("Die Variable %s hat den Wert %i.","x",x);

Mit dem Operator # wird der Name der Variablen zu einer Zeichenkette umgewandelt und in diesem Fall ausgegeben. Wie man sieht, ist der Nutzen dieser Möglichkeit eher gering.

Es gibt noch die Möglichkeit, Werte mit einem Makro zu verketten:

#include <stdio.h>
 
#define LINK(hello, world) s1##s2
 
int main()
{
    char *hello="Hello_";
    char *world="World";
    printf("%s",LINK(hello,world);
}

Die Ausgabe lautet dann: Hello_World

Der Operator ## sorgt dafür, dass die beiden Werte miteinander verkettet werden. Diese Anwendung kommt aber noch seltener vor, als die erste.

Funktionen vs. Makros

Um die Vor- und Nachteile der beiden Möglichkeiten besser zu verstehen, ist es empfehlenswert zu wissen, wie ein Programm intern verarbeitet wird. Dazu folgende Kapitel:

Die Speicherlandschaft eines Prozesses
Der Stack - oder - Was ist ein Funktionsaufruf

Das Codesegment enthält die Anweisungen des gesamten Programms. Wird eine Funktion aufgerufen, so werden die Parameter, falls vorhanden, in den Stack geladen. Der Befehlszeiger (Instruction-Pointer=IP), also der Zeiger, welcher immer den nächsten auszuführenden Befehl liest, springt zu dem Bereich im Codesegment, in dem die jeweilige Funktion gespeichert ist. Lokale Variablen werden dann noch in den Stack abgespeichert, falls sie in der Funktion vorhanden sind.
Funktionen haben zum einen den Nachteil, dass für die Parameter Kopien der übergebenen Variablen erstellt werden. Das gilt auch für Referenzparameter, da diese Zeigervariablen sind. Hinzu kommen noch lokale Variablen. Das alles benötigt zusätzlichen Speicherplatz und in der Zeit, in der die Variablen in den Stack geladen werden, zusätzliche Rechenzeit. Allerdings kann dieses Problem durch globale Variablen gelöst werden. Dieses Vorgehen empfiehlt sich aber nur bei zeitkritischen Anwendungen, weil globale Variablen oft zu Problemen führen können, da jede Funktion diese verändern können und dadurch schnell die Übersicht verloren gehen kann. Außerdem reservieren sie während des gesamten Programmablaufs den globalen Namensbereich, wo hingegen lokale Variablen nach ihrer Nutzung zerstört werden. Ein weiteres „Problem“ von Funktionen ist, dass sie nur einmal im Codesegment vorhanden sind. Der Sprung zu der Position, in der eine Funktion abgespeichert ist, ist rechenintensiv. Jedoch ist dieses „Problem“ auch gleichzeitig ein großer Vorteil von Funktionen. Nicht nur dass sie die Übersicht des Quelltextes verbessern und Redundanzen vermeiden, sondern auch insofern, dass sie das Codesegment des Programms erheblich minimieren, wodurch sehr viel Speicherplatz frei bleibt.

Im Gegensatz dazu werden Inhalte von Makros wie üblich in den Quelltext eingefügt. Das hat zur Folge, dass das Codesegment wächst. Dadurch wird mehr Speicherplatz reserviert. Allerdings werden dadurch keine lokalen Variablen deklariert. Diese Ersparnis ist aber in den meisten Fällen absolut unerheblich, da ein längeres Codesegment in der Regel mehr Speicherplatz verbraucht, als die Anzahl der lokalen Variablen. Der große Vorteil von Makros ist, dass diese direkt nach der vorherigen Anweisung gespeichert sind, wodurch Sprünge vermieden werden können. Die große Schwäche der Makros ist, dass bei häufigem „Aufrufen“ bzw. bei häufigem Einsetzen durch den Präprozessor das Codesegment stark aufgebläht wird. Jedoch ist häufig der Nutzen von Makros äußerst gering und so kann es vorkommen, dass der Schuss nach hinten losgeht.

Fazit: Makros tragen zu einer höheren Verarbeitungsgeschwindigkeit bei, Funktionen dagegen zu einer sparsamen Nutzung des Arbeitsspeichers. Hierbei muss zwischen Effektivität und Effizienz entschieden werden, was nicht immer einfach ist. Makros sind bei zeitkritischen und vor allem kleinen Funktionen, die selten aufgerufen werden, von Vorteil. Allgemein sollten Funktionen die erste Wahl sein, da sie den Arbeitsspeicher erheblich entlasten. Der Geschwindigkeitsvorteil von Makros ist in vielen Fällen eher gering.

Hinweis:
In C++ sind Makros zwar noch gültig, sollten aber gar nicht mehr genutzt werden. Dort gibt es das Schlüsselwort 'inline', mit dem der Compiler wie der Präprozessor den Inhalt einfach einfügt.