Das ATOS-Magazin
 
  zurück zum News-Archiv
Anfang zurück vorwärts Ende 

ANSI C - Ein Programmierkurs - Teil VI

Auflösung der Beispiele für Bitoperatoren

Um das Ergebnis zu berechnen, schreiben wir die Konstanten als Binärzahl. i1 hat den Wert 10, binär 1010. Hier sollte noch kein Problem sein, da es nur eine einfache Zuweisung ist. Die Konstante 0x00FF lautet binär 0000000011111111. Damit ergibt die Verknüpfung:

1010 & 0000000011111111 = 1010 = 10

Das bitweise und kann also benutzt werden, um bestimmte Bits einer Variable so stehen zu lassen, wie sie gesetzt sind und um andere zu löschen, d. h. zu maskieren.

Anschließend verodern wir den Inhalt von i1, der nach der Zuweisung immer noch 10 ist, mit der Konstante 172

i3 = i1 | 172 = 10 | 172 = 1010 | 10101100 = 10101110 = 174

Im Ergebnis werden die Bits gesetzt, die in i3 oder i1 gesetzt sind. Wenn die Schnittmenge leer ist, entspricht dies einer Addition.

Jetzt werden alle Bits negiert und an i4 zugewiesen.

i4 = ~i3 = ~ 182 = ~ 11001010 = 00110101 = 110101 = 32+16+4+1 = 53

Haben Sie auch dieses Ergebnis errechnet? Haben Sie sich gewundert, dass das Programm zur Überprüfung ein anderes Ergebnis lieferte? Wenn i4 vom Datentyp int ist, belegt er 16 bzw. 32 Bit (je nach Compiler). Wir müssen natürlich für das Invertieren sämtliche Bits betrachten. Dies bedeutet für Turbo C i4 = ~i3 = 1111111100110101. Wenn i4 eine vorzeichenbehaftete Zahl ist, dann ist die Zahl negativ, wenn das höchste Bit gesetzt wird. Wie im Kapitel über Zahlensysteme ausführlich erklärt, wird im Zweierkomplemment die höchste Potenz negativ gezählt. Das ergibt für i4 = -215 + 214 + ... +22 + 20 = -175.

Und zum Schluss noch exklusiv oder von i3 mit dem Wert 174 und i2 mit 10.

i5 = i3 ^ i2 = 10101110 ^ 1010 = 10100100 = 164

Die Schiebebefehle verknüpfen 2 Operanden

i6 = i1 << 2;
i7 = i6 >> 2;

Dies können wir ganz einfach prüfen, indem wir uns wieder die Bits hinschreiben. Für die Zuweisung an i6 werden sämtliche Bits aus i1 um 2 Stellen nach links verschoben. Überzählige Stellen fallen links heraus, rechts werden Nullen nachgeschoben. Damit ist i6 = 101000 = 40 und wenn wir alles wieder zurückschieben und links Nullen nachfüllen, kommen wir wieder auf das ursprüngliche Ergebnis, da keine gesetzten Bits weggefallen sind. Wer sich nochmals das Binärsystem anschaut, wird erkennen, dass eine Verschiebung um ein Stelle nach links einer Multiplikation mit 2 entspricht.

Logische Operatoren

Die logischen Operatoren können beliebig mit Vergleichen und auch mit arithmetischen und Bitoperatoren verknüpft werden.

...
i1 = 1;
i2 = 10;
i3 = 27;
i4 = (i2 <= i3) && i1;

Der Wert von i1 ist ungleich Null und entspricht damit einem Wahr. Der Vergleich von i2 und i3 liefert einen Wert ungleich Null, da i2 kleiner als i3 ist. Das verknüpft mit einem logischen und mit i1 liefert einen Wert ungleich 0 als Ergebnis, was einem wahr entspricht.

Prioritäten

Vergleichen Sie einmal das Ergebnis der beiden folgenden Ausdrücke:

...
i1 = 12 * 3 + 5;
i2 = 12 + 5 * 3;

