C++-Programmierung/ Weitere Grundelemente/ Zeiger


Grundlagen zu Zeigern

Bearbeiten

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

Jede Variable wird in C++ an einer bestimmten Position im Hauptspeicher abgelegt. Diese Position nennt man Speicheradresse (engl. 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 Kompilierzeit bekannt, wo sie sich innerhalb des Speichers befinden (also kennt das Programm ihre Adresse).

Eine Adresse ist nichts anderes als eine Ganzzahl, die den Ort, die Nummer des ersten Bytes eines Objekts, angibt. Um eine solche Adressen-Ganzzahl zu speichern, ist ein Zeiger im Wesentlichen eine normale (Ganzzahl-)Variable. Zeiger(variablen) werden deklariert (und definiert), besitzen einen Gültigkeitsbereich, selbst wiederum eine Adresse und einen Wert. Dieser Wert, der Inhalt der Zeigervariable, ist zwar zunächst einmal eine Zahl, aber auch die Adresse einer anderen Variable oder eines Speicherbereichs. Bei der Deklaration einer Zeigervariable wird auch der Typ der Variable angegeben, auf die der Zeiger verweisen soll - also der Typ, den das Objekt besitzt, auf das der Zeiger zeigt.

#include <iostream>

int main() {
    int   Wert;      // eine int-Variable
    int *pWert;      // eine Zeigervariable für eine Variable vom Typ int
    int *pZahl;      // ein weiterer "Zeiger auf int"

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

    pWert = &Wert;   // Adressoperator '&' liefert die Adresse der Variable "Wert"
    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 *.


{
    *pWert += 5;
    *pZahl += 8;

    std::cout << "Wert = " << Wert << std::endl;
}
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.

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.

Zeiger und const

Bearbeiten

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 ersten 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 jener Stelle kann allerdings verändert werden. Natürlich sind auch beide Varianten in Kombination möglich.

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, Zeiger konstant
*p3Wert = Wert2;  // geht nicht, int 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.

Zeiger und Funktionen

Bearbeiten

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:

#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;
}
Ausgabe:
a: 7, b: 9
a: 9, b: 7

Diese Funktion hat natürlich einige Schwachstellen. Beispielsweise stürzt 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.

Funktionen, die einen Zeiger auf einen konstanten Datentyp erwarten, können auch mit einem Zeiger auf einen nicht-konstanten Datentyp aufgerufen werden. Das folgende Minimalbeispiel soll dies zeigen:

// Funktion, die einen Zeiger auf einen konstanten int erwartet
void function(int const* parameter){}

int main() {
    int* zeiger;      // Zeiger auf nicht-konstanten int
    function(zeiger); // Funktioniert
}

Probleme mit Zeigern auf Zeiger auf konstante Daten

Bearbeiten

Ein Problem, über das die meisten Programmierer irgendwann stolpern, ist die Übergabe eines Zeigers auf einen Zeiger auf nicht-konstante Daten an eine Funktion, die einen Zeiger auf einen Zeiger auf konstante Daten erwartet. Da sich das const hier nicht direkt auf die Daten des Zeigers bezieht, sondern erst auf die Daten des Zeigers, auf den der Zeiger zeigt, erlaubt der Compiler die Übergabe nicht. Die Lösung des Problems ist relativ einfach: teilen Sie dem Compiler mittels const_cast explizit mit, dass Sie die Umwandlung vornehmen möchten.

// Funktion, die einen Zeiger auf einen Zeiger einen konstanten int erwartet
void function(int const** parameter){}

int main() {
    int** zeiger;     // Zeiger auf Zeiger auf nicht-konstanten int
    function(zeiger); // Fehler: Typen sind inkompatibel

    // Lösung: explizites hinzucasten der Konstantheit
    function(const_cast< int const** >(zeiger));
}

Wenn Sie das Beispiel ausprobieren möchten, kommentieren Sie die fehlerhafte Zeile aus

In den meisten Fällen werden Sie einen Parametertyp haben, der etwa die Form Typ const*const* hat und Daten übergeben will, deren Typ Typ** lautet. Die Umwandlung der Konstantheit der Daten unmittelbar auf die der Zeiger zeigt, dass der Compiler dies automatisch vornehmen kann. Alle anderen Umwandlungen müssen Sie explizit mittels const_cast erledigen. Es ist somit egal, ob Sie const_cast< int const*const* > oder nur const_cast< int const** > für die explizite Umwandlung angeben.

Zeigerarithmetik

Bearbeiten

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:

#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;
}
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.

#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;
}
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*).

Negativbeispiele

Bearbeiten

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

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.

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
std::cout << "Wert enthält: " << 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.

void-Zeiger (anonyme Zeiger)

Bearbeiten

