8.6 Attributangaben

Mit Attributangaben kann man Definitionen und Deklarationen näher spezifizieren.

extern sagt, daß die damit vereinbarten Namen nicht in dieser Quelltextdatei, sondern in einer anderen definiert werden. Die Vereinbarung ist damit nur eine Deklaration; sonst wäre sie eine Definition (falls es um die Deklaration von Variablen oder Konstanten geht; die Deklaration von Funktionen wird immer als extern behandelt, auch wenn es nicht explizit angegeben wird).

static hat zwei verschiedene Bedeutungen; je nachdem, ob es sich auf einen lokalen oder globalen Namen bezieht. Steht es außerhalb einer Funktionsdefinition, ist die Vereinbarung also global, dann darf der Linker einen als static bezeichneten Namen nicht nach außen an andere Dateien weitergeben; die damit definierten Namen sind also auf die eine Datei beschränkt, in der sie definiert werden. Für lokale Namen ist das ohnehin der Fall.

int          a, b;
static int   i, j;
vereinbart vier Variablen vom Typ int. Auf a und b kann man sowohl in dieser Datei zugreifen als auch mit:
extern     int a, b;
/* ... */
a = 25;
in anderen Dateien. i und j dagegen sind durch das Schlüsselwort static zur Geheimsache erklärt und von anderen Quelltexten aus nicht zugänglich. Dies wird auch häufig für Funktionen verwendet, die dann nur innerhalb der Datei aufrufbar sind, in der sie auch definiert werden.

Bei lokalen Vereinbarungen dagegen wirkt static anders: Normalerweise sind lokale Variablen nur gültig, solange der umgebende Block, also meist die umgebende Funktion, aktiv ist. Beim Verlassen des Blocks werden die Werte der Variablen ungültig und können beim nächsten Eintritt in den Block ganz andere Werte haben. Mit static wird dies verhindert. Damit vereinbarte Variablen behalten ihren Wert auch beim Verlassen der umgebenden Funktion und haben beim nächsten Aufruf den alten Wert. Bei rekursiv aufgerufenen Funktionen kann das ein Schuß nach hinten sein: jede Aufrufebene der Funktion arbeitet mit derselben Variable, wenn sie als static vereinbart ist! Sonst erhält jede Aufrufebene einer Funktion einen eigenen Satz von Variablen, so daß Änderungen einer Variable in einer Aufrufebene die Werte in den anderen Ebenen nicht beeinflussen. Bei static-Variablen ist das nicht der Fall; hier gibt es die betreffende Variable nur einmal für alle Aufrufebenen. Siehe dazu auch Rekursion.

Das Gegenteil von static ist das Schlüsselwort auto. Dieser Zusatz sagt dem Compiler, daß die so vereinbarte Variable nur lokal und temporär, also nicht static, im Speicher gehalten werden soll (automatische Variable). Für statische globale Variablen ist dieser Zusatz nicht zulässig, und bei lokalen Variablen ohnehin Standard. Den Zusatz auto kann man also auch immer genauso gut weglassen.

register ist ein Hinweis des Programmierers an den Compiler, daß eine Variable besser in einem Register aufgehoben werden sollte als im normalen Arbeitsspeicher. Der Zugriff auf Register ist bei den meisten Rechnern erheblich schneller als auf normale Variablen. Der Compiler darf den Hinweis auch ignorieren. Da die Anzahl an Registern in der Regel sehr begrenzt ist (keine bis maximal ein Dutzend Variablen kann man in Registern unterbringen), muß der Compiler ja die Möglichkeit haben, überzählige Variablen anderswo unterzubringen. Beispielsweise braucht man gar keine Felder in Registern unterbringen; dafür ist nie Platz. Zudem darf man auf Registervariablen nicht mit dem Adreßoperator & zugreifen, da sie ja eventuell gar keine Adresse im Speicher haben. Lohnen kann sich register höchstens bei Schleifenzählern oder Variablen, die in kleinen Schleifen viel geändert werden. Gute Compiler erkennen das aber oft auch schon selbst.

Beispiel:

register int   i;
register long  l;
    /* ... */
    while( --i ) l += i;