Das erste Beispiel sollte 41 ergeben, das zweite 27. Also hat wohl die Punktrechnung eine höhere Rangfolge als die Strichrechnung, genau wie man es in der Schule gelernt hat. Die Rangfolge der Operatoren ist im Kapitel Priorität der Operatoren aufgeführt. Wenn zwei Operatoren die gleiche Priorität haben, ist nicht definiert, in welcher Reihenfolge sie ausgeführt werden.

Dies kann zu Problemen führen, wenn sich ein Programm darauf verlässt. Nehmen wir als Beispiel eine Addition von 2 Funktionsaufrufen: x = f() + g(). Wenn sowohl f() als auch g() eine Variable ändern, von der das Ergebnis von f und g abhängt, kann eine andere Reihenfolge zu einem anderen Ergebnis von x führen.

Bei den logischen Operatoren AND und OR wird üblicherweise eine Auswertung von links her solange vorgenommen, bis das Ergebnis feststeht. Ein Beispiel, das zwar nicht unbedingt guter Programmierstil ist, aber die Gefahr dieser Auswertung verdeutlicht:

...
k = 10;
l = 12;
j = 3;
i = ( l < k) && (j = 10);

Welchen Wert hat j? Es ist 3, denn l ist größer als k. Damit steht der Wert des logischen AND schon fest. Ein falsch verknüpft mit irgendwas liefert immer falsch. Der zweite Ausdruck wird deshalb nicht mehr ausgewertet und damit bekommt j nicht den Wert 10 zugewiesen.

Als Fazit bleibt, dass man niemals so programmieren sollte, dass man sich darauf verlässt, dass bestimmte Teile eines Ausdrucks in einer bestimmten Reihenfolge ausgeführt werden! Solche Seiteneffekte können zu schwer zu findenden Fehlern führen.

Priorität der Operatoren

Die Operatoren haben folgende Priorität:

 Operator                       Auswertung von
 
 ()  []  .  ->                  links -> rechts
 ! ~ - ++ -- & * (type) sizeof  rechts -> links
 *  /  %                        links -> rechts
 +  -                           links -> rechts
 <<  >>                         links -> rechts
 <  <=  <  >=                   links -> rechts
 ==  !=                         links -> rechts
 &                              links -> rechts
 ^                              links -> rechts
 |                              links -> rechts
 &&                             links -> rechts
 ||                             links -> rechts
 ?:                             links -> rechts
 =  +=  -=  etc.                rechts -> links
 ,                              links -> rechts

Cookie-Jar

Der Cookie-Jar ist eine Tabelle. Jeder Eintrag besteht aus einem 32-Bit-Wert als Kennung des Cookies und einem 32-Bit-Wert als Wert des Cookies. Die Adresse des Cookies steht in der Speicheradresse $5A0. Enthält diese Adresse den Wert 0, gibt es auf diesem System keinen Cookie-Jar. Ab TOS 1.06 initialisiert das BIOS selbst den Cookie-Jar und trägt dort System-Cookies ein, die die Hardware beschreiben. Das Ende des Cookie-Jars wird durch einen Cookie mit dem Wert 0 angezeigt. Dieser Cookie enthält als Wert die Anzahl Cookies, die in den Cookie-Jar passen.

Für die Kennung des Cookies gilt, dass er druckbar sein und ein Kürzel ergeben sollte, das die Bedeutung des Cookies beschreibt. mit "_" beginnende Kennungen sind für ATARI reserviert.

Mehr über den Cookie-Jar kann im Profibuch nachgelesen werden. Leider ist dieses sehr gute und umfangreiche Nachschlagewerk nicht mehr neu erhältlich.

Weitere Möglichkeiten für Variablendeklarationen

Felder

Um viele gleichartige Variablen zusammenzufassen, gibt es die Felder bzw. Arrays. Felder werden deklariert, indem hinter dem Variablennamen in eckigen Klammern die Anzahl der gewünschten Elemente steht. Wenn in Ausdrücken auf ein einzelnes Element zugegriffen werden soll, schreibt man hinter dem Variablennamen in eckigen Klammern die Nummer bzw. den Feldindex. Der Feldindex läuft in C immer von 0 bis (Anzahl Elemente -1). Das erste Element hat also den Index 0, das letzte mit Index 1 eines weniger als das Feld Elemente hat. Wie sieht das nun aus?