Eine besondere Rolle spielen die „Zeiger auf void“, die so genannten generischen Zeiger. Einer Zeigervariable vom Typ void* kann jeder beliebige Zeiger zugewiesen werden. void-Zeiger werden in der Sprache C beispielsweise 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. Ein Objekt kann nicht vom Typ void sein. Entsprechend ist es auch nicht möglich, einen void-Zeiger zu dereferenzieren. Das folgende kleine Codebeispiel ist praxisfern. Aber wie schon erwähnt gibt es in C++ nur noch sehr selten eine Notwendigkeit für void-Zeiger, und keine dieser seltenen Situationen könnte an dieser Stelle im Buch bereits erklärt werden. Die meisten C++-Programmierer werden in Ihrem gesamten Leben nicht in die Verlegenheit kommen, einen void-Zeiger wirklich verwenden zu müssen.

#include <iostream>

int main() {
    int  value   = 1;
    int* pointer = &value; // zeigt auf value

    void* void_pointer;
    void_pointer = pointer; // void_pointer zeigt wie pointer auf value

Sie können jetzt nicht ohne Weiteres auf *void_pointer zugreifen, um an den Wert von value zu kommen. Da es sich um einen Zeiger vom Typ void handelt, muss man diesen erst umwandeln (engl. to cast). In diesem Fall nach int*.


std::cout << *reinterpret_cast< int* >(void_pointer) << std::endl;
}

Der Ablauf dieser letzten Zeile noch einmal im Detail:

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

Funktionszeiger

Bearbeiten

Zeiger können nicht nur auf Variablen, sondern auch auf Funktionen verweisen. Die Deklaration eines solchen Funktionszeigers sieht im ersten Augenblick etwas schwierig aus, aber sie ist dennoch leicht zu merken. Sie schreiben einfach den Prototypen der Funktion, auf die ihr Zeiger verweisen soll und geben statt des Funktionsnamens den Variablennamen an. Selbigen stellen Sie einen Stern voran, um klar zu machen, dass es sich um einen Zeiger handelt, und um dem Compiler zu vermitteln, dass der Stern nicht zum Rückgabetyp gehört, fassen Sie ihn und den Variablennamen in Klammern ein. Das folgende kleine Beispiel zeigt die Verwendung:

#include <iostream>

int multiplication(int a, int b){
    return a*b;
}

int division(int a, int b){
    return a/b;
}

int main(){
    int (*rechenoperation)(int, int) = 0; // Anlegen eines Funktionszeigers, Initialisierung mit 0

    rechenoperation = &multiplication;
    std::cout << (*rechenoperation)(40, 8) << std::endl;
    rechenoperation = &division;
    std::cout << (*rechenoperation)(40, 8) << std::endl;
}
Ausgabe:
320
5

Man liest: rechenoperation ist ein Zeiger auf eine Funktion, die zwei ints übernimmt und einen int zurückgibt. Im Kapitel über Felder wird eine allgemein gültige Regel für das Lesen komplexer Datentypen vorgestellt. Wie Sie sehen wird der Zeigervariable nacheinander die Adresse zweier Funktionen zugewiesen, die dem Typ der Zeigervariable entsprechen. Eine für den Zeiger gültige Funktion muss also zwei ints übernehmen und einen int zurückgeben.

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. &multiplication() würde Ihnen die Adresse des von multiplication() 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.

Gleiches gilt bei der Dereferenzierung: ein expliziter Stern vor dem Funktionszeiger macht deutlich, dass es sich um eine Zeigervariable handelt. Während der Fehlersuche kann dies beim Lesen des Codes erheblich helfen. Die Klammern sind, wie schon bei der Deklaration, nötig, um dem Compiler mitzuteilen, dass sich der Stern nicht auf den Rückgabewert, sondern auf die Funktion bezieht.

int x;
x = (*rechenoperation)(40, 8); // ruft multiplication()  bzw. division() auf und weist x den Rückgabewert zu
x = rechenoperation(40, 8);    // alternative (nicht empfohlene) Syntax

In C werden Funktionszeiger oft für generische Funktionen verwendet, wofür es in C++ mit den Templates (auf Deutsch etwa „Vorlagen“) eine bessere Lösung gibt. Insbesondere werden in C++ statt Funktionszeigern auch oft Funktoren verwendet, welche aber erst später vorgestellt werden. Vorweggenommen sei an dieser Stelle bereits, dass ein Funktor etwas mehr Schreibaufwand benötigt als ein Funktionszeiger, dafür aber auch einiges kann, was mit einfachen Funktionszeigern nicht möglich ist.

Tipp

Verwenden Sie typedef um komplizierte Typen zu vereinfachen. Für unseren Zeigertyp könnte man etwa auch schreiben:

typedef int (*Zeigertyp)(int, int);    // Zeigertyp als Alias für den Funktionszeigertyp
Zeigertyp rechenoperation = 0; // Anlegen eines Funktionszeigers, Initialisierung mit 0
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 kennen lernen. Auch im Abschnitt „Speicherverwaltung“ werden Ihnen Zeiger noch einmal begegnen.