Abgesehen vom nicht mehr zulässigen Adreßoperator (&) ändert sich durch register nichts am Programm; wenn man Glück hat, läuft es dann aber schneller12.

Die drei Speicherklassen static, auto und register schließen sich gegenseitig aus.

Mit const sagt man dem Compiler, daß man eine Variable nicht vom Programm aus ändern will. Die einzige legale Möglichkeit, einer const-Variablen einen Wert zu geben, ist die Initialisierung13. Macht man an eine solche Variable trotzdem eine Zuweisung, dann erkennt der Compiler dies (hoffentlich) und gibt eine Fehlermeldung aus. Dies ist als zusätzliche Fehlerkontrolle oft sinnvoll. Formal sind so vereinbarte Variablen aber immer noch Variablen! Siehe dazu auch volatile im nächsten Abschnitt.

Mit

const    int zwanzig = 20;
kann man die Variable zwanzig nur lesend verwenden, aber eine Zuweisung wie etwa:
   zwanzig = 25;
ist dann nicht möglich.

Mit

const char *p = "abc123";
bezieht sich const nicht auf den Zeiger p, sondern besagt, daß das, worauf p zeigt, nicht geändert werden darf.

Danach ist also

p++;
zulässig; nicht aber
*p = 'e';

Will man einen Zeiger selbst als const erklären, dann muß man ihn so vereinbaren:

char   const *p = "einszweidrei";
Dann ist der Zeiger selbst konstant.

Beides gleichzeitig ist natürlich auch erlaubt (aber recht selten):

const char const *p = "appel und en ei";
Damit ist der Zeiger p konstant und darf nicht geändert werden; ebenso das worauf er zeigt.

In gewisser Hinsicht das Gegenteil von const ist volatile. Damit gibt man dem Compiler an, daß sich ein Wert eventuell ändern kann, ohne daß das Programm darauf Einfluß hat. Dies ist bei Systemvariablen des Rechners manchmal der Fall. Wenn man dem Compiler dann keinen Hinweis darauf geben kann, ist er eventuell zu schlau. Angenommen, an der Adresse 456 soll bei unserem Rechner in irgendeiner Form die Systemzeit hinterlegt sein und vom Rechner selbst laufend angepaßt werden. Dann kann man so einen Zeiger auf diese Adresse richten und verwenden:

unsigned long    *systemzeit_p = (unsigned long *)456;
long              i;
unsigned long     wert;
    /* ... */
    for( i=0; i<500000; i++ )
        {
            wert = *systemzeit_p;
            /* ... */
        }

Der Compiler könnte jetzt auf die Idee kommen, daß in der for-Schleife ja ohnehin immer der gleiche Wert an wert zugewiesen wird und das Programm so umbauen:

unsigned long    *systemzeit_p = (unsigned long *)456;
long              i;
unsigned long     wert;
    /* ... */
    wert = *systemzeit_p;
    for( i=0; i<500000; i++ )
        {
            /* ... */
        }
Dies ist eigentlich eine hervorragende Idee und wirklich gute Compiler machen das auch, da dann die Zuweisung anstatt 500000 mal in der Schleife nur einmal vor der Schleife ausgeführt wird. Der Compiler kann ja schließlich nicht wissen, daß der Wert, auf den systemzeit_p zeigt, laufend vom Rechner selbst geändert wird. Dadurch ist die eigentlich gut gemeinte Optimierung ein Eigentor.

Diesen Effekt kann man verhindern, indem man alle Werte, die von außerhalb des eigenen Programms geändert werden können, als volatile vereinbart:

volatile unsigned long  *systemzeit_p = (unsigned long *)456;
long                     i;
unsigned long            wert;
    /* ... */
    for( i=0; i<500000; i++ )
        {
            wert = *systemzeit_p;
            /* ... */
        }
So werden zu dreiste Programmoptimierungen durch den Compiler vermieden.

const und volatile können auch kombiniert werden: const volatile bedeutet, daß eine Variable vom Programm aus nicht geändert werden darf (const), wohl aber eventuell von außerhalb geändert werden kann (volatile). Damit ist es dem Programmierer untersagt, den Wert vom Programm aus zu ändern; aber der Compiler rechnet damit, daß dies von außerhalb geschehen kann.

AnyWare@Wachtler.de