...
int i1, i2, i3, iFeld[3];

iFeld[0] = 1;
iFeld[1] = 2;
iFeld[2] = 3;
i1 = iFeld[0];
i2 = iFeld[1] * 3;
i3 = 5 + iFeld[i1];

Dieses Beispiel deklariert ein Feld mit drei Elementen, die zuerst gesetzt und anschließend in Anweisungen benutzt werden. Als Feldindex wird in der letzten Zeile keine Konstante, sondern eine Variable benutzt. Ein Feldindex muss lediglich ganzzahlig sein. Es kann auch wiederum ein Ausdruck sein. Der Feldindex kann auch berechnet werden. Der Compiler übernimmt keine Überprüfung, ob der Feldindex im gültigen Bereich liegt! Dies ist bei Ausdrücken als Feldindex auch nicht mehr möglich! In dem Beispiel kann man also auf iFeld[3] zugreifen, ohne dass es eine Fehlermeldung des Compilers gibt. Zur Laufzeit erfolgt auch keine Überprüfung, da sie viel Zeit kostet und bei dynamisch allozierten Feldern nur schwer zu realisieren ist. Ein Schreibzugriff kann dabei Daten überschreiben, die anderweitig benutzt werden und zu Fehlfunktionen, im ungünstigsten Fall auch zum Absturz führen!

Felder lassen sich auch initialisieren. Dazu werden die Werte durch geschweifte Klammern zusammengefasst und durch Kommata getrennt:

...
int iFeld[3] = { 1, 2, 3 }

Es ist auch möglich, mehrdimensionale Felder zu deklarieren. Dazu wird für jede Dimension die Größe in eigene eckige Klammern geschrieben.

...
int iFeld[3][2] = { 1, 2, 3, 4, 5, 6 }, i;
int jFeld[3][2] = { {1, 2}, {3, 4}, {5, 6} }, i;

i = iFeld[2][1];

Anschaulich ist dies ein Feld mit drei Elementen, bei dem jedes Element wiederum ein Feld mit jeweils zwei Elementen vom Typ int ist. Deshalb werden in der Initialisierung die Werte für den gleichen ersten Index direkt hintereinander geschrieben. Das zweite Beispiel jFeld setzt zur Verdeutlichung noch zusätzliche geschweifte Klammern.

In C gibt es keinen speziellen Datentyp für Zeichenketten bzw. Strings. Eine Zeichenkette ist ein Feld mit Elementen vom Typ char. Das Ende der Zeichenkette wird durch ein Zeichen mit der Nummer 0 markiert. Dieses String-Ende kann auch als Zeichenkonstante '\0' dargestellt werden.

Es ist möglich, bei einer Initialisierung die Größe des Feldes wegzulassen. Zeichenketten können auch mit einer Zeichenkettenkonstante anstelle der Angabe der einzelnen Felder in geschweiften Klammern initialisiert werden.

#include <stdio.h>
#include <string.h>
 
int main()
{
   char str[] = "Hallo";
 
   printf("Länge der Zeichenkette: %ld Zeichen\n",strlen(str));
   printf("Größe des Feldes str  : %ld Zeichen\n",sizeof(str));
}

Die hier benutzten Funktionen werden später noch ausführlich erklärt. Die Funktion strlen liefert die Anzahl Zeichen des Strings. Das abschließende '\0' wird hierbei nicht mitgezählt. Deshalb ist das Ergebnis dieser Ausgabe 5. Da das String-Ende natürlich auch in dem Feld Platz finden muss, ist die zweite Ausgabe, die die Größe eines Objekts im Speicher liefert, 6. Dies wird wichtig, wenn der Speicher für eine Zeichenkette dynamisch alloziert wird! Beispiele dazu finden sich später.

Strukturen

Sollen unterschiedliche Datentypen zu einem zusammengefasst werden, bietet sich dafür eine Struktur bzw. struct an. Eine Variable, die ein struct ist, wird deklariert, indem das Schlüsselwort struct und anschließend in geschweiften Klammern die einzelnen Komponenten mit Datentyp und einem Namen angegeben werden.

