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.
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.
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.
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.
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).
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 erzeugenWenn 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 zuweisenBeachten Sie auch, daß C++ das gleiche Zeichen (
*
) als Multiplikationsoperator verwendet. Der Compiler erkennt aus dem Kontext, welcher Operator gemeint ist.
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
.
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.
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 s
hort.
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.)
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;
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.
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.
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«).
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 mandelete
erneut auf diesem Zeiger auf, stürzt das Programm ab! Setzen Sie den Zeiger daher nach dem Löschen auf0
(Null). Der Aufruf vondelete
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.
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 vondelete
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.
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.
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 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.
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.
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.
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.
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 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.
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
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.
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.
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.
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.
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.
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.
const int * pEins
und int * const pZwei
?
ihrAlter
vom Typ unsigned short
einen Zeiger deklarieren, der auf diese Variable verweist?
ihrAlter
den Wert 50 zu. Verwenden Sie dazu den Zeiger, den Sie in Übung 2 deklariert haben.
#include <iostream.h>
int main()
{
int *pInt;
*pInt = 9;
cout << "Der Wert von pInt: " << *pInt;
return 0;
}
int main()
{
int SomeVariable = 5;
cout << "SomeVariable: " << SomeVariable << "\n";
int *pVar = & SomeVariable;
pVar = 9;
cout << "SomeVariable: " << *pVar << "\n";
return 0;
}
© Markt&Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH