vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


Woche 2

Tag 8



Zeiger

Mit Zeigern (Pointern) läßt sich der Computer-Speicher direkt manipulieren. Damit gehören Zeiger zu den leistungsfähigsten Werkzeugen in der Hand des C++-Programmierers.

Heute lernen Sie,

Zeiger stellen in zweierlei Hinsicht beim Lernen von C++ eine Herausforderung dar: Sie können zeitweise ganz schön verwirrend sein und es ist nicht immer gleich ersichtlich, warum sie benötigt werden. Dieses Kapitel erläutert Schritt für Schritt, wie Zeiger funktionieren. Beachten Sie dabei aber bitte, daß der eigentliche Aha-Effekt erst mit fortschreitender Programmiertätigkeit (auf das Buch bezogen: mit späteren Kapiteln) zutage tritt.

Was ist ein Zeiger?

Ein Zeiger ist eine Variable, die eine Speicheradresse enthält.

Um Zeiger zu verstehen, müssen Sie erst ein wenig über den Hauptspeicher des Computers erfahren. Per Konvention teilt man den Hauptspeicher in fortlaufend numerierte Speicherzellen ein. Diese Nummern sind die sogenannten Speicheradressen, unter denen die Speicherstellen ansprechbar sind.

Jede Variable eines bestimmten Typs befindet sich an einer eindeutig adressierbaren Speicherstelle. Abbildung 8.1 zeigt schematisch die Speicherung der Integer-Variablen Alter vom Typ unsigned long.

Abbildung 8.1:  Schematische Darstellung von theAge im Hauptspeicher

Die Adressierung des Speichers unterscheidet sich bei den verschiedenen Computer- Typen. Normalerweise braucht der Programmierer die konkrete Adresse einer bestimmten Variablen gar nicht zu kennen, da sich der Compiler um die Einzelheiten kümmert. Wenn man allerdings diese Informationen haben möchte, kann sie mit Hilfe des Adreßoperators (&) ermitteln, wie es Listing 8.1 zeigt.

Listing 8.1: Adressen von Variablen

1:    // Listing 8.1 - Adressoperator und Adressen 
2: // lokaler Variablen
3:
4: #include <iostream.h>
5:
6: int main()
7: {
8: unsigned short shortVar=5;
9: unsigned long longVar=65535;
10: long sVar = -65535;
11:
12: cout << "shortVar:\t" << shortVar;
13: cout << " Adresse von shortVar:\t";
14: cout << &shortVar << "\n";
15:
16: cout << "longVar:\t" << longVar;
17: cout << " Adresse von longVar:\t" ;
18: cout << &longVar << "\n";
19:
20: cout << "sVar:\t" << sVar;
21: cout << " Adresse von sVar:\t" ;
22: cout << &sVar << "\n";
23:
24: return 0;
25: }

shortVar: 5             Adresse von shortVar: 0x8fc9:fff4
longVar: 65535 Adresse von longVar: 0x8fc9:fff2
sVar: -65535 Adresse von sVar: 0x8fc9:ffee

(Die Ausgabe kann bei Ihrem Computer ein etwas anderes Aussehen haben.)

Das Programm deklariert und initialisiert drei Variablen: eine vom Typ short in Zeile 8, eine unsigned long in Zeile 9 und eine long in Zeile 10. Die Werte und Adressen dieser Variablen gibt das Programm in den nachfolgenden Zeilen mit Hilfe des Adreßoperators (&) aus.

Der Wert von shortVar lautet (wie erwartet) 5, und die Adresse bei Ausführung des Programms auf dem 386er Computer des Autors ist 0x8fc9:fff4. Diese Adresse hängt vom jeweiligen Computer ab und kann bei einem neuen Programmstart etwas anders aussehen. Was sich allerdings nicht ändert, ist die Differenz von 2 Byte zwischen den beiden ersten Adressen, falls die Darstellung von short Integer-Zahlen auf Ihrem Computer mit 2 Byte erfolgt. Die Differenz zwischen der zweiten und dritten Adresse beträgt 4 Byte bei einer Darstellung von long int mit 4 Byte. Abbildung 8.2 zeigt, wie das Programm die Variablen im Speicher ablegt.

Abbildung 8.2:  Speicherung der Variablen

Es gibt eigentlich keine Notwendigkeit, den tatsächlichen numerischen Wert der Adresse jeder Variablen zu kennen. Wichtig ist einzig, daß jede Variable eine Adresse hat und die entsprechende Anzahl von Bytes im Speicher reserviert sind. Man teilt dem Compiler durch die Deklaration des Variablentyps mit, wieviel Speicher eine bestimmte Variable benötigt. Der Compiler übernimmt dann automatisch die Zuweisung einer Adresse. Wenn man also eine Variable vom Typ unsigned long deklariert, weiß der Compiler, daß 4 Byte im Speicher zu reservieren sind, da jede Variable dieses Typs 4 Byte benötigt.

Die Adresse in einem Zeiger speichern

Jede Variable hat eine Adresse. Selbst ohne Kenntnis der genauen Adresse einer bestimmten Variablen, kann man diese Adresse in einem Zeiger speichern.

Nehmen wir zum Beispiel an, die Variable wieAlt sei vom Typ int. Um einen Zeiger namens pAlter zur Aufnahme ihrer Adresse zu deklarieren, schreibt man

int *pAlter = 0;

Damit deklariert man pAlter als Zeiger auf int. Das heißt, pAlter wird für die Aufnahme der Adresse einer int-Zahl deklariert.

Beachten Sie, daß pAlter eine Variable wie jede andere Variable ist. Wenn man eine Integer-Variable (vom Typ int) deklariert, wird sie für die Aufnahme einer ganzen Zahl eingerichtet. Ein Zeiger ist einfach ein spezieller Typ einer Variablen, die man für die Aufnahme der Adresse eines bestimmten Objekts im Speicher einrichtet. In diesem Beispiel nimmt pAlter die Adresse einer Integer-Variablen auf.

Die obige Anweisung initialisiert pAlter mit 0. Ein Zeiger mit dem Wert 0 heißt Null- Zeiger. Alle Zeiger sollte man bei ihrer Erzeugung initialisieren. Wenn man nicht weiß, was man dem Zeiger zuweisen soll, wählt man einfach den Wert 0. Ein nicht initialisierter Zeiger ist ein sogenannter »wilder Zeiger«. Wie der Name vermuten läßt, sind derartige Zeiger gefährlich.

Praktizieren Sie sichere Programmierung: Initialisieren Sie die Zeiger!

Im nächsten Schritt weist man dem Zeiger explizit die Adresse von wieAlt zu. Das läßt sich beispielsweise wie folgt realisieren:

unsigned short int wieAlt = 50;     // eine Integer-Variable erzeugen
unsigned short int * pAlter = 0; // einen Zeiger erzeugen
pAlter = &wieAlt; // die Adresse von wieAlt an pAlter zuweisen

Die erste Zeile erzeugt die Variable wieAlt vom Typ unsigned short int und initialisiert sie mit dem Wert 50. Die zweite Zeile deklariert pAlter als Zeiger auf den Typ unsigned short int und initialisiert ihn mit 0. Das Sternchen (*) nach dem Variablentyp und vor dem Variablennamen kennzeichnet pAlter als Zeiger.

Die dritte und letzte Zeile weist die Adresse von wieAlt an den Zeiger pAlter zu. Die Zuweisung einer Adresse kennzeichnet der Adreßoperator (&). Ohne diesen Operator hätte man den Wert von wieAlt zugewiesen. Auch wenn es sich dabei um eine gültig Adresse handeln sollte, hat diese wahrscheinlich nichts mit der beabsichtigten Adresse gemein.

Nunmehr enthält pAlter als Wert die Adresse von wieAlt. Die Variable wieAlt hat ihrerseits den Wert 50. Das Ganze kann man auch mit einem Schritt weniger erreichen:

unsigned short int wieAlt = 50;         // eine Variable erzeugen
unsigned short int * pAlter = &wieAlt; // einen Zeiger auf wieAlt erzeugen

pAlter ist ein Zeiger, der nun die Adresse der Variablen wieAlt enthält. Mittels pAlter kann man nun auch den Wert von wieAlt bestimmen, der im Beispiel 50 lautet. Den Zugriff auf wieAlt über den Zeiger pAlter nennt man Umleitung (Indirektion), da man indirekt auf wieAlt mit Hilfe von pAlter zugreift. Später in diesem Kapitel erfahren Sie, wie man mit Hilfe der Indirektion auf den Wert einer Variablen zugreift.

Indirektion bedeutet den Zugriff auf die Variable mit der im Zeiger gespeicherten Adresse. Der Zeiger stellt einen indirekten Weg bereit, um den an dieser Adresse abgelegten Wert zu erhalten.

Zeigernamen

Zeiger können jeden Namen erhalten, der auch für andere Variablen gültig ist. Viele Programmierer folgen der Konvention, allen Zeigernamen zur Kennzeichnung ein p voranzustellen, wie in pAlter und pZahl (p steht für pointer = Zeiger).

Der Indirektionsoperator

Den Indirektionsoperator (*) bezeichnet man auch als Dereferenzierungsoperator. Wenn ein Zeiger dereferenziert wird, ruft man den Wert an der im Zeiger gespeicherten Adresse ab.

Normale Variablen erlauben direkten Zugriff auf ihre eigenen Werte. Wenn man eine neue Variable vom Typ unsigned short int namens ihrAlter erzeugt und den Wert in wieAlt dieser neuen Variablen zuweisen möchte, schreibt man

unsigned short int ihrAlter;
ihrAlter = wieAlt;

Ein Zeiger bietet indirekten Zugriff auf den Wert der Variablen, dessen Adresse er speichert. Um den Wert in wieAlt an die neue Variable ihrAlter mit Hilfe des Zeigers pAlter zuzuweisen, schreibt man

unsigned short int ihrAlter;
ihrAlter = *pAlter;

Der Indirektionsoperator (*) vor der Variablen pAlter bedeutet »der an der nachfolgenden Adresse gespeicherte Wert«. Die Zuweisung ist wie folgt zu lesen: »Nimm den an der Adresse in pAlter gespeicherten Wert und weise ihn ihrAlter zu.«

Der Indirektionsoperator (*) kommt bei Zeigern in zwei unterschiedlichen Versionen vor: Deklaration und Dereferenzierung. Bei der Deklaration eines Zeigers gibt das Sternchen an, daß es sich um einen Zeiger und nicht um eine normale Variable handelt. Dazu ein Beispiel:

unsigned short * pAlter = 0; // einen Zeiger auf unsigned short erzeugen

Wenn der Zeiger dereferenziert wird, gibt der Indirektionsoperator an, daß auf den Wert an der im Zeiger abgelegten Speicheradresse zuzugreifen ist und nicht auf die Adresse selbst:

*pAlter = 5; // Der Speicherzelle, deren Adresse in pAlter steht, den Wert 5 zuweisen

Beachten Sie auch, daß C++ das gleiche Zeichen (*) als Multiplikationsoperator verwendet. Der Compiler erkennt aus dem Kontext, welcher Operator gemeint ist.

Zeiger, Adressen und Variablen

Die folgenden drei Dinge muß man sicher auseinanderhalten können, um Probleme mit Zeigern zu vermeiden:

Sehen Sie sich das folgende Codefragment an:

int dieVariable = 5;
int * pZeiger = &dieVariable ;

Die Variable dieVariable ist als Integer-Variable deklariert und mit dem Wert 5 initialisiert. pZeiger ist als Zeiger auf einen Integer deklariert und wird mit der Adresse von dieVariable initialisiert. pZeiger ist der Zeiger. Die von pZeiger gespeicherte Adresse ist die Adresse von dieVariable. Der Wert an der von pZeiger gespeicherten Adresse lautet 5. Abbildung 8.3 zeigt eine schematische Darstellung von dieVariable und pZeiger.

Daten mit Hilfe von Zeigern manipulieren

Nachdem man einem Zeiger die Adresse einer Variablen zugewiesen hat, kann man mit diesem Zeiger auf die Daten in der Variablen zugreifen. Listing 8.2 demonstriert, wie der Zugriff auf eine lokale Variable mittels eines Zeigers vonstatten geht und wie der Zeiger die Werte in dieser Variablen manipuliert.

Listing 8.2: Daten mit Hilfe von Zeigern manipulieren

1:     // Listing 8.2 Verwendung von Zeigern
2:
3: #include <iostream.h>
4:
5: typedef unsigned short int USHORT;
6: int main()
7: {
8: USHORT myAge; // eine Variable
9: USHORT * pAge = 0; // ein Zeiger
10: myAge = 5;
11: cout << "myAge: " << myAge << "\n";
12: pAge = &myAge; // Adresse von myAge an pAge zuweisen
13: cout << "*pAge: " << *pAge << "\n\n";
14: cout << "*pAge = 7\n";
15: *pAge = 7; // myAge auf 7 setzen
16: cout << "*pAge: " << *pAge << "\n";
17: cout << "myAge: " << myAge << "\n\n";
18: cout << "myAge = 9\n";
19: myAge = 9;
20: cout << "myAge: " << myAge << "\n";
21: cout << "*pAge: " << *pAge << "\n";
22:
23: return 0;
24: }

myAge: 5
*pAge: 5

*pAge = 7
*pAge: 7
myAge: 7

myAge = 9
myAge: 9
*pAge: 9

Dieses Programm deklariert zwei Variablen: myAge vom Typ unsigned short und pAge als Zeiger auf eine Variable vom Typ unsigned short. In Zeile 10 wird der Variablen myAge der Wert 5 zugewiesen. Die Ausgabe in Zeile 11 dient der Kontrolle der Zuweisung.

Zeile 12 weist pAge die Adresse von myAge zu. Zeile 13 dereferenziert pAge und gibt das erhaltene Ergebnis aus. Es zeigt, daß der Wert an der in pAge gespeicherten Adresse der in myAge abgelegte Wert 5 ist. Zeile 15 weist den Wert 7 der Variablen zu, die sich an der in pAge gespeicherten Adresse befindet. Damit setzt man myAge auf 7. Eine Bestätigung liefern die Ausgabeanweisungen in den Zeilen 16 bis 17.

Zeile 19 weist der Variablen myAge den Wert 9 zu. Auf diesen Wert wird in Zeile 20 direkt und in Zeile 21 indirekt - durch Dereferenzierung von pAge - zugegriffen.

Adressen untersuchen

Zeiger erlauben die Manipulation von Adressen, ohne daß man deren eigentlichen Wert kennt. Üblicherweise vertraut man darauf, daß nach der Zuweisung einer Variablenadresse an einen Zeiger der Wert des Zeigers wirklich die Adresse der Variablen ist. An dieser Stelle wollen wir aber einmal überprüfen, ob dem wirklich so ist. Listing 8.3 zeigt ein entsprechendes Programm.

Listing 8.3: Untersuchen, was in einem Zeiger gespeichert ist

1:      // Listing 8.3 Was in einem Zeiger gespeichert ist
2:
3: #include <iostream.h>
4:
5:
6: int main()
7: {
8: unsigned short int myAge = 5, yourAge = 10;
9: unsigned short int * pAge = &myAge; // ein Zeiger
10: cout << "myAge:\t" << myAge << "\tyourAge:\t" << yourAge <<"\n";
11: cout <<"&myAge:\t" << &myAge <<"\t&yourAge:\t" << &yourAge <<"\n";
12: cout << "pAge:\t" << pAge << "\n";
13: cout << "*pAge:\t" << *pAge << "\n";
14: pAge = &yourAge; // den Zeiger erneut zuweisen
15: cout << "myAge:\t" << myAge << "\tyourAge:\t" << yourAge << "\n";
16: cout <<"&myAge:\t" << &myAge <<"\t&yourAge:\t" << &yourAge <<"\n";
17: cout << "pAge:\t" << pAge << "\n";
18: cout << "*pAge:\t" << *pAge << "\n";
19: cout << "&pAge:\t" << &pAge << "\n";
20: return 0;
21: }

myAge:     5             yourAge:  10
&myAge: 0x355C &yourAge: 0x355E
pAge: 0x355C
*pAge: 5
myAge: 5 yourAge: 10
&myAge: 0x355C &yourAge: 0x355E
pAge: 0x355E
*pAge: 10
&pAge: 0x355A

(Die Ausgabe kann bei Ihnen etwas andere Ergebnisse zeigen.)

Zeile 8 deklariert myAge und yourAge als Integer-Variablen vom Typ unsigned short. Zeile 9 deklariert pAge als Zeiger auf einen Integerwert vom Typ unsigned short. Der Zeiger wird mit der Adresse der Variablen myAge initialisiert.

Die Zeilen 10 und 11 geben die Werte und Adressen von myAge und yourAge aus. Zeile 12 zeigt den Inhalt von pAge an, wobei es sich um die Adresse von myAge handelt. Zeile 13 dereferenziert pAge und zeigt das Ergebnis an: den Wert an der in pAge verzeichneten Adresse, das heißt, den Wert in myAge, also 5.

Das Wesen der Zeiger dürfte nun klar sein. Zeile 12 zeigt, daß pAge die Adresse von myAge speichert, und Zeile 13 zeigt, wie man den in myAge gespeicherten Wert durch Dereferenzierung des Zeigers pAge ermittelt. Bevor Sie im Stoff fortfahren, sollten Ihnen diese Dinge in Fleisch und Blut übergegangen sein. Studieren Sie den Code, und sehen Sie sich die Ausgaben an.

In Zeile 14 findet eine Neuzuweisung von pAge statt, so daß diese Variable (der Zeiger) nun auf die Adresse von yourAge weist. Die Werte und Adressen werden auch hier wieder ausgegeben. Anhand der Ausgabe kann man ablesen, daß pAge nun die Adresse der Variablen yourAge enthält und daß die Dereferenzierung den Wert in yourAge holt.