Die Struktur kann auch einen Namen erhalten, der nach dem Schlüsselwort struct angegeben wird. Über diesen Namen können weitere Variablen deklariert werden, ohne dass der Aufbau der Struktur nochmals angegeben werden muss.

Auf eine Einzelne Komponente eines struct wird zugegriffen, indem dem Namen der Variable ein Punkt folgt, gefolgt vom Namen der gewünschten Komponente. Die einzelnen Bestandteile eines struct müssen nicht notwendigerweise verschiedene Datentypen sein. Wenn es für das Verständnis günstiger ist, über einen Namen und nicht über ein Feldindex zuzugreifen, wie z.B. bei komplexen Zahlen, bietet sich ein struct an.

int main()
{
   struct complex {
      float real;
      float imaginaer;
   };
   struct {
      int i;
      long j;
   } a;
   struct complex c;

   a.i = 10;
}

In dem Beispiel wird eine Variable a deklariert, die vom Datentyp struct ist. Als Beispiel für den Zugriff auf die Komponenten wird die Komponente i auf den Wert 10 gesetzt. Über die Variable a wird ein struct mit dem Namen complex deklariert. Hier wird aber mit der Strukturdeklaration keine Variable deklariert. Weiter unten wird eine Variable c deklariert, die vom Typ struct ist. Hier wird nur der Name der Struktur angegeben; der Aufbau der Struktur wurde schon oben beschrieben und muss nicht nochmals wiederholt werden. Ein struct kann auch mehr als 2 Komponenten wie in dem Beispiel haben.

Damit können wir zum angekündigten Beispiel kommen, dem Cookie-Jar. Eine kurze Erklärung, was der Cookie-Jars ist und wie er aufgebaut ist, findet sich im Kapitel Cookie-Jar. Da ein Cookie aus zwei Teilen besteht, die jeweils vier Byte belegen, bietet sich ein struct an. Als Komponenten sind sowohl Felder mit 4 char als auch long bzw. unsigned long möglich. Wegen der einfacheren Handhabung, z.B. Vergleiche, wählen wir unsigned long. Damit haben wir das erste Teilstück des Programms cookie-1.c, das wir in den weiteren Kapiteln ausbauen.

...
struct cookie {
   unsigned long name;
   unsigned long value;
};

Da im Cookie-Jar beide 4 Byte-Werte direkt hintereinander im Speicher stehen, wäre es nützlich, wenn die Struktur im Speicher genauso angeordnet ist. Diese Ausrichtung oder Alignment ist vom Compiler und Maschine abhängig und kann bei einigen Compilern im Quelltext mit Hilfe des Präprozessors eingestellt werden. Auf 68K-Prozessoren wird üblicherweise ein Alignment von 2 Bytes benutzt, um Variablen auf gerade Adressen zu legen. Bei TC ist das Alignment 1 Byte, wenn char kombiniert werden, andernfalls 2 Bytes, und kann nicht geändert werden. Das Alignment ist meines Wissens für die verschiedenen Compiler nicht dokumentiert.

enum

Mit einem enum bzw. Aufzählungsdatentyp lassen sich Konstanten für unterschiedliche Werte einer Variablen definieren. Eingeleitet wird eine Aufzählung mit dem Schlüsselwort enum, gefolgt von geschweiften Klammern und den möglichen Werten, getrennt durch Kommata, innerhalb der Klammern. Damit ähnelt das Beispiel demjenigen für Strukturen.

int main()
{
   enum ampel { ROT, GELB, GRUEN };
   enum { EINS, ZWEI, DREI } a;
   enum ampel c;

   a = EINS;
}

Hier wird eine Variable vom Typ enum deklariert, die damit den Wertbereich EINS, ZWEI und DREI hat. Die Variable a wird auf den Wert EINS gesetzt. Es ist genauso möglich, einen enum mit einem Namen zu deklarieren und diesen Namen später zu benutzen, ohne den Wertebereich nochmals aufzuführen.

