Der Adressoperator, Zuweisungen und generische Zeiger

Bei allen bisherigen Beispielen wurde einer Zeigervariablen p nie die Adresse einer Variablen zugewiesen. Das ist aber oft notwendig, wenn man mit der Variablen *p arbeiten will. Variablen können folgendermaßen erzeugt werden:

  1. Durch eine Variablendefinition erzeugt der Compiler eine Variable.
  2. Durch den Aufruf von Funktionen wie new können Variable während der Laufzeit eines Programms erzeugt werden.

In diesem Abschnitt wird die erste dieser beiden Möglichkeiten betrachtet. Die zweite folgt unter Dynamisch erzeugte Variablen

Mit dem Referenzierungs- oder Adressoperator & vor dem Namen einer Variablen erhält man ihre Adresse. Wenn die Variable v den Datentyp T hat, hat &v den Datentyp „Zeiger auf T“.

Beispiel 2a: Durch die Definitionen

int i=17; double d=18; char c='A';

wird Speicherplatz für die Variablen i, d und c reserviert. Die Adressen dieser Variablen haben den als Kommentar angegebenen Datentyp:

&i // Zeiger auf int
&d // Zeiger auf double
&c // Zeiger auf char

Einer Zeigervariablen kann durch eine Zuweisung oder Initialisierung ein Zeiger desselben Datentyps zugewiesen werden. Dabei wird die Adresse übertragen. Auf die zugehörige dereferenzierte Variable wirkt sich eine Zuweisung der Zeiger nicht aus.

Beispiel 2b: Die Adressen der Variablen

int i=17; double d=18; char c='A';

werden den Zeigern pi, pd und pc zugewiesen:

int* pi; double* pd;
pi = &i; // pi wird die Adresse von i zugewiesen
pd = &d; // pd wird die Adresse von d zugewiesen
char* pc=&c;// initialisiere pc mit &c

Weist man einer Zeigervariablen p wie in diesem Beispiel die Adresse einer Variablen v zu, kann der Speicherbereich von v sowohl unter dem Namen v als auch unter dem Namen *p angesprochen werden. Ändert man den Wert einer der beiden Variablen, wird damit automatisch auch der Wert der anderen Variablen verändert, ohne dass das durch eine explizite Anweisung aus dem Quelltext hervorgeht. Dieser Effekt wird als Aliasing bezeichnet.

Beispiel 2c: Nach den Zuweisungen des letzten Beispiels stellen *pi und i denselben Speicherbereich mit dem Wert 17 dar. Durch die nächsten Zuweisungen wird sowohl der Wert von *pi als auch der von i verändert, obwohl keine explizite Zuweisung an i stattfindet.

*pi=18; // i = 18;
*pi=(*pi)+1; // i = 19;

Es ist nur selten sinnvoll, denselben Speicherbereich unter zwei verschiedenen Namen anzusprechen: Meist ist es recht verwirrend, wenn sich der Wert einer Variablen verändert, ohne dass ihr explizit ein neuer Wert zugewiesen wurde. Wenn das in diesem Abschnitt trotzdem gemacht wird, dann nur um zu zeigen, dass Zuweisungen, die ja für „gewöhnliche“ Variable in gewisser Weise die einfachsten Anweisungen sind, bei Zeigern nicht ganz so einfach sein müssen.

Das soll aber nicht heißen, dass Aliasing immer schlecht ist. Wenn eine Funktion einen Parameter eines Zeigertyps hat, kann man über diesen Zeiger die Speicherbereiche ansprechen, deren Adresse als Argument übergeben wird. Ein solches Aliasing ist normalerweise nicht verwirrend. In C, wo es keine Referenzparameter gibt, werden solche Parameter oft verwendet.

Beispiel 2d: Wenn die Funktion

void f(int* p) { *p=17; }

wie in den nächsten Anweisungen aufgerufen wird, erhält i den Wert 17:

int i=18; f(&i);

In C++ kann einer Zeigervariablen nur ein Zeiger desselben Datentyps zugewiesen werden, falls man nicht eine der unten beschriebenen Zeigerkonversionen verwendet.

Beispiel 2e: Nach den Definitionen der letzten Beispiele verweigert der Compiler die Zuweisung

pd=pi; // Fehler: Konvertierung von 'int *'
// nach 'double *' nicht möglich

Wäre diese Zuweisung möglich, wäre *pd die 8 Bytes breite double-Variable ab der Adresse in pi. Falls pi auf eine int-Variable i zeigt, würde das Bitmuster von i (Binärdarstellung) als das einer Gleitkommazahl (Mantisse usw.) interpretiert und würde einen sinnlosen Wert darstellen. Falls ab der Adresse in pi nur 4 Bytes für eine int-Variable reserviert sind, kann der Zugriff auf *pd zu einer Zugriffsverletzung und zu einem Programmabsturz führen.

Da alle Zeiger eine Hauptspeicheradresse enthalten und deshalb gleich viele Bytes belegen, wäre es rein technisch kein Problem, einer Zeigervariablen pd des Datentyps double* einen Zeiger pi des Datentyps int* zuzuweisen. In C ist das auch möglich. Wie das letzte Beispiel aber zeigt, ist eine solche Zuweisung meist sinnlos.

Deshalb werden Zuweisungen an Zeigervariable vom Compiler normalerweise als Fehler betrachtet, wenn der zugewiesene Ausdruck nicht denselben Zeigertyp hat. Die einzigen Ausnahmen sind unter 1. bis 4. aufgeführt:

1. Einer Zeigervariablen kann unabhängig vom Datentyp der Wert 0 (Null) zugewiesen werden. Da keine Variable die Adresse 0 haben kann, bringt man mit diesem Wert meist zum Ausdruck, dass eine Zeigervariable nicht auf einen reservierten Speicherbereich zeigt. Das Ganzzahlliteral 0 ist der einzige Ganzzahlwert, den man einem Zeiger ohne eine explizite Typkonversion zuweisen kann. Ein solcher Zeiger wird auch als Nullzeiger bezeichnet.

Wenn man eine Zeigervariable bei ihrer Definition nicht mit der Adresse eines reservierten Speicherbereichs initialisieren kann, empfiehlt es sich immer, sie mit dem Wert 0 zu initialisieren:

int* pi = 0;

Hält man diese Konvention konsequent ein, kann man durch eine Abfrage auf den Wert 0 immer feststellen, ob sie auf einen reservierten Speicherbereich zeigt oder nicht:

if (pi!=0)
 Memo1->Lines->Add(*pi);
else
 Memo1->Lines->Add("*pi nicht definiert! ");

Diese Konvention erweist sich selbst dann als vorteilhaft, man eine solche Prüfung vergisst, da eine Dereferenzierung des Nullzeigers immer zu einer Zugriffsverletzung führt. Bei der Dereferenzierung eines Zeigers mit einem unbestimmten Wert ist dagegen eine Zugriffsverletzung keineswegs sicher, da der Zeiger zufällig auch auf reservierten Speicher zeigen kann. In C wird anstelle des Literals 0 meist das Makro NULL verwendet. Dafür besteht in C++ keine Notwendigkeit.

2. Mit einer expliziten Typkonversion (Typecast) kann man einer Zeigervariablen einen Zeiger auf einen anderen Datentyp zuweisen. Dabei gibt man den Zieldatentyp in Klammern vor dem zu konvertierenden Ausdruck an. Solche Typkonversionen sind in C++ aber meist weder notwendig noch sinnvoll.

Beispiel 2f: Durch diese Zuweisung erhält pd die Adresse in pi:

pd=(double*)pi; // konvertiert pi in double*

Wie oben schon erläutert, stellt dann *pd den Speicherbereich ab der Adresse in i als bedeutungslosen double-Wert dar. Der Zugriff auf *pd kann zu einer Zugriffsverletzung führen.

3. Der Datentyp void* wird als generischer Zeigertyp bezeichnet. Einem generischen Zeiger kann ein Zeiger auf einen beliebigen Zeigertyp zugewiesen werden.

Ein generischer Zeiger zeigt aber auf keinen bestimmten Datentyp. Deshalb ist es nicht möglich, einen generischen Zeiger ohne explizite Typkonversion zu dereferenzieren. Mit einer expliziten Typkonversion kann er aber in einen beliebigen Zeigertyp konvertiert werden.

Beispiel: Nach den Definitionen

int* pi;
void* pv;

ist die erste und die dritte der folgenden Zuweisungen möglich:

pv = pi;
pi = pv; // Fehler: Konvertierung nicht möglich
pi = (int*)pv; // explizite Typkonversion

Die Dereferenzierung eines generischen Zeigers ist ohne explizite Typkonversion nicht möglich:

*pi = *pv; // Fehler: Kein zulässiger Typ
int i = *((int*)pv); // das geht

Generische Zeiger werden vor allem in C für Funktionen und Datenstrukturen verwendet, die mit Zeigern auf beliebige Datentypen arbeiten können. In C++ sind sie aber meist weder notwendig noch sinnvoll.

4. Einem Zeiger auf ein Objekt einer Basisklasse kann auch ein Zeiger auf ein Objekt einer abgeleiteten Klasse zugewiesen werden.

Zeiger desselben Datentyps können mit ==,!=, <, ⇐, > oder >= verglichen werden. Das Ergebnis ergibt sich aus dem Vergleich der Adressen. Unabhängig vom Datentyp ist ein Vergleich mit dem Wert 0 (Null) möglich.