Zeile 19 gibt die Adresse von pAge selbst aus. Wie jede Variable hat auch pAge eine Adresse, und diese Adresse läßt sich in einem Zeiger speichern. (Auf die Zuweisung der Adresse eines Zeigers an einen anderen Zeiger gehen wir in Kürze ein.)

Was Sie tun sollten

Verwenden Sie den Indirektionsoperator (*), um auf die Daten an einer in einem Zeiger enthaltenen Adresse zuzugreifen.

Initialisieren Sie alle Zeiger entweder mit einer gültigen Adresse oder mit Null (0).

Denken Sie an den Unterschied zwischen der Adresse in einem Zeiger und dem Wert an dieser Adresse.

Mit Zeigern programmieren

Deklarieren Sie einen Zeiger wie folgt: Zuerst geben Sie den Typ der Variablen oder des Objekts an, dessen Adresse im Zeiger gespeichert werden soll. Anschließend folgt der Zeigeroperator (*) und der Name des Zeigers. Ein Beispiel:

    unsigned short int * pZeiger = 0;

Um einen Zeiger zu initialisieren oder ihm einen Wert zuzuweisen, stellen Sie vor den Namen der Variablen, deren Adresse zugewiesen wird, den Adreßoperator (&). Ein Beispiel:

    unsigned short int dieVariable = 5;
unsigned short int * pZeiger = & dieVariable;

Zur Dereferenzierung eines Zeigers stellen Sie dem Zeigernamen den Dereferenzierungsoperator (*) voran. Ein Beispiel:

    unsigned short int derWert = *pZeiger;

Warum man Zeiger verwendet

Bisher haben Sie in einzelnen Schritten die Zuweisung der Adresse einer Variablen an einen Zeiger kennengelernt. In der Praxis wird das allerdings in dieser Form kaum vorkommen. Warum plagt man sich überhaupt mit einem Zeiger herum, wenn man bereits eine Variable mit Zugriff auf deren Wert hat? Diese Art der Zeigermanipulation einer automatischen Variablen sollte hier nur die Arbeitsweise von Zeigern demonstrieren. Nachdem Sie nun mit der Syntax der Zeiger vertraut sind, können Sie sich den echten Einsatzfällen zuwenden. Vorwiegend kommen dabei drei Aufgaben in Betracht:

Das restliche Kapitel konzentriert sich auf die Verwaltung von Daten im Heap und den Zugriff auf Datenelemente und Funktionen einer Klasse. Morgen lernen Sie, wie man Variablen als Referenz übergibt.

Stack und Heap

Im Abschnitt »Arbeitsweise von Funktionen - ein Blick hinter die Kulissen« aus Kapitel 5 werden fünf Speicherbereiche erwähnt:

Lokale Variablen befinden sich zusammen mit den Funktionsparametern auf dem Stack. Der Code steht natürlich im Codebereich, und globale Variablen im Bereich der globalen Namen. Die Register dienen internen Verwaltungsaufgaben. Dazu gehören beispielsweise Stack-Operationen und die Steuerung des Programmablaufs. Der verbleibende Speicher geht an den sogenannten Heap (frei verfügbarer Speicher).

Leider sind lokale Variablen nicht dauerhaft verfügbar: Kehrt eine Funktion zurück, werden die Variablen verworfen. Globale Variablen lösen dieses Problem für den Preis des uneingeschränkten Zugriffs durch das gesamte Programm hindurch. Dabei entsteht ein Code, der schwer zu verstehen und zu warten ist. Bringt man die Daten auf den Heap, umgeht man beide Nachteile.

Man kann sich den Heap als ausgedehnten Speicherbereich vorstellen, in dem Tausende von fortlaufend numerierten Fächern auf Ihre Daten warten. Genau wie beim Stack lassen sich diese Fächer aber nicht benennen. Man muß die Adresse des zu reservierenden Fachs abrufen und diese Adresse dann in einem Zeiger festhalten.

Als Analogie dazu folgendes Beispiel: Ein Freund gibt Ihnen die gebührenfreie Rufnummer für Acme Mail Order. Sie gehen nach Hause und programmieren Ihr Telefon mit dieser Nummer. Dann schmeißen Sie den Zettel weg, auf dem Sie sich die Nummer notiert hatten. Wenn Sie die Kurzwahltaste drücken, läutet das Telefon irgendwo und Acme Mail Order antwortet. An die Nummer können Sie sich nicht erinnern, und Sie wissen auch nicht, wo sich das andere Telefon befindet. Aber die Kurzwahltaste gibt Ihnen den Zugriff zu Acme Mail Order. Vergleichbar mit Acme Mail Order sind die Daten in Ihrem Heap. Sie wissen nicht, wo sie abgelegt sind, aber Sie wissen, wie man sie holen kann. Man greift über die Adresse darauf zu - im obigen Beispiel mit der Telefonnummer. Diese Nummer müssen Sie aber nicht kennen, Sie brauchen sie nur in einem Zeiger ablegen - das heißt, der Kurzwahltaste zuordnen. Der Zeiger gibt Ihnen den Zugriff auf Ihre Daten, ohne daß Sie sich um die Details kümmern müssen.

Kehrt eine Funktion zurück, räumt sie automatisch den Stack auf. Alle lokalen Variablen verlieren ihren Gültigkeitsbereich und verschwinden vom Stack. Den Heap räumt ein Programm erst am Ende auf. Es liegt in Ihrer Verantwortung, reservierten Speicher freizugeben, wenn Sie ihn nicht mehr benötigen.

Der Vorteil des Heap liegt darin, daß der reservierte Speicher verfügbar bleibt, bis man ihn explizit freigibt. Wenn man Speicher auf dem Heap reserviert, während das Programm eine Funktion abarbeitet, bleibt der Speicher nach Rückkehr der Funktion weiterhin zugänglich.

Gegenüber globalen Variablen bringt ein derartiger Zugriff auf den Speicher den Vorteil, daß nur Funktionen mit Zugriff auf den Zeiger auch auf die Daten zugreifen können. Damit hat man eine gut kontrollierte Schnittstelle zu den Daten und vermeidet die Probleme, daß fremde Funktionen die Daten in unerwarteter und nicht vorgesehener Weise verändern.

Damit das Ganze funktioniert, muß man einen Zeiger auf einen Bereich im Heap erzeugen und den Zeiger an Funktionen übergeben können. Die folgenden Abschnitte erläutern diese Vorgehensweise.

new

In C++ weist man Heap-Speicher mit dem Schlüsselwort new zu. Auf new folgt der Typ des Objekts, für das man Speicher reservieren will, damit der Compiler die erforderliche Speichergröße kennt. So reserviert die Anweisung new unsigned short int 2 Byte auf dem Heap und new long reserviert 4 Byte.

Der Rückgabewert von new ist eine Speicheradresse. Diese muß man einem Zeiger zuweisen. Um einen unsigned short im Heap zu erzeugen, schreibt man etwa

unsigned short int * pZeiger;
pZeiger = new unsigned short int;

Natürlich kann man den Zeiger auch gleich bei seiner Erzeugung initialisieren:

unsigned short int * pZeiger = new unsigned short int;

In beiden Fällen zeigt nun pZeiger auf einen unsigned short int im Heap. Man kann diesen Zeiger wie jeden anderen Zeiger auf eine Variable verwenden und einen Wert in diesem Bereich des Speichers ablegen:

*pZeiger = 72;

Das bedeutet: »Lege 72 im Wert von pZeiger ab« oder »Weise den Wert 72 an den Bereich im Heap zu, auf den pZeiger zeigt«.

Wenn new keinen Speicher im Heap erzeugen kann - Speicher ist immerhin eine begrenzte Ressource - wird eine Ausnahme ausgelöst (siehe Kapitel 20, »Exceptions und Fehlerbehandlung«).

delete

Wenn man den Speicherbereich nicht mehr benötigt, ruft man für den Zeiger delete auf. delete gibt den Speicher an den Heap zurück. Denken Sie daran, daß der Zeiger selbst - im Gegensatz zum Speicher, auf den er zeigt - eine lokale Variable ist. Wenn die Funktion, in der er deklariert ist, zurückkehrt, verliert der Zeiger seinen Gültigkeitsbereich und ist nicht mehr zugänglich. Der mit dem Operator new reservierte Speicher wird allerdings nicht automatisch freigegeben. Auf den Speicher kann man nicht mehr zugreifen - es entsteht eine sogenannte Speicherlücke. Diese Lücke läßt sich bis zum Ende des Programms nicht mehr beseitigen und es scheint, als ob der Computer leck wäre und Speicherplatz verliere.

Mit dem Schlüsselwort delete gibt man Speicher an den Heap zurück:

delete pZeiger;