Der ersten Konstante in einem enum ordnet der Compiler dem Wert 0 zu, die weiteren werden aufsteigend durchnumeriert. Damit haben in dem Beispiel die Konstanten folgende Werte:

0 ROT EINS
1 GELB ZWEI
2 GRUEN DREI

Es ist möglich, Konstanten in einem enum einen bestimmten Wert zuzuordnen. Die danach aufgeführten Konstanten werden aufsteigend ab dem angegebenen Wert nummeriert:

int main()
{
   enum { EINS=1, ZWEI, DREI } a;

   a = EINS;
}

Damit haben die Konstanten folgende Werte:

1 EINS
2 ZWEI
3 DREI

Es ist auch möglich, mehreren Konstanten den gleichen Wert zuzuweisen.

union

Wenn z.B. in Abhängigkeit einer anderen Information alternativ unterschiedliche Datentypen gespeichert werden sollen, bietet sich eine union an. Nach der Beschreibung von struct und enum können wir die Benutzung schon erraten. Auch hier lassen sich mehr als zwei Komponenten zusammenfassen.

int main()
{
   union test1 {
      char x;
      int y;
   };
   union {
      int Wert;
      char Feld[2];
   } x;
   union test1 y;

   x.Wert = 1;
}

Auf die unterschiedlichen Komponenten einer union wird genau wie bei einem struct über den Namen der Komponente zugegriffen. Allerdings belegen sämtliche Komponenten ein und denselben Speicherplatz. Der für die union angelegte Speicherplatz ist der Platz der größten Komponente, unabhängig davon, welche Komponente benutzt wird. Damit lässt sich nur eine Komponente zur gleichen Zeit sinnvoll nutzen. Alternativ lässt sich auch eine union benutzen, um auf den gleichen Speicher auf verschiedene Weise zuzugreifen. Da der Name eines Cookies in der Regel gemäß einer Empfehlung von Atari ein Kürzel darstellt, wäre es wünschenswert, auch auf die einzelnen Zeichen des Namens zuzugreifen. Damit lässt sich der struct für den cookie wie folgt erweitern:

...
struct cookie {
   union {
      unsigned long name_long;
      char name_array[4];
   } name;
   unsigned long value;
};

typedef

Es wäre schön, wenn Datentypen, die man mit den oben aufgeführten Methoden bildet, auch so benutzen kann, wie die direkt in C vorhandenen, d. h. wenn man also einfach den Namen eines Datentyps gefolgt von dem Variablennamen schreiben kann. Dazu gibt es die Möglichkeit, mit typedef einen Datentyp zu definieren. Vom Syntax sieht eine Typdefinition aus wie eine Deklaration einer Variablen, der wie eine Speicherklasse das Schlüsselwort typedef vorangestellt ist.

int main()
{
   typedef struct {
      int real;
      int imag;
   } complex;
   complex c;

   c.real = 10;
}

Dieses Beispiel definiert einen Datentyp mit dem Namen complex und darunter eine Variable vom Typ complex. Die Definition des Datentyps kann wie jede andere Deklaration auch außerhalb der Funktion stattfinden. Mit diesem Wissen können wir mit unserer Cookie-Struktur einen Datentyp definieren.

#include 'cookie-1.wml'

Bitfelder

Wenn eine Reihe von Informationen, die nur zwei Zustände besitzen (z.B. ein, aus), abgelegt werden soll, kann jede dieser Informationen in einem Bit gespeichert werden. Sämtliche Zustände können dann in einem long oder int zusammengefasst werden. Zum Beispiel kann ein Bild farbig sein, gepackt sein, ...

#define IS_FARBE  0x01
#define IS_PACKED 0x02

int flags = IS_FARBE | IS_PACKED;
int uncomp = farbe & ~IS_PACKED;

