int
Variablen, auf anderen Architekturen können sie
darüber hinaus gehen. C/C++ bietet Möglichkeiten diese ``Namen''
oder Adressen der Speicherstellen für Programmierzwecke
nutzbar zu machen. An Übersichtlichkeit gewinnt das Programm dadurch,
daß man die Adressen selbst benennen darf. Das Programmsegment
int i=5; double a=12.34; char *c1="hello"; // declaration of an array of char int j=0;erzeugt beispielsweise die in Abb. abgebildeten Speichereinträge (linke Spalte). Nur die Speichereinträge benötigen wirklich Platz im Computer die Daten enthalten. Die Adressen bzw. Namen belegen nicht mehr Platz wenn man im Programm anstelle von
i
den weniger
kryptischen Namen counter
verwenden würde.
Im allgemeinen besteht leider keine
Garantie, daß hintereinander definierte Variablen auch
direkt nacheinander im Speicher liegen (Ausnahme: Felder).
Die Variable c1
vom Typ char
enthält einen Verweis
auf einen ganz anderen Speicherbereich, in dem die gespeicherten
Zeichen abgelegt sind.
Die Definition von *c1
reserviert im Speicher einen
Bereich, der genügt um das Wort "hello"
abzulegen.
Auf die einzelnen Elemente 'h'
, 'e'
, 'l'
,
'l'
und 'o'
kann man allerdings auch mit der
Formulierung c[0]
, c[1]
, c[2]
, c[3]
,
und c[4]
direkt zugreifen. Man beachte dabei daß
Numerierungen in C++ immer mit 0
beginnen.
Wenn z.B. nötig wird Daten in eine geordnete Liste schnell einzufügen oder zu löschen, so ist diese Operation nicht durch die Anordnung der Zeichen in einem derartigen linearen Feld zu erreichen. Ein lineares Feld ist dadurch ausgezeichnet, daß alle Daten in aufeinanderfolgenden Speicherbereichen liegen (die genaue Lokalisierung wird vom Computer übernommen). Sowohl das Einfügen als auch das Entfernen einer Zahl aus diesem Feld erfordert das Umkopieren aller ``oberhalb'' des modifizierten Elementes liegenden Daten, im Mittel also etwa halbsoviele Operationen wie das Feld Elemente aufweist. Dieser Aufwand ist nur bei kleinen Feldern akzeptabel. Eine Teillösung dieses Problems liegt in einer verketteten Liste, in der jedes Element noch die Information erhält ``wo'' das jeweils nächste im Speicher liegt (d.h. welches der Name der Speicherzelle ist, die den Wert des Elementes speichert). Die tatsächliche Position des Elementes spielt keine Rolle, so daß immer nur die Speicherzellennamen manipuliert werden müssen. Um zum Beispiel ein Element zu entfernen, muß nur der Namenseintrag des Vorgängers von in der Liste so geändert werden, daß er nicht mehr auf sondern auf dessen Nachfolger zeigt. Kopieren anderer Daten ist nicht mehr nötig, allerdings benötigt eine verkettete Liste evtl. mehr Speicher als eine lineare Liste, da für eine Zahl sowohl ihr Wert als auch eine Adresse abgelegt werden muß.
Um den Umgang mit solchen Speicherzellennamen unabhängig vom jeweiligen
Rechner zu gestalten, hält C/C++ den Zeiger/Pointer-Typ bereit.
Ein Zeiger kann auf einen beliebigen Datentyp verweisen. Dieser Typ kann auch
wieder ein Zeiger sein. Zur Deklaration benutzt man einen *
. Um die
Addresse einer Variablen zu erhalten, verwendet man den &
Operator.
Die Deklarationen im Programmsegment
// Declaration part int i = 5; // initialisation int *p_i = &i; // p_i is a pointer to an int, // here the variable i int **pp_i = (&p_i); // pointer to pointer to int // How to use * and & legally in the program *p_i = 1; // set i to 1, p_i remains unchanged **pp_i = 2; // set i to 2, p_i remains unchanged int j = 7; // declare another integer j *pp_i = &j; // change p_i to point on j, *p_i != i p_i = &i; // let p_i point to i againführen zu der in Abb. abgebildeten Speicherbelegung.
Um den Wert einer Speicherstelle, auf die ein Zeiger verweist,
zu ändern oder in einer Zuweisung oder Rechnung zu benutzen,
benutzen wir ebenfalls den *
Operator.
*p_i = 27; // i is 27 now *p_i = 2 * (*p_i) + 5; // equivalent to: i = 2 * i + 5;
Konstante Pointer werden durch das Setzen von const rechts vom Pointersymbol * deklariert. Sie müssen bereits bei der Deklaration initialisert werden. Jede weitere Zuweisung auf den Pointer wird vom Compiler als Fehler erkannt.
const int *cp_i = &i; // ok, i cannot be changed // through use of cp_i // cp_i can be changed const char newline = '\n'; const char * const p_nl = &newline; // const pointer to const char // note: p_nl = '0'; -> error!
Der *
Operator hat damit zwei Bedeutungen. Steht er in einer
Variablendeklaration, d.h. bei einem Typ, so kennzeichnet er einen
Pointer. Vor einer Variablen (vom Typ Pointer) bewirkt er den Zugriff
auf den Inhalt der Speicherstelle auf die der Pointer zeigt.
Analog hat der &
Operator zwei Bedeutungen. Bei einer
Variablendeklaration, d.h bei einem Typ kennzeichnet er eine Referenz.
Vor einer Variablen ist es der sog. adress of Operator, d.h. er
liefert die Adresse der Variablen.
Mit Zeigern kann Arithmetik getrieben werden; im obigen Beipiel
läßt z.B. p_i = p_i + 5
den Zeiger um 5 Speicherstellen für
int
weiterwandern. In dieser Operation benötigt der Compiler die
Information, wie groß der Datentyp ist, auf den verwiesen wird, um
richtig inkrementieren zu können. Ebenfalls kann die Differenz von
Pointern gebildet werden, wobei das Ergebnis ein int
eger ist. Ein
definiertes Ergebnis hat diese Operation jedoch nur, wenn die beiden
pointer in ein und dasselbe Datenfeld verweisen, s.u..
Wenn der Wert einer Speicherstelle benötigt wird, die man durch
Fortschalten des Pointers um n
Speicherstellen erhält, so gibt
es dafür eine kurze Notation mittels []
:
int p[30]; // declare a field of int *(p_i + 25) = 15; // the value of the 25th entry (past the one // pointed to by p_i) is set to 15 p_i[25] = 15; // equivalent ...
Die Sprache garantiert, daß 0 niemals als Adresse eines wirklichen Datenelementes vorkommt. Dieser Wert läßt sich also verwenden, um irgendwie geartete Fehler anzuzeigen, die bei einer Operation mit Pointern aufgetreten sind.