Zeiger (Pointer)

Zeiger (engl. pointers) sind Variablen, die als Wert die Speicheradresse einer anderen Variable enthalten.

Jede Variable wird in CPP an einer bestimmten Position im Hauptspeicher abgelegt. Diese Position nennt man Speicheradresse (engl. memory address). CPP bietet die Möglichkeit, die Adresse jeder Variable zu ermitteln. Solange eine Variable gültig ist, bleibt sie an ein und derselben Stelle im Speicher.

Am einfachsten vergegenwärtigt man sich dieses Konzept anhand der globalen Variablen. Diese werden außerhalb aller Funktionen und Klassen deklariert und sind überall gültig. Auf sie kann man von jeder Klasse und jeder Funktion aus zugreifen. Über globale Variablen ist bereits zur Kompilierzeit bekannt, wo sie sich innerhalb des Speichers befinden (also kennt das Programm ihre Adresse).

Zeiger sind nichts anderes als normale Variablen. Sie werden deklariert (und definiert), besitzen einen Gültigkeitsbereich, eine Adresse und einen Wert. Dieser Wert, der Inhalt der Zeigervariable, ist aber nicht wie in unseren bisherigen Beispielen eine Zahl, sondern die Adresse einer anderen Variable oder eines Speicherbereichs. Bei der Deklaration einer Zeigervariable wird der Typ der Variable festgelegt, auf den sie verweisen soll.

#include <iostream>
 