Die einzelnen Bits müssen nun allerdings mittels der Bitoperationen gesetzt und gelöscht werden. Einfacher ist es, ein Bitfeld zu definieren. Ein Bitfeld wird wie eine Struktur benutzt. Jede Komponente ist eine Menge von Bits und wird Bitfeld genannt. Jede Komponente muss deshalb vom Typ int sein, besser aber unsigned int, und erhält einen Doppelpunkt gefolgt von der Anzahl Bits. Für Zwischenräume können leere Bitfelder angelegt werden, die nur aus einem Doppelpunkt und der Anzahl Bits ohne Namen bestehen. Mit der Anzahl 0 kann ein Bitfeld auf eine Wortgrenze ausgerichtet werden. Benutzt werden die Bitfelder wie Strukturen.

struct {
   unsigned int is_farbe : 1;
   unsigned int is_packed : 1;
} flags;

flags.is_farbe = 1;

Leider ist die Reihenfolge der Bits und auch, ob ein Bitfeld eine Wortgrenze überschreiten kann, von der Implementierung abhängig. Damit sind Bitfelder zwar sehr praktisch, um externe Daten wie z.B. die Register von einem Controller zu beschreiben, leider sind solche Programme aber nicht mehr portabel.

Zeiger

Ein Zeiger oder Pointer ist die Adresse einer Variablen. Bisher hat der Compiler bei Variablen selbst entschieden, wo eine Variable im Speicher liegt. Mit dem Namen der Variablen hat man den Wert bekommen. Mit einem Pointer speichert man die Adresse, wo der Wert zu finden ist. Wie wird nun ein Zeiger deklariert? Dazu wird bei einer Deklaration vor den Namen der Variablen ein * gesetzt und anstelle einer Variablen eines bestimmten Datentyps hat man einen Zeiger auf diesen Typ deklariert.

...
int i,*ptri;

Dieses kleine Beispiel deklariert eine Variable vom Typ int mit dem Namen i und einen Zeiger auf einen int mit dem Namen ptri. Um auf den Wert, auf den der Zeiger zeigt, zuzugreifen, muss der Zeiger dereferenziert werden. Dazu schreibt man wiederum einen * vor die Variable, wenn man sie in einem Ausdruck benutzt.

...
i = *ptri;

Nur, wo liegt denn jetzt eigentlich der Speicher, auf den unser Pointer zeigt? Hierfür ist der Programmierer selbst verantwortlich! Hieraus ergibt sich die Mächtigkeit, aber auch die Fehlerträchtigkeit von Pointern. Deshalb werden wir uns auch genug Zeit lassen und mit Hilfe von Beispielen die Arbeit mit Pointern und das Verhalten anschaulich darstellen.

Eine Möglichkeit ist es, sich die Adresse einer Variablen mit dem Adreßoperator & geben zu lassen. Dazu folgt ein kleines Beispielprogramm:

#include <stdio.h>
 
int main(void)
{
   int i,j,p;

   i = 3
   p = &i;
   j = *p;
   printf("i = %d, j = %d\n",i,j);
}

Dieses Beispiel setzt i auf den Wert 3. Dann wird p auf den Wert der Adresse von i gesetzt. Anschließend wird der Wert, auf den p zeigt, j zugewiesen. Danach hat j den Wert 3, denn der Wert, auf den p zeigt, ist natürlich i.

Damit das noch anschaulicher wird, malen wir uns einfach mal einen möglichen Speicher auf und tragen die entsprechenden Werte ein:

 +-----+-----+-----+-----+-----+
 I     I     I     I     I     I
 +-----+-----+-----+-----+-----+
    0     1     2     3     4

Der Compiler legt jetzt in diesen einfachen Speicher unsere drei Variablen an. Wir machen es uns jetzt einfach und ordnen jede Variable einer Zelle zu:

 +-----+-----+-----+-----+-----+
 |     |     |     |     |     |
 +-----+-----+-----+-----+-----+
    0     1     2     3     4
    i     j     p

Jetzt schreiben wir den Wert 3 in die Variable i:

 +-----+-----+-----+-----+-----+
 |  3  |     |     |     |     |
 +-----+-----+-----+-----+-----+
    0     1     2     3     4
    i     j     p

Nun wird in die Variable p die Adresse von i geschrieben. Wir sehen, dass i in der Zelle 0 abgespeichert ist. Also hat p den Wert 0:

 +-----+-----+-----+-----+-----+
 |  3  |     |  0  |     |     |
 +-----+-----+-----+-----+-----+
    0     1     2     3     4
    i     j     p