Wenn man den Zeiger löscht, gibt man praktisch den Speicher frei, dessen Adresse im Zeiger abgelegt ist. Man sagt also: »Gebe den Speicher, auf den der Zeiger verweist, an den Heap zurück«. Der Zeiger bleibt weiterhin ein Zeiger und kann erneut zugewiesen werden. Listing 8.4 zeigt, wie man eine Variable auf dem Heap reserviert, diese Variable verwendet und sie dann löscht.

Wenn man delete auf einen Zeiger anwendet, wird der Speicher, auf den der Zeiger verweist, freigegeben. Ruft man delete erneut auf diesem Zeiger auf, stürzt das Programm ab! Setzen Sie den Zeiger daher nach dem Löschen auf 0 (Null). Der Aufruf von delete für einen Null-Zeiger ist garantiert sicher. Dazu folgendes Beispiel:

  Animal *pDog = new Animal;
delete pDog; // gibt den Speicher frei
pDog = 0; // setzt den Zeiger auf Null
//...
delete pDog; // gefahrlos

Listing 8.4: Reservieren und Löschen von Zeigern

1:     // Listing 8.4
2: // Speicher reservieren und loeschen
3:
4: #include <iostream.h>
5: int main()
6: {
7: int localVariable = 5;
8: int * pLocal= &localVariable;
9: int * pHeap = new int;
10: *pHeap = 7;
11: cout << "localVariable: " << localVariable << "\n";
12: cout << "*pLocal: " << *pLocal << "\n";
13: cout << "*pHeap: " << *pHeap << "\n";
14: delete pHeap;
15: pHeap = new int;
16: *pHeap = 9;
17: cout << "*pHeap: " << *pHeap << "\n";
18: delete pHeap;
19: return 0;
20: }

localVariable: 5
*pLocal: 5
*pHeap: 7
*pHeap: 9

Zeile 7 deklariert und initialisiert eine lokale Variable. Zeile 8 deklariert und initialisiert einen Zeiger mit der Adresse der lokalen Variablen. Zeile 9 deklariert einen weiteren Zeiger und initialisiert ihn mit dem Ergebnis aus dem Aufruf von new int. Damit reserviert man im Heap Speicher für eine ganze Zahl (int).

Zeile 10 weist den Wert 7 an die neu reservierte Speicherstelle zu. Zeile 11 gibt den Wert der lokalen Variablen aus, und Zeile 12 zeigt den Wert an, auf den pLocal verweist. Wie erwartet, sind die Ergebnisse gleich. Zeile 13 gibt den Wert aus, auf den pHeap zeigt. Der in Zeile 10 zugewiesene Wert ist also tatsächlich zugänglich.

Der Aufruf von delete in Zeile 14 gibt den in Zeile 9 reservierten Speicher an den Heap zurück. Diese Aktion gibt den Speicher frei und hebt die Zuordnung des Zeigers zu diesem Speicherbereich auf. pHeap steht nun wieder als Zeiger für einen anderen Speicherbereich zur Verfügung. In den Zeilen 15 und 16 findet eine erneute Zuweisung statt, und Zeile 17 bringt das Ergebnis auf den Bildschirm. Zeile 18 gibt den Speicher an den Heap zurück.

Obwohl Zeile 18 redundant ist (durch das Ende des Programms wird der Speicher ohnehin zurückgegeben), empfiehlt es sich immer, diesen Speicher explizit freizugeben. Wenn man das Programm ändert oder erweitert, ist es vorteilhaft, daß dieser Schritt bereits berücksichtigt wurde.

Speicherlücken

Unbeabsichtigte Speicherlücken entstehen auch, wenn man einem Zeiger einen neuen Speicherbereich zuweist, ohne vorher den vom Zeiger referenzierten Speicher freizugeben. Sehen Sie sich das folgende Codefragment an:

1:   unsigned short int * pZeiger = new unsigned short int;
2: *pZeiger = 72;
3: pZeiger = new unsigned short int;
4: *pZeiger = 84;

Zeile 1 erzeugt pZeiger und weist ihm die Adresse eines Bereichs auf dem Heap zu. Zeile 2 speichert den Wert 72 in diesem Speicherbereich. Zeile 3 nimmt eine erneute Zuweisung von pZeiger auf einen anderen Speicherbereich vor. Zeile 4 plaziert den Wert 84 in diesem Bereich. Der ursprüngliche Bereich, in dem sich der Wert 72 befindet, ist nun nicht mehr zugänglich, da der Zeiger auf diesen Speicherbereich neu zugewiesen wurde. Es gibt keine Möglichkeit mehr, auf diesen Speicherbereich zuzugreifen, und man kann ihn auch nicht mehr freigeben, bevor das Programm beendet wird.

Das Codefragment sollte daher besser wie folgt formuliert werden:

1: unsigned short int * pZeiger = new unsigned short int;
2: *pZeiger = 72;
3: delete pZeiger;
4: pZeiger = new unsigned short int;
5: *pZeiger = 84;

Jetzt wird in Zeile 3 der Speicher, auf den pZeiger ursprünglich gezeigt hat, gelöscht und der Speicherbereich wird freigegeben.

Für jeden Aufruf von new sollte auch ein korrespondierender Aufruf von delete vorhanden sein. An jedem Punkt im Programm muß klar sein, welcher Zeiger einen Speicherbereich besitzt. Außerdem ist sicherzustellen, daß nicht mehr benötigter Speicher umgehend an den Heap zurückgegeben wird.

Objekte auf dem Heap erzeugen

So wie man einen Zeiger auf eine Ganzzahl erzeugen kann, läßt sich auch ein Zeiger auf ein beliebiges Objekt erzeugen. Wenn man ein Objekt vom Typ Cat (Katze) deklariert hat, kann man einen Zeiger auf diese Klasse deklarieren und ein Cat-Objekt auf dem Heap instantiieren, wie es auch auf dem Stack möglich ist. Die Syntax entspricht der für Integer-Objekte:

Cat *pCat = new Cat;

Dabei wird der Standardkonstruktor aufgerufen - der Konstruktor, der keine Parameter übernimmt. Der Aufruf des Konstruktors findet immer statt, wenn man ein Objekt - auf dem Stack oder dem Heap - erzeugt.

Objekte löschen

Beim Aufruf von delete für einen Zeiger zu einem Objekt auf dem Heap wird der Destruktor dieses Objekts aufgerufen, bevor die Freigabe des Speichers erfolgt. Damit kann die Klasse Aufräumungsarbeiten erledigen, genau wie bei Objekten, die auf dem Stack zerstört werden. Listing 8.5 demonstriert das Erzeugen und Löschen von Objekten auf dem Heap.

Listing 8.5: Objekte auf dem Heap erzeugen und löschen

1:    // Listing 8.5
2: // Objekte auf dem Heap erzeugen
3:
4: #include <iostream.h>
5:
6: class SimpleCat
7: {
8: public:
9: SimpleCat();
10: ~SimpleCat();
11: private:
12: int itsAge;
13: };
14:
15: SimpleCat::SimpleCat()
16: {
17: cout << "Konstruktor aufgerufen.\n";
18: itsAge = 1;
19: }
20:
21: SimpleCat::~SimpleCat()
22: {
23: cout << "Destruktor aufgerufen.\n";
24: }
25:
26: int main()
27: {
28: cout << "SimpleCat Frisky...\n";
29: SimpleCat Frisky;
30: cout << "SimpleCat *pRags = new SimpleCat...\n";
31: SimpleCat * pRags = new SimpleCat;
32: cout << "Loeschen pRags...\n";
33: delete pRags;
34: cout << "Beenden, Frisky geht...\n";
35: return 0;
36: }

SimpleCat Frisky...
Konstruktor aufgerufen.
SimpleCat * pRags = new SimpleCat...
Konstruktor aufgerufen.
Loeschen pRags...
Destruktor aufgerufen.
Beenden, Frisky geht...
Destruktor aufgerufen.

Die Zeilen 6 bis 13 deklarieren die rudimentäre Klasse SimpleCat. Zeile 9 deklariert den Konstruktor von SimpleCat, und die Zeilen 15 bis 19 enthalten dessen Definition. Der Destruktor von SimpleCat steht in Zeile 10, die zugehörige Definition in den Zeilen 21 bis 24.

Zeile 29 erzeugt das Objekt Frisky auf dem Stack und bewirkt damit den Aufruf des Konstruktors. Zeile 31 erzeugt auf dem Heap ein SimpleCat-Objekt, auf das pRags zeigt. Der Konstruktor wird erneut aufgerufen. Zeile 33 ruft delete auf pRags auf, und löst damit den Aufruf des Destruktors aus. Am Ende der Funktion verliert Frisky den Gültigkeitsbereich, und der Destruktor wird aufgerufen.

Auf Datenelemente zugreifen

Auf Datenelemente und Funktionen greift man für lokal erzeugte Cat-Objekte mit dem Punktoperator (.) zu. Um das Cat-Objekt im Heap anzusprechen, muß man den Zeiger dereferenzieren und den Punktoperator auf das Objekt anwenden, auf das der Zeiger verweist. Für einen Zugriff auf die Elementfunktion GetAge() schreibt man daher

(*pRags).GetAge();

