C++-Programmierung/ Weitere Grundelemente/ Zeiger

Aus Wikibooks

Wechseln zu: Navigation, Suche


Inhaltsverzeichnis

[Bearbeiten] Grundlagen zu Zeigern

Zeiger (engl. pointer) sind Variablen, die als Wert die Speicheradresse einer anderen Variablen enthalten. Zeiger können auf eine Variable eines Typs zeigen, oder auf einen Speicherbereich ohne Typ, deklariert mit dem Schlüsselwort void.

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

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

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

Crystal Clear app terminal.png
#include <iostream>

int main() {
    int   Wert;      // eine int-Variable
    int *pWert;      // eine Zeigervariable, zeigt auf einen int
    int *pZahl;      // ein weiterer "Zeiger auf int"

    Wert = 10;       // Zuweisung eines Wertes an eine int-Variable

    pWert = &Wert;   // Adressoperator '&' liefert die Adresse einer Variablen
    pZahl = pWert;   // pZahl und pWert zeigen jetzt auf dieselbe Variable

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

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

Crystal Clear app terminal.png
    *pWert += 5;
    *pZahl += 8;

    std::cout << "Wert = " << Wert << std::endl;
}
Crystal Clear app kscreensaver.png
Ausgabe:
Wert = 23

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

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

Symbol move vote.svg
Thema wird später näher erläutert…
Der Sinn von Zeigern erschließt sich vor allem Anfängern nicht unmittelbar. Das ändert sich allerdings schnell, sobald der dynamische Speicher besprochen wird.

[Bearbeiten] Zeiger und const

Das Schlüsselwort const kann auf zweierlei Arten in Verbindung mit Zeigern genutzt werden:

  • Um den Wert, auf den der Zeiger zeigt, konstant zu machen
  • Um den Zeiger selbst konstant zu machen

Im ersteren Fall kann der Zeiger im Laufe seines Lebens auf verschiedene Objekte zeigen, diese Werte können dann allerdings (über diesen Zeiger) nicht geändert werden. Im zweiten Fall kann der Zeiger nicht auf eine andere Adresse "umgebogen" werden. Der Wert an dieser Stelle kann allerdings verändert werden. Natürlich sind auch beide Varianten in Kombination möglich.

Crystal Clear action button cancel.png
int               Wert1;           // eine int-Variable
int               Wert2;           // noch eine int-Variable
int const *       p1Wert = &Wert1; // Zeiger auf konstanten int
int * const       p2Wert = &Wert1; // konstanter Zeiger auf int
int const * const p3Wert = &Wert1; // konstanter Zeiger auf konstanten int

p1Wert  = &Wert2; // geht
*p1Wert = Wert2;  // geht nicht, int konstant

p2Wert  = &Wert2; // geht nicht, Zeiger konstant
*p2Wert = Wert2;  // geht

p3Wert  = &Wert2; // geht nicht, int konstant
*p3Wert = Wert2;  // geht nicht, Zeiger konstant

Wie Sie sich sicher noch erinnern, gehört const immer zu dem was links von ihm steht. Es sei denn links steht nichts mehr, dann gehört es zu dem was rechts davon steht.

[Bearbeiten] Zeigerarithmetik

Zeiger sind keine Zahlen. Deshalb sind einige arithmetischen Operationen auf Zeiger nicht anwendbar und für die übrigen gelten andere Rechenregeln als in der Zahlenarithmetik. C++ kennt die Größe des Speicherbereichs, auf den ein Zeiger verweist. Inkrementieren (oder Dekrementieren) verändert die referenzierte Adresse unter Berücksichtigung dieser Speichergröße. Das folgende Beispiel soll den Unterschied zwischen Zahlen und Zeigerarithmetik verdeutlichen:

Crystal Clear app terminal.png
#include <iostream>

int main() {
    std::cout << "Zahlenarithmetik" << std::endl;

    int a = 1;   // a wird 1
    std::cout << "a: " << a << std::endl;

    a++;         // a wird 2
    std::cout << "a: " << a << std::endl;

    std::cout << "Zeigerarithmetik" << std::endl;

    int *p = &a; // Adresse von a an p zuweisen

    std::cout << "p verweist auf: " << p << std::endl;
    std::cout << " Größe von int: " << sizeof(int) << std::endl;

    p++;
    std::cout << "p verweist auf: " << p << std::endl;
}
Crystal Clear app kscreensaver.png
Ausgabe:
Zahlenarithmetik
a: 1
a: 2
Zeigerarithmetik
p verweist auf: 0x7fff3aa60090
 Größe von int: 4
p verweist auf: 0x7fff3aa60094
Die Ausgabe sieht bei Ihnen vermutlich etwas anders aus.

Wie Sie sehen, erhöht sich der Wert des Zeigers nicht um eins, sondern um vier, was genau der Größe des Typs entspricht auf den er zeigt: int. Auf einer Platform auf der int eine andere Größe hat würde natürlich entsprechend dieser Größe gezählt werden. Im nächsten Beispiel sehen Sie, wie ein Zeiger auf eine weitere Zeigervariable verweist, welche ihrerseits auf einen int-Wert zeigt.

Crystal Clear app terminal.png
#include <iostream>

int main() {
    int   a  = 1;
    int  *p  = &a; // Adresse von a an p zuweisen
    int **pp = &p; // Adresse von p an pp zuweisen
    std::cout << "pp verweist auf: " << pp << std::endl;
    std::cout << " Größe von int*: " << sizeof(int*) << std::endl;
    ++pp;
    std::cout << "pp verweist auf: " << pp << std::endl;
}
Crystal Clear app kscreensaver.png
Ausgabe:
pp verweist auf: 0x7fff940cb6f0
 Größe von int*: 8
pp verweist auf: 0x7fff940cb6f8
Die Ausgabe sieht bei Ihnen vermutlich etwas anders aus.

Wie Sie sehen hat ein Zeiger auf int im Beispiel eine Größe von 8 Byte. Die Größe von Datentypen ist allerdings architektur-, compiler- und systembedingt. pp ist vom Typ „Zeiger auf Zeiger auf int“, was sich dann in C++ als int** schreibt. Um also auf die Variable "hinter" diesen beiden Zeigern zuzugreifen, muss man **pp schreiben.

Es spielt keine Rolle, ob man in Deklarationen int* p; oder int *p; schreibt. Einige Programmierer schreiben den Stern direkt hinter den Datentyp ( int* p), andere schreiben ihn direkt vor Variablennamen ( int *p) und wieder andere lassen zu beidem ein Leerzeichen ( int * p). In diesem Buch wird die Konvention verfolgt, den Stern direkt vor Variablennamen zu schreiben wenn einer vorhanden ist ( int *p), andernfalls wird er direkt nach dem Datentyp geschrieben ( int*).

[Bearbeiten] Negativbeispiele

Zur Verdeutlichung zwei Beispiele, die nicht funktionieren, weil sie vom Compiler nicht akzeptiert werden:

Crystal Clear action button cancel.png
int * pWert;
int Wert;
pWert = Wert;     // dem Zeiger kann kein int zugewiesen werden
Wert  = pWert;    // umgekehrt natürlich auch nicht

Zeigervariablen erlauben als Wert nur Adressen auf Variablen. Daher kann einer Zeigervariable wie in diesem Beispiel kein Integer-Wert zugewiesen werden.

Im Folgenden wollen wir den Wert, auf den pWert zeigt, inkrementieren. Einige der Beispiele bearbeiten die Adresse, die pWert enthält. Erst das letzte Beispiel verändert tatsächlich den Wert, auf den pWert zeigt. Beachten Sie, dass jede Codezeile ein Einzelbeispiel ist.

Crystal Clear app terminal.png
int Wert = 0;
int *pWert = &Wert; // pWert zeigt auf Wert

pWert += 5;         // ohne Dereferenzierung (*pWert) verändert man die Adresse, auf die  
                    // der Zeiger verweist, und nicht deren Inhalt
std::cout << pWert; // es wird nicht die Zahl ausgegeben, auf die pWert
                    // zeigt, sondern deren (veränderte) Adresse
printf("Wert enthält: %d", pWert);    // gleiche Ausgabe