int main() {
     int   Wert;      // eine int-Variable
     int *pWert;      // eine Zeigervariable, zeigt auf einen int
     int *pZahl;      // ein weiterer "Zeiger auf int"
 
     Wert = 10;       // Zuweisung eines Wertes an eine int-Variable
 
     pWert = &Wert;   // Adressoperator '&' liefert die Adresse einer Variable
     pZahl = pWert;   // pZahl und pWert zeigen jetzt auf dieselbe Variable

Beispielhafte Speicherbelegung des Programms im Hauptspeicher:

DatentypVariableAdresseWert
intWert0x000110
int *pWert0x00050x0001
int *pZahl0x00090x0001
..
..

Der Adressoperator & kann auf jede Variable angewandt werden und liefert deren Adresse, die man einer (dem Variablentyp entsprechenden) Zeigervariablen zuweisen kann. Wie im Beispiel gezeigt, können Zeiger gleichen Typs einander zugewiesen werden. Zeiger verschiedenen Typs bedürfen einer Typumwandlung. Die Zeigervariablen pWert und pZahl sind an verschiedenen Stellen im Speicher abgelegt, nur die Inhalte sind gleich.

Wollen Sie auf den Wert zugreifen, der sich hinter der im Zeiger gespeicherten Adresse verbirgt, so verwenden Sie den Dereferenzierungsoperator *.

  *pWert += 5;
  *pZahl += 8;
 
   std::cout << "Wert = " << Wert << std::endl;

Beispielhafte Speicherbelegung des Programms im Hauptspeicher:

DatentypVariableAdresseWert
intWert0x000110 –> 15 –> 23
int *pWert0x00050x0001
int *pZahl0x00090x0001
..
..

Ausgabe:

Wert = 23

Man nennt das den Zeiger dereferenzieren. Im Beispiel erhalten Sie die Ausgabe Wert = 23, denn pWert und pZahl verweisen ja beide auf die Variable Wert.

Um es noch einmal hervorzuheben: Zeiger auf Integer (int) sind selbst keine Integer. Den Versuch, einer Zeigervariablen eine Zahl zuzuweisen, beantwortet der Compiler mit einer Fehlermeldung oder mindestens einer Warnung. Hier gibt es nur eine Ausnahme: die Zahl 0 darf jedem beliebigen Zeiger zugewiesen werden. Ein solcher Nullzeiger zeigt nirgendwohin. Der Versuch, ihn zu dereferenzieren, führt zu einem Laufzeitfehler.

Bespiel Zeigerübung

int main()
{ 
		int zahl=10;
		int *z=NULL;
		int **zz=NULL;
 
 
		cout<< zahl<<endl;			//Wert wird ausgegeben
		cout<<&zahl<<endl;			//adresse wird ausgegeben
		cout<<&zahl+1<<endl;		//adresse cpn zahl+1
  		cout<< zahl +1<<endl;		//11
 
 
  		z=&zahl;
 
  		cout<< *z<< endl;		//10
		cout<< z<< endl;		//Adresse von zahl= Wert von z 
		cout<< &z<< endl;		//Adresse von Zeiger z
 
		zz=&z;					//Adresse von Zeiger z wird in Zeiger zz 
 
		cout<< *zz<<endl;		//Wert von z= Adresse von zahl	//&zahl		//z
		cout<< **zz<<endl;		//Wert von zahl					//zahl
		cout<< zz<<endl;		//Adresse von z =Wert von zz	//&z		//zz
		cout<< &zz<<endl;		//Adresse von Zeiger zz			//&zz
 
  getch();      
  return 0;   	
}

Verschiedene Konventionen bei der Definition von Zeigern

Bei der Definition einer Zeigervariablen muss das Zeichen * nicht unmittelbar auf den Datentyp folgen. Die folgenden vier Definitionen sind gleichwertig:

int* i;  // Whitespace (z.B. ein Leerzeichen) nach *
int *i;  // Whitespace vor *
int * i; // Whitespace vor * und nach *
int*i;   // Kein whitespace vor * und nach *

Versuchen wir nun, diese vier Definitionen nach demselben Schema wie eine Definition von „gewöhnlichen“ Variablen zu interpretieren. Bei einer solchen Definition bedeutet

T v;

dass eine Variable v definiert wird, die den Datentyp T hat. Für die vier gleichwertigen Definitionen ergeben sich verschiedene Interpretationen, die als Kommentar angegeben sind:

int* i;   // Definition der Variablen i des Datentyps int*
int *i;   // Irreführend: Es wird kein "*i" definiert,
          // obwohl die dereferenzierte Variable *i heißt.
int * i;  // Datentyp int oder int* oder was?
int*i;    // Datentyp int oder int* oder was?

Offensichtlich passen nur die ersten beiden in dieses Schema. Die erste führt dabei zu einer richtigen und die zweite zu einer falschen Interpretation.

Allerdings passt die erste Schreibweise nur bei der Definitionen einer einzelnen Zeigervariablen in dieses Schema, da sich der * bei einer Definition nur auf die Variable unmittelbar rechts vom * bezieht:

int* i,j,k; // definiert int* i, int j, int k
            // und nicht: int* j, int* k

Zur Vermeidung solcher Missverständnisse sind zwei Schreibweisen verbreitet:

int *i,*j,*k; // definiert int* i, int* j, int* k

Dynamisch erzeugte Variablen: new und delete

Mit dem Operator new kann man Variablen während der Laufzeit des Programms erzeugen. Dieser Operator versucht, in einem eigens dafür vorgesehenen Speicherbereich (der oft auch als Heap, dynamischer Speicher oder freier Speicher bezeichnet wird) so viele Bytes zu reservieren, wie eine Variable des angegebenen Datentyps benötigt.

Falls der angeforderte Speicher zur Verfügung gestellt werden konnte, liefert new seine Adresse zurück. Andernfalls wird eine Exception des Typs std::bad_alloc ausgelöst, die einen Programmabbruch zur Folge hat, wenn sie nicht mit einer try- Anweisung abgefangen wird. Da unter 32-bit-Systemen wie Windows unabhängig vom physisch vorhandenen Hauptspeicher 2 GB virtueller Speicher zur Verfügung stehen, kann man meist davon ausgehen, dass der angeforderte Speicher verfügbar ist.

Variablen, die zur Laufzeit erzeugt werden, bezeichnet man auch als dynamisch erzeugte Variablen. Wenn der Unterschied zu vom Compiler erzeugten Variablen (wie „int i;“) betont werden soll, werden diese als „gewöhnliche“ Variablen bezeichnet. Die folgenden Beispiele zeigen, wie man new verwenden kann:

1. Für einen Datentyp T reserviert „new T“ so viele Bytes auf dem Heap, wie für eine Variable des Datentyps T notwendig sind. Der Ausdruck „new T“ hat den Datentyp „Zeiger auf T“. Sein Wert ist die Adresse des reservierten Speicherbereichs und kann einer Variablen des Typs „Zeiger auf T“ zugewiesen werden:

int* pi;
pi=new int; //reserviert sizeof(int) Bytes und weist pi die Adresse dieses Speicherbereichs zu
*pi=17;     // initialisiert den reservierten Speicherbereich

Am besten initialisiert man eine Zeigervariable immer gleich bei ihrer Definition:

int* pi=new int; // initialisiert pi mit der Adresse
*pi=17;

Die explizit geklammerte Version von new kann zur Vermeidung von Mehrdeutigkeiten verwendet werden. Für einfache Datentypen sind beide Versionen gleichwertig:

pi=new(int); // gleichwertig zu pi=new int;

2. In einem new-expression kann man nach dem Datentyp einen new-initializer angeben: Er bewirkt die Initialisierung der mit new erzeugten Variablen. Die zulässigen Ausdrücke und ihre Bedeutung hängen vom Datentyp der dynamisch erzeugten Variablen ab.

Für einen fundamentalen Datentyp (wie int, double usw.) gibt es drei Formen:

a) Der in Klammern angegebene Wert wird zur Initialisierung verwendet:

double* pd=new double(1.5); //Initialisierung *pd=1.5

b) Gibt man keinen Wert zwischen den Klammen an, wird die Variable mit 0 (NULL) initialisiert:

double* pd=new double(); //Initialisierung *pd=0

c) Ohne einen Initialisierer ist ihr Wert unbestimmt:

double* pd=new double;// *pd wird nicht initialisiert

Für einen Klassentyp müssen die Ausdrücke Argumente für einen Konstruktor sein.

3. Gibt man nach einem Datentyp T in eckigen Klammern einen ganzzahligen Ausdruck >= 0 an, wird ein Array dynamisch erzeugt reserviert.

Die Anzahl der Arrayelemente ist durch den ganzzahligen Ausdruck gegeben. Im Unterschied zu einem gewöhnlichen Array muss diese Zahl keine Konstante sein:

typedef double T; // T irgendein Datentyp, hier double
int n=100; // nicht notwendig eine Konstante
T* p=new T[n]; // reserviert n*sizeof(T) Bytes

Wenn durch einen new-Ausdruck ein Array dynamisch erzeugt wird, ist der Wert des Ausdrucks die Adresse des ersten Arrayelements. Die einzelnen Elemente des Arrays können folgendermaßen angesprochen werden:

p[0], ..., p[n–1] // n Elemente des Datentyps T

Die Elemente eines dynamisch erzeugten Arrays können nicht bei ihrer Definition initialisiert werden.

4. Mit dem nur selten eingesetzten new-placement kann man eine Variable an eine bestimmte Adresse platzieren. Damit wird keine Variable wie bei einem gewöhnlichen new-Ausdruck auf dem Heap angelegt. Die Adresse wird als Zeiger nach new angegeben und ist z.B. die Adresse eines zuvor reservierten Speicherbereichs:

int* pi=new int;
double* pd=new(pi) double;// erfordert #include<new.h>

Durch die letzte Anweisung kann man den Speicherbereich ab der Adresse von i als double ansprechen wie nach

double* pd=(double*)pi; // explizite Typkonversion

Dieses Beispiel zeigt, dass die meisten Programme keine Anwendungen für ein new placement haben. Nützlichere Anwendungen gibt es bei Betriebssystemen, die (anders als Windows) Geräte über physikalische Adressen ansprechen.

5. Variable, deren Datentyp eine Klasse der VCL ist, müssen mit new angelegt werden. Dabei muss dem Konstruktor der Eigentümer (z.B. Form1) übergeben werden.

TEdit* pe = new TEdit(Form1);
pe->Parent = Form1;
pe ->SetBounds(10, 20, 100, 30);
pe ->Text = "blablabla";

Es ist nicht möglich, VCL-Komponenten vom Compiler erzeugen zu lassen:

TEdit e; bzw. TEdit e(Form1); // Fehler

Eine gewöhnliche Variable existiert von ihrer Definition bis zum Ende des Bereichs (bei einer lokalen Variablen ist das der Block), in dem sie definiert wurde. Im Unterschied dazu existiert eine dynamisch erzeugte Variable bis der für sie reservierte Speicher mit dem Operator delete wieder freigegeben oder das Programm beendet wird. Man sagt auch, dass eine dynamisch erzeugte Variable durch den Aufruf von delete zerstört wird.

Die erste dieser beiden Alternativen ist für Variable, die keine Arrays sind, und die zweite für Arrays. Dabei muss cast-expression ein Zeiger sein, dessen Wert das Ergebnis eines new-Ausdrucks ist. Nach delete p ist der Wert von p unbestimmt und der Zugriff auf *p unzulässig. Falls p den Wert 0 hat, ist delete p wirkungslos.

Damit man mit dem verfügbaren Speicher sparsam umgeht, sollte man den Operator delete immer dann aufrufen, wenn eine mit new erzeugte Variable nicht mehr benötigt wird. Unnötig reservierter Speicher wird auch als Speicherleck (memory leak) bezeichnet. Ganz generell sollte man diese Regel beachten: Jede mit new erzeugte Variable sollte mit delete auch wieder freigegeben werden.

Die folgenden Beispiele zeigen, wie die in den letzten Beispielen reservierten Speicherbereiche wieder freigegeben werden.

1. Der Speicher für die unter 1. und 2. erzeugten Variablen wird folgendermaßen wieder freigegeben:

delete pi;
delete pd;

2. Der Speicher für das unter 3. erzeugte Array wird freigegeben durch

delete[] p;

3. Es ist möglich, die falsche Form von delete zu verwenden, ohne dass der Compiler eine Warnung oder Fehlermeldung ausgibt:

delete[] pi; // Arrayform für Nicht-Array
delete p; // Nicht-Arrayform für Array

Im C++-Standard ist explizit festgelegt, dass das Verhalten nach einem solchen falschen Aufruf undefiniert ist. Oben wurde empfohlen, einen Zeiger, der nicht auf reservierten Speicher zeigt, immer auf 0 (Null) zu setzen. Deshalb sollte man einen Zeiger nach delete immer auf 0 setzen, z.B. nach Beispiel 1:

pi=0;
pd=0;

Stroustrup empfiehlt dafür eine Funktion wie destroy:

void destroy(int*& p)
{//http://www.research.att.com/~bs/bs_faq2.html
delete p; // delete[] für Zeiger auf dynamische Arrays
p = 0;
}

In dieser Function wird der Zeiger-Parameter als Referenz übergeben, da sich die Zuweisung von 0 auf das Argument auswirken soll. Die wichtigsten Unterschiede zwischen dynamisch erzeugten und „gewöhnlichen“ (vom Compiler erzeugten) Variablen sind:

1. Eine dynamisch erzeugte Variable hat im Unterschied zu einer gewöhnlichen Variablen keinen Namen und kann nur indirekt über einen Zeiger angesprochen werden.

Nach einem erfolgreichen Aufruf von p=new type enthält der Zeiger p die Adresse der Variablen. Falls p überschrieben und nicht anderweitig gespeichert wird, gibt es keine Möglichkeit mehr, sie anzusprechen, obwohl sie weiterhin existiert und Speicher belegt. Der für sie reservierte Speicher wird erst beim Ende des Programms wieder freigegeben.

Da zum Begriff “Variable” auch ihr Name gehört, ist eine “namenlose Variable” eigentlich widersprüchlich. Im C++-Standard wird „object“ als Oberbegriff für namenlose und benannte Variablen verwendet. Ein Objekt in diesem Sinn hat wie eine Variable einen Wert, eine Adresse und einen Datentyp, aber keinen Namen. Da der Begriff „Objekt“ aber auch oft für Variable eines Klassentyps (siehe Objekte) verwendet wird, wird zur Vermeidung von Verwechslungen auch der Begriff „namenlose Variable“ verwendet.

2. Der Name einer gewöhnlichen Variablen ist untrennbar mit reserviertem Speicher verbunden. Es ist nicht möglich, über einen solchen Namen nicht reservierten Speicher anzusprechen. Im Unterschied dazu existiert ein Zeiger p, über den eine dynamisch erzeugte Variable angesprochen wird, unabhängig von dieser Variablen. Ein Zugriff auf *p ist nur nach new und vor delete zulässig. Vor new p oder nach delete p ist der Wert von p unbestimmt und der Zugriff auf *p ein Fehler, der einen Programmabbruch zur Folge haben kann:

int* pi = new int(17);
delete pi;
...
*pi=18; // Jetzt knallts - oder vielleicht auch nicht?

3. Der Speicher für eine gewöhnliche Variable wird automatisch freigegeben, wenn der Gültigkeitsbereich der Variablen verlassen wird. Der Speicher für eine dynamisch erzeugte Variable muss dagegen mit genau einem Aufruf von delete wieder freigegeben werden.

– Falls delete überhaupt nicht aufgerufen wird, kann das eine Verschwendung von Speicher (Speicherleck, memory leak) sein. Falls in einer Schleife immer wieder Speicher reserviert und nicht mehr freigegeben wird, können die swap files immer größer werden und die Leistungsfähigkeit des Systems nachlassen.

– Ein zweifacher Aufruf von delete mit demselben, von Null verschiedenen Zeiger, ist ein Fehler, der einen Programmabsturz zur Folge haben kann.

Beispiel: Anweisungen wie

int* pi = new int(1);
int* pj = new int(2);

zur Reservierung und

delete pi;
delete pj;

zur Freigabe von Speicher sehen harmlos aus. Wenn dazwischen aber eine ebenso harmlos aussehende Zuweisung stattfindet,

pi = pj;

haben die beiden delete Anweisungen den Wert von pj als Operanden. Diese Zuweisung führt also dazu, dass pj doppelt und pi überhaupt nicht freigegeben wird.

4. Oben haben wir gesehen, dass eine Zuweisung von Zeigern zu Aliasing führen kann. Bei einer Zuweisung an einen Zeiger auf eine dynamisch erzeugte Variable besteht außerdem noch die Gefahr von Speicherlecks. Das kann z.B. mit den folgenden Strategien vermieden werden:

– Man vermeidet solche Zuweisungen. Diese Strategie wird von der smart pointer Klasse scoped_ptr der Boost-Bibliothek (siehe http://boost.org/) verfolgt.

– Der Speicher für diese Variable wird vorher freigegeben.

int* pi = new int(17);
int* pj = new int(18);
delete pi; // um ein Speicherleck zu vermeiden
pi = pj;

Da anschließend zwei Zeiger pi und pj auf die mit new(18) erzeugte Variable *pj zeigen, muss darauf geachtet werden, dass der Speicher für diese Variable nicht freigegeben wird, solange über einen anderen Zeiger noch darauf zugegriffen werden kann. Diese Strategie wird von der smart pointer Klasse shared_ptr verfolgt.

– Der Speicher für eine dynamisch erzeugte Variable wird automatisch wieder freigegeben, wenn es keine Referenz mehr auf diese Variable gibt. Das wird als garbage collection bezeichnet. Garbage collection gehört allerdings noch nicht zum C++-Standard 2003. Es steht aber über die smart pointer Klasse shared_ptr der Boost-Bibliothek (siehe Abschnitt 3.12.5) sowie in speziellen Erweiterungen wie z.B. C++/CLI zur Verfügung. Es soll außerdem in den nächsten C++-Standard aufgenommen werden.

5. Der Operand von delete muss einen Wert haben, der das Ergebnis eines new-Ausdrucks ist. Wendet man delete auf einen anderen Ausdruck an, ist das außer bei einem Nullzeiger ein Fehler, der einen Programmabbruch zur Folge haben kann. Insbesondere ist es ein Fehler, delete auf einen Zeiger anzuwenden,

a) dem nie ein new-Ausdruck zugewiesen wurde.
b) der nach new verändert wurde.
c) der auf eine gewöhnliche Variable zeigt.

Beispiele: Der Aufruf von delete mit p1, p2 und p3 ist ein Fehler:

int* p1; // a)
int* p2 = new int(1);
p2++; // b)
int i = 17;
int* p3 = &i; // c)

6. Bei gewöhnlichen Variablen prüft der Compiler bei den meisten Operationen anhand des Datentyps, ob sie zulässig sind oder nicht. Ob für einen Zeiger delete aufgerufen werden muss oder nicht aufgerufen werden darf, ergibt sich dagegen nur aus dem bisherigen Ablauf des Programms.

Beispiel: Nach Anweisungen wie den folgenden ist es unmöglich, zu entscheiden, ob delete p aufgerufen werden muss oder nicht:

int i,x;
int* p;
if (x>0) p=&i;
else p = new int;

Diese Beispiele zeigen, dass mit dynamisch erzeugten Variablen Fehler möglich sind, die mit „gewöhnlichen“ Variablen nicht vorkommen können. Zwar sehen die Anforderungen bei den einfachen Beispielen hier gar nicht so schwierig aus. Falls aber new und delete in verschiedenen Teilen des Quelltextes stehen und man nicht genau weiß, welche Anweisungen dazwischen ausgeführt werden, können sich leicht Fehler einschleichen, die nicht leicht zu finden sind.

– Deshalb sollte man „gewöhnliche“ Variablen möglichst immer vorziehen.
– Falls sich Zeiger nicht vermeiden lassen, sollte man das Programm immer so einfach gestalten, dass möglichst keine Unklarheiten aufkommen können.
Smart pointer sind oft eine Alternative, die viele Probleme vermeidet.

Dynamisch erzeugte Variable bieten in der bisher verwendeten Form keine Vorteile gegenüber „gewöhnlichen“ Variablen. Trotzdem gibt es Situationen, in denen sie notwendig sind:

Hinweis: In der Programmiersprache C gibt es anstelle der Operatoren new und delete die Funktionen malloc bzw. free. Sie funktionieren im Prinzip genauso wie new und delete und können auch in C++ verwendet werden. Da sie aber mit void*-Zeigern arbeiten, sind sie fehleranfälliger. Deshalb sollte man immer new und delete gegenüber malloc bzw. free bevorzugen. Man kann in einem Programm sowohl malloc, new, free und delete verwenden. Speicher, der mit new reserviert wurde, sollte aber nie mit free freigeben werden. Dasselbe gilt auch für malloc und delete.