Mit den Klammern stellt man sicher, daß pRags vor dem Zugriff auf GetAge() dereferenziert wird.

Da diese Methode umständlich ist, bietet C++ einen Kurzoperator für den indirekten Zugriff: den Elementverweis-Operator (->), der aus einem Bindestrich und einem unmittelbar folgenden Größer-als-Symbol besteht. C++ behandelt diese Zeichenfolge als ein einzelnes Symbol. Listing 8.6 demonstriert den Zugriff auf Elementvariablen und Funktionen von Objekten, die auf dem Heap erzeugt wurden.

Listing 8.6: Zugriff auf Datenelemente von Objekten auf dem Heap

1:     // Listing 8.6
2: // Zugriff auf Datenelemente von Objekten auf dem Heap
3:
4: #include <iostream.h>
5:
6: class SimpleCat
7: {
8: public:
9: SimpleCat() {itsAge = 2; }
10: ~SimpleCat() {}
11: int GetAge() const { return itsAge; }
12: void SetAge(int age) { itsAge = age; }
13: private:
14: int itsAge;
15: };
16:
17: int main()
18: {
19: SimpleCat * Frisky = new SimpleCat;
20: cout << "Frisky ist " << Frisky->GetAge() << " Jahre alt.\n";
21: Frisky->SetAge(5);
22: cout << "Frisky ist " << Frisky->GetAge() << " Jahre alt.\n";
23: delete Frisky;
24: return 0;
25: }

Frisky ist 2 Jahre alt.
Frisky ist 5 Jahre alt.

Zeile 19 instantiiert ein SimpleCat-Objekt auf dem Heap. Der Standardkonstruktor setzt das Alter auf 2. Zeile 20 ruft die Methode GetAge() auf. Da es sich hierbei um einen Zeiger handelt, wird der Elementverweis-Operator (->) für den Zugriff auf Datenelemente und Funktionen verwendet. In Zeile 21 steht der Aufruf von SetAge(), und in Zeile 22 findet ein erneuter Zugriff auf GetAge() statt.

Datenelemente auf dem Heap

Die Datenelemente einer Klasse können ebenfalls Zeiger auf Heap-Objekte deklarieren. Der Speicher kann im Konstruktor der Klasse oder einer ihrer Methoden reserviert und in ihrem Destruktor gelöscht werden, wie es Listing 8.7 verdeutlicht.

Listing 8.7: Zeiger als Datenelemente

1:  // Listing 8.7
2: // Zeiger als Datenelemente
3:
4: #include <iostream.h>
5:
6: class SimpleCat
7: {
8: public:
9: SimpleCat();
10: ~SimpleCat();
11: int GetAge() const { return *itsAge; }
12: void SetAge(int age) { *itsAge = age; }
13:
14: int GetWeight() const { return *itsWeight; }
15: void setWeight (int weight) { *itsWeight = weight; }
16:
17: private:
18: int * itsAge;
19: int * itsWeight;
20: };
21:
22: SimpleCat::SimpleCat()
23: {
24: itsAge = new int(2);
25: itsWeight = new int(5);
26: }
27:
28: SimpleCat::~SimpleCat()
29: {
30: delete itsAge;
31: delete itsWeight;
32: }
33:
34: int main()
35: {
36: SimpleCat *Frisky = new SimpleCat;
37: cout << "Frisky ist " << Frisky->GetAge() << " Jahre alt.\n";
38: Frisky->SetAge(5);
39: cout << "Frisky ist " << Frisky->GetAge() << " Jahre alt.\n";
40: delete Frisky;
41: return 0;
42: }

Frisky ist 2 Jahre alt.
Frisky ist 5 Jahre alt.

Die Klasse SimpleCat deklariert in den Zeilen 18 und 19 zwei Elementvariablen als Zeiger auf Integer. Der Konstruktor (Zeilen 22 bis 26) initialisiert die Zeiger auf einen Bereich im Heap und auf Standardwerte.

Der Destruktor (Zeilen 28 bis 32) räumt den reservierten Speicher auf. Da es sich um den Destruktor handelt, gibt es keinen Grund, diesen Zeigern Null zuzuweisen, da sie danach nicht mehr zugänglich sind. Hier kann man ausnahmsweise die Regel brechen, daß gelöschte Zeiger den Wert Null erhalten sollten, obwohl die Einhaltung dieser Regel auch nicht stört.

Die aufrufende Funktion - in diesem Fall main() - ist nicht davon unterrichtet, daß itsAge und itsWeight Zeiger auf Speicher im Heap sind. main() ruft weiterhin GetAge() und SetAge() auf, und die Einzelheiten der Speicherverwaltung werden - wie es sich gehört - in der Implementierung der Klasse versteckt.

Beim Löschen von Frisky in Zeile 40 wird der Destruktor aufgerufen. Der Destruktor löscht alle zugehörigen Elementzeiger. Wenn diese ihrerseits auf Objekte anderer benutzerdefinierter Klassen zeigen, werden deren Destruktoren ebenfalls aufgerufen.

Wenn ich auf dem Stack ein Objekt deklariere mit Elementvariablen auf dem Heap, was liegt dann auf dem Stack und was auf dem Heap? Zum Beispiel

#include <iostream.h>

class SimpleCat
{
public:
SimpleCat();
~SimpleCat();
int GetAge() const { return *itsAge; }
// andere Methoden

private:
int * itsAge;
int * itsWeight;
};

SimpleCat::SimpleCat()
{
itsAge = new int(2);
itsWeight = new int(5);
}

SimpleCat::~SimpleCat()
{
delete itsAge;
delete itsWeight;
}

int main()
{
SimpleCat Frisky;
cout << "Frisky ist " <<
Frisky.GetAge() <<
" Jahre alt\n";
Frisky.SetAge(5);
cout << "Frisky ist " <<
Frisky.GetAge() << " Jahre alt\n";
return 0;
}

Antwort: Auf dem Stack befindet sich die lokale Variable Frisky. Diese Variable enthält zwei Zeiger, die beide jeweils 4 Byte Speicherplatz auf dem Stack einnehmen und die Speicheradresse eines Integers auf dem Heap enthalten. So sind in dem Beispiel 8 Byte auf dem Stack und 8 Byte auf dem Heap belegt.

Sofern nicht gerade ein triftiger Grund vorliegt, wäre es allerdings ziemlich dumm, in einem richtigen Programm ein Objekt Cat einzurichten, das seine eigenen Elemente als Referenz hält. In obigem Beispiel gibt es dafür keinen Grund (außer, daß es sich um eine Demonstration handelt), aber es gibt andere Fälle, in denen eine solche Konstruktion durchaus sinnvoll wäre.

Dies bringt die Frage auf: Was wollen wir eigentlich erreichen? Zur Erinnerung: Am Anfang aller Programmierarbeit steht der Entwurf. Und wenn Sie in Ihrem Entwurf ein Objekt vorgesehen haben, das sich auf ein zweites Objekt bezieht, wobei das zweite Objekt unter Umständen vor dem ersten ins Leben gerufen wird und auch nach dessen Zerstörung noch weiter besteht, dann muß das erste Objekt eine Referenz auf das zweite enthalten.

So könnte zum Beispiel das erste Objekt ein Fenster und das zweite Objekt ein Dokument sein. Das Fenster benötigt Zugriff auf das Dokument, kontrolliert jedoch nicht die Lebensdauer des Dokuments. Aus diesem Grunde muß das Fenster eine Referenz auf das Dokument enthalten.

Implementiert wird dies in C++ mit Hilfe von Zeigern oder Referenzen. Referenzen werden in Kapitel 9 behandelt.

Der this-Zeiger

Jede Elementfunktion einer Klasse verfügt über einen versteckten Parameter: den Zeiger this, der auf das eigene Objekt zeigt. Dieser Zeiger wird bei jedem Aufruf von GetAge() oder SetAge()als versteckter Parameter mit übergeben.

Daß es auch möglich ist, den Zeiger this explizit aufzurufen, veranschaulicht Listing 8.8.

Listing 8.8: Der Zeiger this

1:      // Listing 8.8
2: // Verwendung des this-Zeigers
3:
4: #include <iostream.h>
5:
6: class Rectangle
7: {
8: public:
9: Rectangle();
10: ~Rectangle();
11: void SetLength(int length) { this->itsLength = length; }
12: int GetLength() const { return this->itsLength; }
13:
14: void SetWidth(int width) { itsWidth = width; }
15: int GetWidth() const { return itsWidth; }
16:
17: private:
18: int itsLength;
19: int itsWidth;
20: };
21:
22: Rectangle::Rectangle()
23: {
24: itsWidth = 5;
25: itsLength = 10;
26: }
27: Rectangle::~Rectangle()
28: {}
29:
30: int main()
31: {
32: Rectangle theRect;
33: cout <<"theRect ist " << theRect.GetLength() <<" Meter lang.\n";
34: cout <<"theRect ist " << theRect.GetWidth() <<" Meter breit.\n";
35: theRect.SetLength(20);
36: theRect.SetWidth(10);
37: cout <<"theRect ist " << theRect.GetLength() <<" Meter lang.\n";
38: cout <<"theRect ist " << theRect.GetWidth() <<" Meter breit.\n";
39: return 0;
40: }

