8. Zeiger
Zeiger gehören eigentlich zu den elementaren Datentypen.
Die bisher vorgestellten Datentypen ermöglichen es, unabhängig von Speicheradressen
zu arbeiten. Es wurde nur mit dem Namen der Variablen gearbeitet und nur das Programm selber
wusste, welche Speicheradressen sich hinter den Variablennamen verbergen.
Zu einer Variablen gehören folgende Dinge:
der Name (kann, muss aber nicht!),
die Speicheradresse, an der der Wert der Variablen gespeichert ist,
der Inhalt der Variable, also der Wert an der Speicheradresse und
der Datentyp, um den Inhalt zu interpretieren und um die Anzahl der Speicherzellen
zu bestimmen.
Beispiel:
Name | Adresse (hex.) | Inhalt | Datentyp | Interpretation |
Zahl1 | 00003D24h | 00000000 00001011 | short | 11 |
00003D25h | ||||
Zahl2 | 00003D26h | 00000000 00011110 | short | 30 |
00003D27h | ||||
Zeichen | 00003D28h | 01000010 | char | 'B' |
Die Adressen in diesem Beispiel sind frei erfunden; jede Ähnlichkeit mit vorhandenen
Speicheradressen ist rein zufällig! :-)
Jede Speicheradresse kann 1 Byte = 8 Bits aufnehmen. Die beiden Zahlen
im Beispiel sind short-Zahlen (16 Bit-Zahlen) und belegen daher 2
Speicheradressen. Dagegen hat die char-Variable nur 8 Bits und
belegt damit auch nur eine Speicheradresse.
8.1. Zeiger und Adressen
In C/C++ wird an vielen Stellen mit Zeigern (im engl. Pointer) gearbeitet, um
unabhängig von Variablennamen auf die Inhalte der Variablen zugreifen zu können.
Ein Zeiger "zeigt" auf eine Variable, d.h. ein Zeiger ist eine Zahlenvariable und beinhaltet
als Wert die Adresse der Variablen, auf die sie zeigt. Dadurch benötigen Zeiger immer den
gleichen Speicherplatz (z.B. 32 Bit unter einem 32Bit-Betriebssystem).
Bei der Deklaration/Definition von Zeigern wird der Datentyp angegeben, auf den der Zeiger zeigen
soll. Vor den Variablennamen von Zeigern wird ein Sternchen (*) gesetzt.
Beispiel:
int *ip;
Diese Zeile bewirkt, dass ein Zeiger mit dem Namen ip deklariert und definiert
wird. Dieser Zeiger zeigt jetzt grundsätzlich auf Variablen vom Typ int.
Diese Datentypangabe ist wichtig, da mit ihr nicht nur auf die angegebene Speicheradresse, sondern
auch noch auf die nächsten 3 zugegriffen wird (weil eine int-Zahl
32 Bits = 4 Bytes = 4 Speicheradressen beinhaltet). Im nächsten Beispiel wird mit
einem Zeiger auf eine int-Variable gezeigt.
Beispiel:
int i; /* Deklaration der int-Zahl i */
int *ip; /* Deklaration des Zeigers ip */
ip = &i; /* Speicheradresse von i in ip speichern */
i = 99;
*ip = 100; /* weist i die Zahl 100 zu, da ip auf i zeigt */
Der unäre Adressoperator & liefert die
Speicheradresse eines Objekts bzw. einer Variablen. Es wird in der Literatur manchmal auch
fälschlicherweise von einer Referenz auf das Objekt bzw. vom Referenzieren gesprochen.
Unter dem Begriff Referenz wird jedoch in C++ etwas anderes verstanden!
Im Beispiel wird also die Speicheradresse der Variable i dem Zeiger
ip zugewiesen. Danach wird der Variable i der Wert
99 zugewiesen. In der letzten Zeile wird dem Inhalt der Variablen, auf die der Zeiger
ip zeigt, der Wert 100 zugewiesen. Da der Zeiger auf die Variable
i zeigt, hat i anschließend den Wert 100. Dies
wird durch den unären Variablenoperator * erreicht.
Es wird auch fälschlicherweise vom Dereferenzieren (siehe oben) gesprochen. Im Speicher
sieht es nach dem Ablauf des Programms folgendermaßen aus (die Speicheradressen sind
wieder rein zufällig gewählt):
Name | Adresse (hex.) | Inhalt | Datentyp |
i | 00004711h | 100 | int |
00004712h | |||
00004713h | |||
00004714h | |||
ip | 00004715h | 00004711h | int* |
00004716h | |||
00004717h | |||
00004718h |
Hier noch einmal eine Tabelle mit verschiedenen Ausdrücken und die im Beispiel
resultierenden Werte:
Ausdruck | Ergebnis |
i | 100 |
&i | 00004711h |
ip | 00004711h |
*ip | 100 |
&ip | 00004715h |
Der Variablenoperator liefert aber nicht nur den Zugriff auf den Wert der Variable, auf die
er zeigt, sondern er liefert die komplette Variable (daher auch der Begriff Variablenoperator)!
D.h. es kann auch wieder auf die Adresse der Variablen zugegriffen werden, wie das folgende
Beispiel zeigt.
Beispiel:
kap08_01.c
01 #include <stdio.h>
02
03 int main()
04 {
05 int i = 5;
06 int *ip = &i;
07
08 printf(" ip = %p\n", ip); /* Liefert */
09 printf(" &*ip = %p\n", &*ip); /* immer */
10 printf(" *&ip = %p\n", *&ip); /* die */
11 printf("&*&*ip = %p\n", &*&*ip); /* gleiche */
12 printf("*&*&ip = %p\n", *&*&ip); /* Adresse! */
13
14 return 0;
15 }
Dabei ist zu sehen, dass sich Adressoperator und Variablenoperator aufheben, egal ob erst der
Adressoperator und dann der Variablenoperator steht oder umgekehrt; in allen Fällen wird
die gleiche Adresse - nämlich die Speicheradresse der Variablen i
- ausgegeben.
8.2. Der Zeigerwert NULL
Es gibt einen speziellen Zeigerwert, nämlich den Wert NULL
(definiert in der Headerdatei stdio.h). Dieser Zeigerwert zeigt nicht
irgendwo hin, sondern definitiv auf "nichts". Die meisten Compiler definieren den
NULL-Zeiger als einen Zeiger, der auf die Speicheradresse 00000000h
zeigt, also auf die allererste Speicheradresse im Arbeitsspeicher. Dieser Zeigerwert kann
abgefragt werden, z.B.
if (ip == NULL) ....
Genauso wie die "normalen" Variablen haben Zeiger nach der Definition einen unbekannten
Wert, d.h. sie "zeigen" irgendwo hin. Um dies zu vermeiden, sollten Zeiger immer mit dem
Zeigerwert NULL initialisiert werden.
8.3. Typprüfung
In C wird - im Gegensatz zu C++ und anderen Programmiersprachen - der Typ eines Zeigers
nicht geprüft. Es ist also möglich, einen
int-Zeiger plötzlich auf ein char zeigen zu
lassen.
Wird mal ein Zeiger benötigt, bei dem noch nicht feststeht, auf welchen Datentypen
er zeigen soll, muss ein Zeiger auf void verwendet werden.
Beispiel:
int *ip = NULL; /* Zeiger auf int */
char *cp = NULL; /* Zeiger auf char */
void *vp; /* Zeiger auf void */
vp = ip; /* korrekt */
vp = cp; /* korrekt */
/* umgekehrt: auch korrekt (aber nur in C!) */
ip = vp; /* nur in C korrekt */
cp = vp; /* nur in C korrekt */
In C++ können die umgekehrten Zuweisungen nur mit einer expliziten Typumwandlung
durchgeführt werden. Dies sollte aber nicht ohne wichtigen Grund gemacht werden, weil
damit die Typkontrolle des C++-Compilers umgangen wird und somit eine weitere Fehlerquelle
gegeben ist.
Beispiel:
int *ip = NULL; /* Zeiger auf int */
void *vp = NULL; /* Zeiger auf void */
ip = (int *) vp; /* jetzt auch in C++ korrekt! */
8.4. Zeiger-Arithmetik
Mit Zeiger-Arithmetik wird die Addition und Subtraktion von Zeigern mit
ganzen Zahlen bezeichnet. Auch lassen sich zwei Zeiger subtrahieren - vorausgesetzt, sie
zeigen auf den gleichen Datentypen.
Beispiel:
int i1 = 5, i2 = 7;
int *ip1 = &i1, *ip2 = &i2;
ip2 = ip1 + 1; /* erlaubt */
ip1 = ip2 - 1; /* erlaubt */
ip2 = 1 + ip1; /* erlaubt */
ip1 = 1 - ip2; /* Fehler! */
i1 = ip1 + ip2; /* Fehler! */
i2 = ip2 - ip1; /* erlaubt */
Alle anderen Operationen wie Multiplizieren usw. dürfen nicht auf Zeiger angewendet
werden.
Dabei wird bei der Zeiger-Arithmetik nicht in Bytes sondern in Anzahl von Elementen des
Datentyps, auf den der Zeiger zeigt, gerechnet. Wird also zu einem
int-Zeiger eine 1 addiert, wird nicht auf die nächste Speicheradresse,
sondern auf den nächsten int gezeigt.
Beispiel:
int i = 100;
int *ip1 = &i;
int *ip2 = ip1 + 1;
In diesem Beispiel zeigt der Zeiger ip2 nach der Initialisierung
(ip1 + 1; also 00004711h + 1) nicht auf die Adresse 00004712h sondern
ein Element (also ein int) weiter auf die Adresse 00004715h.
Name | Adresse (hex.) | Inhalt | Datentyp |
i | 00004711h | 100 | int |
00004712h | |||
00004713h | |||
00004714h | |||
ip1 | 00004715h | 00004711h | int* |
00004716h | |||
00004717h | |||
00004718h | |||
ip2 = ip1 + 1 | 00004719h | 00004715h | int* |
0000471ah | |||
0000471bh | |||
0000471ch |
Entsprechend liefert die Differenz von zwei Zeigern nicht die Anzahl der dazwischen liegenden
Bytes, sondern die Anzahl der dazwischen liegenden Datenelementen.
Bei einem void-Zeiger steht der Datentyp, auf den der Zeiger zeigt, nicht
fest. Es wird hier in Bytes gerechnet. Wenn also im obigen Beispiel die Zeiger ip1
und ip2 jeweils ein Zeiger auf void wäre, würde
ip2 auf die Adresse 00004712h zeigen.
void *ip1 = &i;
void *ip2 = ip1 + 1; /* zeigt so auf 00004712h! */
Hinweis:
Bei der Zeigerarithmetik muss darauf geachtet werden, dass der resultierende Zeiger auf eine
Adresse zeigt, die innerhalb des Programmsegments oder innerhalb eines reservierten
Speicherbereichs liegt. Ansonsten kommt es schnell zu einem Speicherzugriffsfehler!
8.5. Zeiger und Arrays
Zeiger und Arrays haben vieles gemeinsam. Tatsächlich setzen die meisten Compiler alle
Array-Befehle in Zeiger-Befehle um. Der Name eines Arrays (also der Variablenname) ohne eckigen
Klammern und Indexangaben gibt die Startadresse des Arrays zurück, d.h. die Adresse des
ersten Elements. Er ist damit wie ein Zeiger einsetzbar. Im Gegensatz zu einem richtigen
Zeiger kann ihm allerdings keine andere Adresse zugeordnet werden; er ist kein
L-Wert (siehe Kapitel Variablen Abschnitt L-Werte und R-Werte).
Beispiel:
int *IntZeiger = NULL; /* Zeiger auf int */
int IntArray[5]; /* Array von int */
IntZeiger = IntArray; /* Zeiger auf Array-Startadresse */
IntArray[0] = 5; /* ist identisch mit folgender Zeile */
*IntZeiger = 5;
Es kann aber nicht nur auf die Startadresse des Arrays mit Zeigern zugegriffen werden,
sondern auch auf jedes andere Element des Arrays. Dazu wird die sogenannte
Zeiger-Arithmetik verwendet. Der Begriff Zeiger-Arithmetik bedeutet
das Addieren und Subtrahieren von ganzen Zahlen zu bzw. von einem Zeiger unter
Berücksichtigung des Speicherbedarfs des Datentyps, auf den der Zeiger zeigt. Zum
besseren Verständnis wird das obige Beispiel erweitert.
Beispiel:
int *IntZeiger = NULL; /* Zeiger auf int */
int IntArray[5]; /* Array von int */
IntZeiger = IntArray; /* Zeiger auf Array-Startadresse */
IntArray[0] = 5; /* ist identisch mit folgender Zeile */
*IntZeiger = 5;
IntArray[1] = 4; /* ist identisch mit folgender Zeile */
*(IntZeiger + 1) = 4;
In der letzen Zeile wird zu dem Wert des Zeigers noch eine 1 dazuaddiert. Damit zeigt
dieser Zeiger nicht auf die nächste Speicheradresse, sondern auf das nächste
Element im Array (alle Elemente eines Arrays - auch mehrdimensionale - liegen direkt
hintereinander im Speicher). Im Beispiel liegt diese Adresse nicht eine sondern 4
Speicheradressen weiter, da der Datentyp int einen Speicherbedarf von
4 Byte hat. Das bedeutet, dass bei der Zeiger-Arithmetik der Speicherbedarf des
Datentyps, auf den der Zeiger zeigt, bei der Addition und Subtraktion berücksichtigt
wird.
Array | Zeiger | Adresse (hex.) | Inhalt |
IntArray[0] | *IntZeiger --> | 00004711h | 5 |
00004712h | |||
00004713h | |||
00004714h | |||
IntArray[1] | *(IntZeiger + 1) --> | 00004715h | 4 |
00004716h | |||
00004717h | |||
00004718h |
Mit Hilfe des Inkrement-Operators (++) kann diese letzte Zeile des
obigen Beispiels auch folgendermaßen geschrieben werden:
*(++IntZeiger) = 4;
Allerdings zeigt jetzt der Zeiger nicht mehr auf die Startadresse des Arrays, da das
Inkrementieren den Zeiger selbst verändert hat. Im Beispiel zeigt der Zeiger nun nicht
auf die Adresse 00004712h sondern auf die Adresse 00004715h.
8.6. Zeiger und Zeichenketten
Da Zeichenketten ein Spezialfall von Arrays sind, gilt der ganze vorige Abschnitt auch
für Zeichenketten. Hier sollen noch einige Beispiele zur Verwendung von Zeigern und
Zeichenketten zur Verdeutlichung vorgestellt werden.
Beispiel:
/* Textlänge ermitteln: */
char Text[] = "Dies ist ein Textarray!";
char *TextZeiger = Text;
int TextLaenge = 0;
while (*TextZeiger++)
TextLaenge++;
printf("Textlänge: %i",TextLaenge);
In der Bedingung der while-Schleife wird das erste Zeichen in der
Zeichenkette (dahin zeigt der Textzeiger) geprüft. Ist dieses wahr (also ungleich Null
und damit ungleich dem Nullzeichen für Textende), wird die Schleife ausgeführt.
Zuvor wird noch der Zeiger auf das nächste Zeichen gesetzt (also der Zeiger
inkrementiert). In der Schleife wird die Variable TextLaenge um eins
erhöht. Nun wird in der Bedingung das zweite Zeichen geprüft usw. Ist das
Textende erreicht, ist das Zeichen, auf das der Zeiger zeigt, das Nullzeichen, das den
ASCII-Wert Null hat. Null bedeutet aber auch falsch, womit die
while-Schleife nun abgebrochen wird. In der Variablen
TextLaenge steht damit die Anzahl der Zeichen in der
Zeichenkette.
Beispiel:
/* Text kopieren: */
char Text1[] = "Dieser Text soll kopiert werden!";
char Text2[35],*TextZeiger1 = Text1,*TextZeiger2 = Text2;
while (*TextZeiger2++ = *TextZeiger1++)
;
printf("kopierter Text: %s",Text2);
In dieser Schleife wird eine Zeichenkette kopiert. Als Bedingung wird die Zuweisung des
ersten Zeichens der Originalzeichenkette Text1 nach Text2 durchgeführt.
Anschließend werden beide Zeiger ums eins erhöht; sie zeigen damit auf das
nächste Zeichen. Das Ergebnis der Zuweisung ist das kopierte Zeichen selber und wird
jetzt als Bedingung geprüft. Ist es wahr (also ungleich Null und damit ungleich dem
Nullzeichen), wird die Schleife durchgeführt (Leeranweisung). Dann wird das
nächste Zeichen kopiert usw. Ist das Textende erreicht, ist das Zeichen, auf das der
Zeiger zeigt, das Nullzeichen. Auch dieses wird kopiert. Dann werden die Zeiger ums eins
erhöht und das kopierte Zeichen geprüft. Das Nullzeichen hat den ASCII-Wert Null
und dadurch ist die Bedingung falsch; die Schleife wird abgebrochen. Das Beispiel kopiert
also den vollständigen Text einschließlich des Nullzeichens; die Zeiger zeigen
anschließend auf das erste Zeichen nach dem Nullzeichen!
8.7. Zeiger und Strukturen
Mit Zeigern kann auf alle Objekte gezeigt werden, so auch auf Strukturen
(struct). Dabei verändert sich die Schreibweise, wenn mit Zeigern
auf Felder einer Struktur zugegriffen wird. Anstelle des Punktes zwischen Strukturnamen und
Feld wird nun ein Pfeil - bestehend aus Minuszeichen und Größer-als-Zeichen
(->) - verwendet. Das folgende Beispiel verdeutlicht dieses.
Beispiel:
struct Buch
{
char Titel[100];
char Autor[100];
char ISBN[20];
char Standort[10];
float Preis;
} Buecher[50]; /* 50mal struct Buch */
struct Buch *BuchZeiger;
BuchZeiger = Buecher; /* zeigt auf's 1. Array-Element */
Buecher[0].Preis = 9.99; /* ist identisch mit */
(*BuchZeiger).Preis = 9.99; /* ist identisch mit */
BuchZeiger->Preis = 9.99;
Hier gilt es, ganz genau hinzusehen und einen Teilausdruck nach dem anderen auszuwerten, um
zu verstehen, was tatsächlich passiert. Ansonsten ändert sich beim Umgang mit den
Strukturen gar nichts.
8.8. Unveränderbare Zeiger
Ein Zeiger kann unveränderbar sein (d.h. er kann auf keine andere Adresse zeigen) oder
auf eine unveränderbare Variable zeigen - oder beides; je nachdem, an welcher Stelle das
Schlüsselwort const verwendet wird.
Beispiel:
int i1 = 5;
int const i2 = 3;
// veränderbarer Zeiger auf veränderbare Variable:
int * ip1 = &i1;
// veränderbarer Zeiger auf unveränderbare Variable:
int const * ip2 = &i2;
// unveränderbarer Zeiger auf veränderbare Variable:
int * const ip3 = &i1;
// unveränderbarer Zeiger auf unveränderbare Variable:
int const * const ip4 = &i2;
*(ip1++); // erlaubt!
(*ip1)++; // erlaubt!
*(ip2++); // erlaubt!
(*ip2)++; // Fehler - unveränderbare Variable!
*(ip3++); // Fehler - unveränderbarer Zeiger!
(*ip3)++; // erlaubt!
*(ip4++); // Fehler - unveränderbarer Zeiger!
(*ip4)++; // Fehler - unveränderbare Variable!
Dabei muss eine Variable, auf die ein Zeiger auf unveränderbare Variable (z.B.
int const *) zeigt, nicht unbedingt unveränderbar sein. Dies mag
vielleicht unsinnig erscheinen, kann aber durchaus sinnvoll sein.
Beispiel:
int i = 5; // veränderbare Variable
int *ip1 = &i; // Zeiger auf veränderbare Variable
int const * ip2 = &i; // Zeiger auf
// unveränderbare Variable
i = 7; // ok
*ip1 = 9; // ok
*ip2 = 11; // Fehler, da ip2 auf
// unveränderbare Variable zeigt!
Obwohl alle drei Zuweisungen auf die gleiche (veränderbare) Speicheradresse zugreifen, hat
der Zeiger ip2 keine Veränderungsberechtigung. Man sagt auch: Der
Zeiger ip1 bietet eine veränderbare Ansicht (im engl: non-constant
view) und der Zeiger ip2 eine unveränderbare Ansicht (im engl.
constant view).
Anders herum funktioniert es übrigens nicht: Ein Zeiger auf eine veränderbare
Variable kann nicht auf eine unveränderbare Variable zeigen.
Beispiel:
int const i = 5;
int * ip = &i; // Fehler, da i unveränderbar ist!
8.9. Zeiger auf Zeiger
Ein Zeiger kann auch auf einen anderen Zeiger zeigen.
Beispiel:
kap08_02.c
01 #include <stdio.h>
02
03 int main()
04 {
05 int Wert = 1234;
06 int *Zeiger_auf_Wert = &Wert;
07 int **Zeiger_auf_Zeiger = &Zeiger_auf_Wert;
08
09 printf(" Wert = %i\n", Wert);
10 printf(" *Zeiger_auf_Wert = %i\n", *Zeiger_auf_Wert);
11 printf("**Zeiger_auf_Zeiger = %i\n", **Zeiger_auf_Zeiger);
12
13 return 0;
14 }
Ein Zeiger auf einen Zeiger wird durch einen doppelten Variablenoperator
(**) geschaffen. Diesem Zeiger wird die Adresse eines anderen Zeigers
zugewiesen, dem wiederum die Adresse einer Variablen zugewiesen wurde. Das angegebene
Beispiel gibt folgendes aus:
Wert = 1234
*Zeiger_auf_Wert = 1234
**Zeiger_auf_Zeiger = 1234
Dies kann beliebig fortgesetzt werden: Es wäre also möglich, einen Zeiger auf
Zeiger auf Zeiger auf Zeiger usw. zu erzeugen, aber es wird keine sinnvolle Anwendung
dafür geben.
Voriges Kapitel: 7. Strukturierte Datentypen
Nächstes Kapitel: 9. Funktionen