Nun schreiben wir den Wert, auf den p zeigt, in die Variable j. Wir sehen, dass p auf die Adresse 0 zeigt, also die Adresse von i. Und was dort steht, wird j zugewiesen. Auf der Adresse 0 steht 3, wir haben ja die 3 der Variablen i zugewiesen. Damit bekommen wir:

 +-----+-----+-----+-----+-----+
 |  3  |  3  |  0  |     |     |
 +-----+-----+-----+-----+-----+
    0     1     2     3     4
    i     j     p

In vielen Beispielen wird durch Pfeile gezeigt, auf welche Variable ein Pointer zeigt. Das könnte dann folgendermaßen in unser Beispiel integriert werden:

 +-----+-----+-----+-----+-----+
 |  3  |  3  |  0  |     |     |
 +-----+-----+-----+-----+-----+
    0     1     2     3     4
    i <+  j     p
       |        |
       +--------+

Eine weitere Möglichkeit, den Speicher für einen Zeiger zu verwalten, ist die Verwendung der Funktionen malloc, free, ... um sich damit einen Speicherbereich zuweisen zu lassen. Beispiele hierzu folgen später.

Zeiger und Felder

Zeiger und Felder sind in C verwandt und beide Darstellungsarten können zum Teil alternativ benutzt werden. Der Name eines Feldes ohne die eckigen Klammern entspricht der Anfangsadresse eines Feldes und kann einem Zeiger auf den Datentyp der Feldelemente zugewiesen werden. An den Namen des Feldes kann natürlich keine Zuweisung erfolgen. Durch die die Deklaration des Feldes ist der dazugehörige Speicher angelegt worden und kann deshalb nicht mehr verlegt werden.

...
int *x;   /* Zeiger auf einen int */
int f[3]; /* Feld mit 3 int */
x = f;    /* x zeigt jetzt auf das erste Element von f */
f = x;    /* verboten !! */

Um den Pointer auf das nächste Feldelement zu setzen, ist es nicht nötig, diese Adresse dem Pointer zuzuweisen. Auch mit Pointern ist Arithmetik möglich. Wird ein Wert zu einem Pointer addiert oder von einem Pointer subtrahiert, so wird der Pointer tatsächlich um den Wert multipliziert mit der Größe des Datentyps verändert. Wird in obigen Beispiel x++; als nächste Anweisung geschrieben, so zeigt x auf den nächsten int, also auf f[1] und nicht auf das nächste Byte im Speicher.

...
int *x;   /* Zeiger auf einen int */
int f[3]; /* Feld mit 3 int */

x = f;    /* x zeigt auf f[0] */
x++;      /* x zeigt auf f[1] */
x++;      /* x zeigt auf f[2] */
x++;      /* x zeigt hinter f!! */

Da keine Überprüfung stattfinden kann, ist es auch möglich, den Zeiger auf Speicher zeigen zu lassen, der von uns nicht in irgendeiner Form reserviert wurde. Damit werden möglicherweise Variablen überschrieben, die noch anderweitig benötigt werden. Ein Absturz des Programms kann die Folge sein.

Es lässt sich auch mit der Pointerschreibweise auf ein Feld zugreifen.

...
int i,f[[3];

i = *f;
i = *(f+1);  /* entspricht i = f[1]; */

Und die Feldschreibweise kann auf Pointer angewandt werden.

...
int i,*p;

i = f[0];
i = p[1];

Zeiger und Strukturen

Für die Verwendung von Zeigern auf Strukturen existiert noch eine Ersatzdarstellung für den Zugriff auf Komponenten. Mit dem bisherigen Wissen würde man den Zeiger mit dem * dereferenzieren und anschließend mit dem Punkt auf die Komponente zugreifen.

...
struct {
   x:int;
   y:int;
   z:int;
} *pImag;
int i;

i = (*pImag).x;

Da der Punkt für den Zugriff auf die Komponenten eine höhere Priorität hat, muss die Dereferenzierung geklammert werden. Diesen Zugriff kann man mit einem Pfeil -> abkürzen. Damit sieht das Beispiel wie folgt aus:

...
struct {
   x:int;
   y:int;
   z:int;
} *pImag;
int i;

i = pImag->x;

Achtung: Da das Beispiel nur das Prinzip zeigt, ist hier nicht dafür gesorgt worden, dass der Zeiger auf einen definierten Speicherbereich zeigt.

Zeiger auf Zeiger

Es ist möglich, Zeiger auf jeden Datentyp zu deklarieren. Also auch auf einen Zeiger, was vielleicht auf den ersten Blick etwas verwirrend ist. Notwendig ist dies aber z.B. schon beim Zugriff auf den Cookie-Jar. Hier existiert eine Systemvariable, die den Zeiger auf den Cookie-Jar enthält. Von dieser Systemvariable ist die Adresse bekannt. Wenn wir dafür eine Variable anlegen wollen, benötigen wir einen Zeiger, der auf einen Zeiger zeigt, der auf den Cookie-Jar zeigt. Genau das macht das folgende Beispiel. Was die ersten Zeilen mit dem Lattenkreuz bzw. Hash nun genau bedeuten, wird im Kapitel über den Preprozessor erklärt. Sie dienen dazu, das Programm an Unterschiede zwischen den verschiedenen Compilern anzupassen. Der Zugriff auf das Betriebssystem gehört nicht zu den genormten Teilen eines ANSI-Compilers.

#ifdef __TURBOC__
#include <tos.h>
#else
#ifdef __GNUC__
#include <osbind.h>
#else
#include <tosbind.h>
#endif
#endif


typedef struct cookie_entry {
   union {
      unsigned long name_long;
      char name_array[4];
   } name;
   unsigned long value;
} CookieEntry;
 
int main(void)
{  CookieEntry **CookieJarPtr, *CookieJar;
   long OldStack;

   OldStack=Super(0L);
   CookieJarPtr = (CookieEntry**)0x5a0L;
   CookieJar=*CookieJarPtr;
   Super((void *)OldStack);
   return 0;
}

Um auf die Systemvariable zuzugreifen, wurde ein Zeiger CookieJarPtr angelegt, der auf die Adresse 0x5a0 gesetzt wird, wo der Zeiger auf den Cookie-Jar steht. Anschließend wird diese Adresse ausgelesen, also auf den Wert zugegriffen, auf den der Pointer zeigt, um den Zeiger auf den Cookie-Jar zu erhalten. Da auf diese Systemvariable nur im Supervisormodus zugegriffen werden kann, sind die beiden Anweisungen in die Aufrufe der Betriebssystemfunktion Super geklammert. Eine Beschreibung dieser Funktionen ist allerdings nicht Bestandteil dieses Kurses.

Die Zeiger kann man natürlich auch grafisch darstellen:

 +------------+     +----------+     +-----++-----+
 |CookieJarPtr|---->|_p_cookies|---->|     ||     |
 +------------+     +----------+     |name ||name |
                       0x5a0         |     ||     |
                     +---------+     |value||value|
                     |CookieJar|---->|     ||     |
                     +---------+     +-----++-----+
 
                                Cookie

Der Zeiger CookieJarPtrd auf eine Adresse gesetzt, die einen Zeiger auf den Cookie-Jar enthält. Dies ist die Systemvariable _p_cookies. Der Zeiger CookieJar wird auf den gleichen Wert gesetzt und beide zeigen nun auf den ersten Cookie. In dem Cookie-Jar stehen die Cookies direkt hintereinander. Der Cookie-Jar ist also ein Feld bzw. array. Allerdings ist seine Größe nicht vorgegeben.

Die nächste Folge beschäftigt sich mit Kontrollstrukturen.

Michael Bernstein


Anfang zurück vorwärts Ende  Seitenanfang

Copyright und alle Rechte beim ATOS-Magazin. Nachdruck und Veröffentlichung von Inhalten nur mit schriftlicher Zustimmung der Redaktion.
Impressum - Rückmeldung via Mail oder Formular - Nachricht an webmaster