pWert++;            // Diese Operation verändert wiederum die Adresse, da nicht dereferenziert wird.
*pWert++;           // Auf diese Idee kommt man als nächstes. Doch auch das hat nicht den
                    // gewünschten Effekt. Da der (Post-)Inkrement-Operator vor dem Dereferenzierungs-
                    // operator ausgewertet wird, verändert sich wieder die Adresse.
(*pWert)++;         // Da der Ausdruck in der Klammer zuerst ausgewertet wird, erreichen wir
                    // diesmal den gewünschten Effekt: Eine Änderung des Wertes.

[Bearbeiten] void-Zeiger (anonyme Zeiger)

Eine besondere Rolle spielen die „Zeiger auf void“, die so genannten generischen Zeiger. Einem Zeiger vom Typ void* kann jeder beliebige Zeiger zugewiesen werden. void-Zeiger werden in der Sprache C z.B. bei der dynamischen Speicherverwaltung verwendet. In C++ kommt man weitgehend ohne sie aus. Vermeiden Sie Zeiger auf void wenn Sie eine andere Möglichkeit haben.

Eine Variable kann nicht vom Typ void sein. Daher würde folgende Zeile zu einem Fehler führen:

Crystal Clear action button cancel.png
void variable;

Sie können einer Variable, die auf void zeigt, einen beliebigen Zeiger zuweisen. Deshalb werden solche Variablen meist für Zeiger verwendet, dessen Typ noch nicht feststeht und sich erst im Laufe des Programmes ergibt oder aber als temporärer Speicher mit wechselnden Zeigertypen.

Crystal Clear app terminal.png
#include <iostream>

int main() {
    int intValue    = 1;
    int *intPointer = &intValue; // zeigt auf intValue

    void *voidPointer;
    voidPointer = intPointer; // voidPointer zeigt auf intPointer

Sie können jetzt nicht ohne Weiteres auf *voidPointer zugreifen, um an die Adresse von intValue zu bekommen. Da es sich um einen Zeiger, vom Typ void handelt, muss man diesen erst casten. In diesem Fall nach int*.

Crystal Clear app terminal.png
    std::cout << *reinterpret_cast<int*>(voidPointer) << std::endl;
}

Ablauf im Detail:

  1. Zeiger *voidPointer ist vom Typ void und zeigt auf intPointer
  2. reinterpret_cast<int*>, Zeiger ist vom Typ int*
  3. Zeiger dereferenzieren ( *), um Wert zu erhalten

[Bearbeiten] Zeiger und Funktionen

Wenn Sie einen Zeiger als Parameter an eine Funktion übergeben, können Sie den Wert an der übergebenen Adresse ändern. Eine Funktion, welche die Werte zweier Variablen vertauscht, könnte folgendermaßen implementiert werden:

Crystal Clear app terminal.png
#include <iostream>

void swap(int *wert1, int *wert2) {
    int tmp;
    tmp    = *wert1;
    *wert1 = *wert2;
    *wert2 = tmp;
}

int main() {
    int a = 7, b = 9;

    std::cout << "a: " << a << ", b: " << b << std::endl;
    swap(&a, &b);
    std::cout << "a: " << a << ", b: " << b << std::endl;
}
Crystal Clear app kscreensaver.png
Ausgabe:
a: 7, b: 9
a: 9, b: 7

Diese Funktion hat natürlich einige Schwachstellen. Beispielsweise stürtzt sie ab, wenn ihr ein Nullzeiger übergeben wird. Aber sie zeigt, dass es mit Zeigern möglich ist, den Wert einer Variable außerhalb der Funktion zu verändern. In Kürze werden Sie sehen, dass sich dieses Beispiel besser mit Referenzen lösen lässt.

[Bearbeiten] Funktionszeiger

Zeiger können nicht nur auf Variablen, sondern auch auf Funktionen verweisen.

Crystal Clear app terminal.png
int (*pFunc)(int);   // Anlegen eines Funktionszeigers
int (Klasse::*pFunc)(int);   // Anlegen eines Funktionszeigers auf eine Memberfunktion

Um einen Zeiger auf eine Funktion zu deklarieren, benutzen Sie den Prototyp der Funktion und ersetzen dort den Funktionsnamen durch (*Variablenname). Das ganze wird gelesen als: pFunc ist ein Zeiger auf eine Funktion, die einen int übernimmt und einen int zurückgibt. Ohne die Klammern um den Variablennamen müsste man lesen: pFunc ist eine Funktion, die einen int übernimmt und einen Zeiger auf int zurückgibt.

Dem Zeiger kann die Adresse einer typentsprechenden Funktion zugewiesen werden. Typentsprechend heißt so viel wie: Übernimmt einen int-Wert als Argument und gibt einen int-Wert zurück. Die Funktion muss also einen ihrer Zeigervariablen entsprechenden Prototyp besitzen.

Crystal Clear app terminal.png
int sqr(int x) {   // Prototyp der Funktion f
    return x * x;
}

pFunc = & sqr;   // Adresse der Funktion sqr() der Variable pFunc zuweisen
pFunc = & Klasse::sqr;   // bei Verwendung für Memberfunktionen muss hier die Klasse angegeben werden

Um die Adresse der Funktion zu erhalten, müssen Sie den Adressoperator auf den Funktionsnamen anwenden. Beachten Sie, dass die Klammern, die Sie zum Aufruf einer Funktion immer setzen müssen, hier keinesfalls gesetzt werden dürfen. &sqr() würde Ihnen die Adresse, des von sqr() zurückgelieferten Objekts beschaffen. Es sei auch darauf hingewiesen, dass der Adressoperator nicht zwingend zum Ermitteln der Funktionsadresse notwendig ist. Sie sollten Ihn aus Gründen der Übersicht allerdings immer mitschreiben.

Crystal Clear app terminal.png
int x;
x = (*pFunc)(2); // ruft sqr() auf und weist x den Rückgabewert zu
x = pFunc(2);    // alternative (nicht empfohlene) Syntax

x = (*this.*pFunc)(2) // auch hier die Variante für eine Memberfunktion

Auch für das Aufrufen einer Funktion über einen Funktionszeiger gibt es zwei Möglichkeiten. Sie können den Zeiger erst dereferenzieren, dann benötigen Sie Klammern um die Dereferenzierung, damit nicht das zurückgegebene Objekt dereferenziert wird, oder Sie rufen die Funktion mit der gleichen Syntax auf, über die Sie dies bei einem direkten Aufruf der Funktion tun würden.

Die Syntax der zweiten Variante ist einfacher, allerdings wird dabei nicht deutlich, dass eine Funktion über einen Zeiger aufgerufen wird. Hier gehen die Meinungen darüber, welche Variante besser ist, auseinander. Fakt ist jedoch, dass die erste Variante eindeutiger das Geschehen dokumentiert.

Typisches Beispiel für den Einsatz von Funktionszeigern stellt eine Sortierroutine dar, der man die Vergleichsfunktion als Argument übergibt. Ein ausführliches (englischsprachiges) Tutorial über Funktionszeiger finden Sie unter http://www.newty.de/fpt/index.html.

[Bearbeiten] Zeiger und Referenzen und Klassen

Symbol move vote.svg
Thema wird später näher erläutert…
Im Zusammenhang mit Klassen werden uns weitere Arten von Zeigern begegnen:
  • Zeiger auf statische Datenelemente
  • Zeiger auf Elementfunktionen.
Beide werden Sie zu einem späteren Zeitpunkt noch kennenlernen.

[Bearbeiten] Löschen von Zeigern

Zeiger müssen prinzipiell nicht gelöscht werden, weil diese nur Verweise auf Adressbereiche sind. Es ist dennoch möglich, mit delete einen Zeiger zu löschen. Dabei sollte man beachten, dass der Zeiger seinen Gültigkeitsbereich verliert. Man gibt nur den Speicher frei, auf den der Zeiger verweist. Man kann dem Zeiger aber trotzdem noch Werte zuweisen. delete sollte nicht aufgerufen werden, wenn ein Zeiger auf einen nicht-vorhandenen Bereich zeigt, da dies das Programm zum Absturz bringen kann. Daher sollte nach dem Aufruf von delete, der Wert des Zeigers auf 0 gesetzt werden, weil delete bei Null-Zeigern keinen Fehler erzeugt.


Persönliche Werkzeuge