theRect ist 10 Meter lang.
theRect ist 5 Meter breit.
theRect ist 20 Meter lang.
theRect ist 10 Meter breit.

Die Zugriffsfunktionen SetLength()und GetLength()verwenden explizit den Zeiger this, um auf die Elementvariablen des Objekts Rectangle zuzugreifen. Dagegen arbeiten die Zugriffsfunktionen SetWidth() und GetWidth() ohne diesen Zeiger. Im Verhalten ist kein Unterschied festzustellen, obwohl die Syntax leichter zu verstehen ist.

Wäre das alles, wofür der Zeiger this gut ist, hätte man kaum Anlaß, sich mit diesem Zeiger zu beschäftigen. Gerade aber weil this ein Zeiger ist, kann er die Speicheradresse eines Objekts aufnehmen. In dieser Eigenschaft kann der Zeiger ein leistungsfähiges Werkzeug sein, und Sie werden in Kapitel 10, »Funktionen - weiterführende Themen«, wenn es um das Überladen von Operatoren geht, noch ein praktisches Beispiel hierzu sehen. Momentan genügt es zu wissen, daß es den Zeiger this gibt und daß er ein Zeiger auf das Objekt selbst ist.

Um das Erzeugen oder Löschen des Zeigers this braucht man sich nicht zu kümmern. Das erledigt der Compiler.

Vagabundierende Zeiger

Zu den schwer zu lokalisierenden Fehlerquellen gehören vagabundierende Zeiger. Derartige Zeiger entstehen, wenn man delete für den Zeiger aufruft - dabei den Speicher freigibt, auf den der Zeiger verweist - und dann vergißt, den Zeiger auf Null zu setzen. Wenn Sie danach versuchen, den Zeiger erneut zu verwenden, ohne eine Neuzuweisung vorzunehmen, läßt sich das Ergebnis nur schwer vorhersagen. Im besten Falle stürzt Ihr Programm ab.

Es verhält sich genauso, als ob die Firma Acme Mail Order umgezogen wäre und man weiterhin dieselbe Kurzwahltaste auf dem Telefon betätigt. Vielleicht passiert nichts Furchtbares - irgendein Telefon klingelt in einem verwaisten Warenhaus. Die Telefonnummer könnte aber auch einer Munitionsfabrik zugewiesen worden sein, und Ihr Anruf löst eine Explosion aus und bläst die ganze Stadt weg!

Man sollte also keinen Zeiger verwenden, nachdem man delete auf den Zeiger angewendet hat. Der Zeiger verweist dann immer noch auf den alten Speicherbereich, in dem der Compiler mittlerweile vielleicht schon andere Daten untergebracht hat. Die Verwendung des Zeigers kann zum Absturz des Programms führen. Der schlimmere Fall ist, daß das Programm noch unbekümmert läuft und erst einige Minuten später abstürzt - eine sogenannte Zeitbombe. Setzen Sie daher den Zeiger sicherheitshalber auf Null (0), nachdem Sie ihn gelöscht haben. Damit ist der Zeiger entwaffnet.

Listing 8.9 demonstriert die Entstehung eines vagabundierenden Zeigers.

Dies Programm erzeugt absichtlich einen vagabundierenden Zeiger. Führen Sie dieses Programm NICHT aus, es stürzt ab - wenn Sie Glück haben.

Listing 8.9: Einen vagabundierenden Zeiger erzeugen

1:     // Listing 8.9
2: // Ein vagabundierender Zeiger
3: typedef unsigned short int USHORT;
4: #include <iostream.h>
5:
6: int main()
7: {
8: USHORT * pInt = new USHORT;
9: *pInt = 10;
10: cout << "*pInt: " << *pInt << endl;
11: delete pInt;
12:
13: long * pLong = new long;
14: *pLong = 90000;
15: cout << "*pLong: " << *pLong << endl;
16:
17: *pInt = 20; // uh oh, der wurde geloescht!
18:
19: cout << "*pInt: " << *pInt << endl;
20: cout << "*pLong: " << *pLong << endl;
21: delete pLong;
22: return 0;
23: }

*pInt:   10
*pLong: 90000
*pInt: 20
*pLong: 65556

(Die Ausgabe kann bei Ihnen etwas andere Ergebnisse zeigen.)

Zeile 8 deklariert pInt als Zeiger auf USHORT, der auf einen neu eingerichteten (den von new allokierten) Speicher zeigt. Zeile 9 legt an dieser Speicherposition den Wert 10 ab und Zeile 10 gibt den Wert aus. Nachdem der Wert ausgegeben wurde, wird delete auf den Zeiger angewendet. Damit wird pInt zu einem sogenannten vagabundierenden Zeiger.

Zeile 13 deklariert einen neuen Zeiger, pLong, der ebenfalls auf einen von new allokierten Speicher weist. Zeile 14 weist pLong den Wert 90000 zu und Zeile 15 gibt diesen Wert aus.

Zeile 17 weist dem Speicher, auf den pInt zeigt, den Wert 20 zu. Allerdings zeigt pInt auf keine gültige Adresse mehr im Speicher, denn der Speicher, auf den pInt zeigte, wurde durch den Aufruf von delete freigegeben. Damit ist die Zuweisung eines Wertes an diese Speicherposition mit Sicherheit verhängnisvoll.

Zeile 19 gibt den Wert von pInt aus, der selbstverständlich 20 lautet. Zeile 20 gibt den Wert bei pLong, 20, aus. Der hat sich plötzlich in 65556 geändert. Zwei Fragen drängen sich auf:

Warum hat sich der Wert von pLong geändert, obwohl an pLong nicht gerührt wurde?

Wohin ist der Wert 20 verschwunden, als in Zeile 17 pInt verwendet wurde?

Sie haben sicher schon bemerkt, daß es sich hierbei um zwei verwandte Fragen handelt. Als in Zeile 17 ein Wert im Zeiger pInt ablegt wurde, hat der Compiler den Wert 20 an der Speicherposition abgelegt, auf die der Zeiger pInt zuvor gezeigt hatte. Dieser Speicher wurde jedoch in Zeile 11 freigegeben, und der Compiler hatte die Möglichkeit, diesen Speicher neu zuzuweisen. Bei der Erzeugung von pLong in Zeile 13 wurde diesem Zeiger die Speicherposition zugewiesen, auf die zuvor pInt gewiesen hat. (Dies muß nicht auf allen Computern so sein, da es davon abhängt, wo die Werte im Speicher abgelegt werden.) Dadurch, daß der Wert 20 der Position zugewiesen wurde, auf die pInt zuvor gezeigt hatte, wurde der Wert, auf den pLong zeigte, überschrieben. Dieses Überschreiben eines Speichers ist oft die unglückliche Folge, wenn man einen vagabundierenden Zeiger verwendet.

Dies ist ein besonders teuflischer Fehler, da der Wert, der sich geändert hat, gar nicht mit dem vagabundierenden Zeiger in Verbindung gebracht wird. Die Wertänderung von pLong war nur eine Nebenwirkung der fehlerhaften Verwendung von pInt. In einem großen Programm wäre ein solcher Fehler nur sehr schwer aufzuspüren.

Nur zum Spaß möchte ich Ihnen hier zeigen, wie es zu dem Wert 65556 an der Speicheradresse gekommen ist:

pInt zeigte auf eine bestimmte Speicheradresse, der man den Wert 10 zugewiesen hatte.

delete wurde auf pInt aufgerufen. Damit wurde dem Compiler mitgeteilt, daß diese Position jetzt für andere Werte frei ist. Dann wurde pLong eben diese Speicherposition zugewiesen.

pLong wurde der Wert 90000 zugewiesen. Der von mir verwendete Computer speicherte den 4-Byte-Wert von 90000 (00 01 5F 90) in vertauschter Byte-Reihenfolge. Der Wert wurde also als 5F 90 00 01 gespeichert.

pInt wurde der Wert 20 - oder 00 14 in hexadezimaler Schreibweise - zugewiesen. Da pInt immer noch auf die gleiche Adresse zeigte, wurden die ersten 2 Byte von pLong überschrieben, was einen Wert von 00 14 00 01 zur Folge hatte.

Der Wert von pLong wurde ausgegeben, wobei die Bytes wieder zurück in ihre korrekte Reihenfolge getauscht wurden. Das Ergebnis, 00 01 00 14 wurde dann von DOS in den Wert 65556 überführt.

Was Sie tun sollten

... und was nicht

Verwenden Sie new, um Objekte auf dem Heap zu erzeugen.

Verwenden Sie delete, um Objekte auf dem Heap zu zerstören und ihren Speicher wieder freizugeben.

Testen Sie, welcher Wert von new zurückgegeben wird.

Vergessen Sie nicht, für jede new-Anweisung ein delete-Anweisung mit aufzunehmen.

Vergessen Sie nicht, Zeigern, für die delete aufgerufen wurde, anschließend eine Null zuzuweisen.

Was ist der Unterschied zwischen einem Nullzeiger und einem vagabundierenden Zeiger?

Antwort: Wenn Sie einen Zeiger löschen, teilen Sie dem Compiler mit, den Speicher freizugeben. Der Zeiger selbst existiert dann allerdings noch. Er ist zu einem vagabundierenden Zeiger geworden.

Mit meinZeiger = 0; wandeln Sie den vagabundierenden Zeiger in einen Nullzeiger um.

Wenn Sie einen Zeiger, den Sie bereits mit delete gelöscht haben, noch einmal löschen, ist das Verhalten Ihres Programmes undefiniert. Das heißt, alles ist möglich - wenn Sie Glück haben, stürzt lediglich Ihr Programm ab. Wenn Sie einen Nullzeiger löschen, passiert dagegen nichts.

Die Verwendung eines vagabundierenden oder eines Nullzeigers (z.B. meinZeiger = 5;) ist illegal und kann zu einem Absturz führen. Ist es ein Nullzeiger, ist der Absturz sicher - ein weiterer Vorteil von Nullzeigern gegenüber vagabundierenden Zeigern. Wir ziehen vorhersehbare Abstürze vor, da sie einfacher zu debuggen sind.

Konstante Zeiger

Bei Zeigern kann man das Schlüsselwort const vor oder nach dem Typ (oder an beiden Stellen) verwenden. Die folgenden Beispiele zeigen gültige Deklarationen:

const int * pEins;
int * const pZwei;
const int * const pDrei;

pEins ist ein Zeiger auf eine konstante Ganzzahl. Der Wert, auf den er zeigt, läßt sich nicht über diesen Zeiger ändern.

pZwei ist ein konstanter Zeiger auf eine Ganzzahl. Die Ganzzahl kann man ändern, aber pZwei kann nicht auf etwas anderes zeigen.

pDrei ist ein konstanter Zeiger auf eine konstante Ganzzahl. Hier läßt sich weder der Wert ändern, auf den der Zeiger verweist, noch kann man pDrei durch eine erneute Zuweisung auf etwas anderes zeigen lassen.

Um diese Möglichkeiten auseinanderzuhalten, müssen Sie rechts des Schlüsselwortes const nachschauen, was als konstant deklariert wird. Steht dahinter der Typ, ist es der Wert, der konstant ist. Steht die Variable hinter dem Schlüsselwort const, ist die Zeigervariable selbst konstant.

const int * p1;  // die int-Variable, auf die verwiesen wird, ist konstant
int * const p2; // p2 ist konstant und kann nicht auf etwas anderes zeigen

Konstante Zeiger und Elementfunktionen

In Kapitel 6, »Klassen«, haben Sie gelernt, daß man das Schlüsselwort const auf Elementfunktionen anwenden kann. Für const deklarierte Funktionen erzeugt der Compiler eine Fehlermeldung, wenn in der Funktion versucht wird, die Daten des Objekts zu verändern.

Wenn man einen Zeiger auf ein konstantes Objekt deklariert, lassen sich mit diesem Zeiger einzig und allein konstante Methoden aufrufen. Listing 8.10 verdeutlicht diesen Sachverhalt.

Listing 8.10: Zeiger auf const-Objekt

1:      // Listing 8.10
2: // Zeiger und konstante Methoden
3:
4: #include <iostream.h>
5:
6: class Rectangle
7: {
8: public:
9: Rectangle();
10: ~Rectangle();
11: void SetLength(int length) { itsLength = length; }
12: int GetLength() const { return itsLength; }
13: void SetWidth(int width) { itsWidth = width; }
14: int GetWidth() const { return itsWidth; }
15:
16: private:
17: int itsLength;
18: int itsWidth;
19: };
20:
21: Rectangle::Rectangle()
22: {
23: itsWidth = 5;,
24: itsLength = 10;
25: }
26:
27: Rectangle::~Rectangle()
28: {}
29:
30: int main()
31: {
32: Rectangle* pRect = new Rectangle;
33: const Rectangle * pConstRect = new Rectangle;
34: Rectangle * const pConstPtr = new Rectangle;
35:
36: cout << "pRect Breite: " << pRect->GetWidth() << " Meter\n";
37: cout << "pConstRect Breite: " << pConstRect->GetWidth()
<< " Meter\n";
38: cout << "pConstPtr Breite: " << pConstPtr->GetWidth()
<< " Meter\n";
39:
40: pRect->SetWidth(10);
41: // pConstRect->SetWidth(10);
42: pConstPtr->SetWidth(10);
43:
44: cout << "pRect Breite: " << pRect->GetWidth() << " Meter\n";
45: cout << "pConstRect Breite: " << pConstRect->GetWidth()
<< " Meter\n";
46: cout << "pConstPtr Breite: " << pConstPtr->GetWidth()
<< " Meter\n";
47: return 0;
48: }

pRect Breite:      5 Meter
pConstRect Breite: 5 Meter
pConstPtr Breite: 5 Meter
pRect Breite: 10 Meter
pConstRect Breite: 5 Meter
pConstPtr Breite: 10 Meter

In den Zeilen 6 bis 19 steht die Deklaration der Klasse Rectangle. Zeile 14 deklariert die Elementmethode GetWidth() als const. Zeile 32 deklariert einen Zeiger auf Rectangle . Zeile 33 deklariert pConstRect als Zeiger auf das konstante Rectangle-Objekt und Zeile 34 pConstPtr als konstanten Zeiger auf Rectangle.

Die Zeilen 36 bis 38 geben die Breiten der Rectangle-Objekte aus.

Zeile 40 setzt die Breite des Rechtecks über pRect auf den Wert 10. In Zeile 41 käme eigentlich pConstRect zum Einsatz. Die Deklaration dieses Zeigers bezieht sich aber auf ein konstantes Rectangle-Objekt. Deshalb ist der Aufruf einer nicht als const deklarierten Elementfunktion unzulässig, und die Zeile wurde auskommentiert. In Zeile 42 ruft pConstPtr die Methode SetWidth(10) auf. pConstPtr wurde deklariert als konstanten Zeiger auf ein Rechteck. Der Zeiger ist also konstant und kann nicht auf irgend etwas anderes zeigen, aber das Rechteck ist nicht konstant, und somit geht dieser Aufruf in Ordnung.

Konstante this-Zeiger

Wenn man ein Objekt als const deklariert, hat man praktisch den Zeiger this als Zeiger auf ein konstantes Objekt deklariert. Einen const this-Zeiger kann man nur auf konstante Elementfunktionen anwenden.

Konstante Objekte und konstante Zeiger behandeln wir noch einmal morgen, wenn es um Referenzen auf konstante Objekte geht.

Was Sie tun sollten

Schützen Sie Objekte, die als Referenz übergeben wurden, mit const vor etwaigen Änderungen.

Übergeben Sie Objekte, die geändert werden sollen, als Referenz.

Übergeben Sie kleine Objekte, die nicht geändert werden sollen, als Wert.

Vergessen Sie nicht, allen Zeigern eine Null zuzuweisen, nachdem ein Aufruf an delete erfolgt ist.

Zeigerarithmetik

Zeiger können voneinander subtrahiert werden. Eine leistungsstarke Technik, die dies nutzt, besteht darin, mit zwei Zeigern auf verschiedene Elemente in einem Array zu zeigen und dann die Differenz zu nehmen, um festzustellen, wie viele Elemente zwischen den beiden liegen. Diese Technik kann beim Parsen von Zeichenarrays recht nützlich sein.

Listing 8.11: Wörter aus einem Zeichenstring parsen

1:    #include <iostream.h>
2: #include <ctype.h>
3: #include <string.h>
4: bool GetWord(char* string, char* word, int& wordOffset);
5: // Treiberprogramm
6: int main()
7: {
8: const int bufferSize = 255;
9: char buffer[bufferSize+1]; // enthält den ganzen String
10: char word[bufferSize+1]; // enthält das Wort
11: int wordOffset = 0; // am Anfang beginnen
12:
13: cout << "Geben Sie einen String ein: ";
14: cin.getline(buffer,bufferSize);
15:
16: while (GetWord(buffer,word,wordOffset))
17: {
18: cout << "Dieses Wort ausgelesen: " << word << endl;
19: }
20:
21: return 0;
22:
23: }
24:
25:
26: // Funktion zum Auslesen von Wörter aus einem String.
27: bool GetWord(char* string, char* word, int& wordOffset)
28: {
29:
30: if (!string[wordOffset]) // Ende des Strings?
31: return false;
32:
33: char *p1, *p2;
34: p1 = p2 = string+wordOffset; // zeigt auf das naechste Wort
35:
36: // beseitigt fuehrende Leerzeichen
37: for (int i = 0; i<(int)strlen(p1) && !isalnum(p1[0]); i++)
38: p1++;
39:
40: // feststellen, ob sich ein Wort ergibt
41: if (!isalnum(p1[0]))
42: return false;
43:
44: // p1 zeigt jetzt auf den Anfang des naechsten Wortes
45: // mit p2 auch darauf zeigen
46: p2 = p1;
47:
48: // setzt p2 an das Ende des Wortes
49: while (isalnum(p2[0]))
50: p2++;
51:
52: // p2 ist jetzt am Ende des Wortes
53: // p1 ist am Anfang des Wortes
54: // Laenge des Wortes ist die Differenz
55: int len = int (p2 - p1);
56:
57: // kopiert das Wort in den Puffer
58: strncpy (word,p1,len);
59:
60: // null beendet es
61: word[len]='\0';
62:
63: // sucht jetzt den Anfang des naechsten Wortes
64: for (int i = int(p2-string); i<(int)strlen
(string) && !isalnum(p2[0]); i++)
65: p2++;
66:
67: wordOffset = int(p2-string);
68:
69: return true;
70: }

Geben Sie einen String ein: dieser Code erschien zuerst im C++ Bericht 
Dieses Wort ausgelesen: dieser
Dieses Wort ausgelesen: Code
Dieses Wort ausgelesen: erschien
Dieses Wort ausgelesen: zuerst
Dieses Wort ausgelesen: im
Dieses Wort ausgelesen: C
Dieses Wort ausgelesen: Bericht

Zeile 13 fordert den Anwender auf, einen String einzugeben. Dieser String wird in Zeile 16 zusammen mit einem Puffer für das erste Wort und der Integer-Variable WordOffset, die in Zeile 11 mit 0 initialisiert wurde, an GetWord() weitergegeben. Die von GetWord() zurückgegebenen Wörter werden nacheinander ausgegeben, bis GetWord() false zurückliefert.

Jeder Aufruf von GetWord() verursacht einen Sprung zu Zeile 27. Zeile 30 prüft, ob der Wert von string[wordOffset]) Null ist. Dieser Fall tritt ein, wenn wir das Ende des Strings erreicht haben, woraufhin GetWord() false zurückliefert.

Zeile 33 deklariert zwei Zeichen-Zeiger, p1 und p2, die in Zeile 34 auf den String gerichtet werden, um wordOffset Positionen vom Anfang des Strings versetzt. Am Anfang ist wordOffset 0, so daß die Zeiger auf den Anfang des Strings verweisen.

Die Zeilen 37 und 38 durchlaufen den String und setzen p1 auf das erste alphanumerische Zeichen. Die Zeilen 41 und 42 stellen sicher, daß ein alphanumerisches Zeichen gefunden wurde. Falls nicht, wird false zurückgeliefert.

p1 zeigt jetzt auf den Anfang des nächsten Wortes und Zeile 46 setzt p2 auf die gleiche Position.

Die Zeilen 49 und 50 lassen nun den Zeiger p2 das Wort durchlaufen, bis er auf das erste nicht-alphanumerische Zeichen trifft. Damit zeigt p2 auf das Ende des Wortes, auf dessen Anfang p1 zeigt. Durch die Subtraktion p1 von p2 in Zeile 55 und die Typenumwandlung in einen Integer können wir die Länge des Wortes ermitteln. Daraufhin kopieren wir das Wort in den Puffer word. Dabei bildet die Position, auf die p1 zeigt, den Anfang, und die Länge des Wortes ist das Ergebnis der errechneten Differenz.

Zeile 61 hängt eine Null daran, um das Ende des Wortes zu markieren. p2 wird dann inkrementiert, um auf den Anfang des nächsten Wortes zu weisen, und der Offset dieses Wortes wird in die int-Referenz wordOffset abgelegt. Zum Schluß wird true zurückgeliefert, um anzuzeigen, daß ein Wort gefunden wurde.

Dies ist ein klassisches Beispiel für einen Code, den man am besten versteht, wenn man ihn im Debugger schrittweise ausführt.

Zusammenfassung

Zeiger bieten eine leistungsfähige Möglichkeit für den indirekten Zugriff auf Daten. Jede Variable hat eine Adresse, die sich über den Adreßoperator (&) ermitteln läßt. Die Adresse kann man in einem Zeiger speichern.

Die Deklaration eines Zeigers besteht aus dem Typ des Objekts, auf das der Zeiger verweist, gefolgt vom Indirektionsoperator (*) und dem Namen des Zeigers. Es empfiehlt sich, Zeiger zu initialisieren, so daß sie auf ein Objekt oder NULL (0) zeigen.

Auf den Wert, der an der in einem Zeiger gespeicherten Adresse abgelegt ist, greift man mit dem Indirektionsoperator (*) zu. Konstante Zeiger lassen sich nicht durch eine erneute Zuweisung auf ein anderes Objekt richten. Zeiger auf konstante Objekte gestatten keine Änderung der Objekte, auf die sie verweisen.

Mit dem Schlüsselwort new erzeugt man neue Objekte auf dem Heap. Die von new zurückgegebene Adresse legt man in einem Zeiger ab. Durch Anwendung von delete auf den Zeiger gibt man den Speicher wieder frei. Der betreffende Zeiger wird dabei aber nicht zerstört. Aus diesem Grund muß man den Zeiger nach Freigabe des Speichers erneut initialisieren.

Fragen und Antworten

Frage:
Warum sind Zeiger so wichtig?

Antwort:
Heute haben Sie erfahren, wie Zeiger die Adressen von Objekten im Heap aufnehmen. Sie können auch Zeiger als Argumente an Funktionen übergeben. In Kapitel 13 werden Sie zudem lernen, wie man Zeiger bei polymorphen Klassen einsetzt.

Frage:
Warum soll ich mir die Mühe machen, Objekte auf dem Heap zu deklarieren?

Antwort:
Objekte, die auf dem Heap angelegt wurden, bleiben auch nach Rückkehr der Funktion bestehen. Darüber hinaus kann man zur Laufzeit entscheiden, wie viele Objekte man benötigt, und muß die Objekte nicht im voraus deklarieren. Auf dieses Thema gehen wir morgen näher ein.

Frage:
Warum soll ich ein Objekt als const deklarieren, obwohl dadurch die Verwendungsmöglichkeiten eingeschränkt sind?

Antwort:
Als Programmierer möchte man den Compiler auch zur Fehlersuche einsetzen. So sind beispielsweise solche Fehler schwer zu finden, die darauf beruhen, daß eine aufgerufene Funktion ein Objekt ändert, diese Änderung aber mit der aufrufenden Funktion nicht im ursächlichen Zusammenhang steht. Durch die Deklaration des Objekts als const verhindert man derartige Änderungen.

Workshop

Der Workshop enthält Quizfragen, die Ihnen helfen sollen, Ihr Wissen zu festigen, und Übungen, die Sie anregen sollen, das eben Gelernte umzusetzen und eigene Erfahrungen zu sammeln. Versuchen Sie, das Quiz und die Übungen zu beantworten und zu verstehen, bevor Sie die Lösungen in Anhang D lesen und zur Lektion des nächsten Tages übergehen.

Quiz

  1. Mit welchem Operator bestimmt man die Adresse einer Variablen?
  2. Mit welchem Operator ermittelt man einen Wert, der an einer Adresse gespeichert ist, auf die ein Zeiger weist?
  3. Was ist ein Zeiger?
  4. Worin besteht der Unterschied zwischen der in einem Zeiger gespeicherten Adresse und dem Wert dieser Adresse?
  5. Worin besteht der Unterschied zwischen dem Indirektionsoperator und dem Adreßoperator?
  6. Worin besteht der Unterschied zwischen const int * pEins und int * const pZwei?

Übungen

  1. Was bewirken die folgenden Deklarationen:
  1. Wie würden Sie für eine Variable namens ihrAlter vom Typ unsigned short einen Zeiger deklarieren, der auf diese Variable verweist?
  2. Weisen Sie der Variable ihrAlter den Wert 50 zu. Verwenden Sie dazu den Zeiger, den Sie in Übung 2 deklariert haben.
  3. Schreiben Sie ein kleines Programm, das einen Integer und einen Zeiger auf diesen Integer deklariert. Weisen Sie dem Zeiger die Adresse des Integers zu. Verwenden Sie den Zeiger, um der Integer-Variable einen Wert zuzuweisen.
  4. FEHLERSUCHE: Was ist falsch an folgendem Code?
    #include <iostream.h>
    int main()
    {
    int *pInt;
    *pInt = 9;
    cout << "Der Wert von pInt: " << *pInt;
    return 0;
    }
  5. FEHLERSUCHE: Was ist falsch an folgendem Code?
    int main()
    {
    int SomeVariable = 5;
    cout << "SomeVariable: " << SomeVariable << "\n";
    int *pVar = & SomeVariable;
    pVar = 9;
    cout << "SomeVariable: " << *pVar << "\n";
    return 0;
    }


vorheriges KapitelInhaltsverzeichnisStichwortverzeichnisFeedbacknächstes Kapitel


© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH