Kapitel 6 - Zeiger und Referenzen

6.1 Einführung
6.2 Referenzen
6.3 Zeiger (Pointer)
6.4 Abgeleitete Typen
6.5 Objekte und L-Werte
6.6 Eine Referenz zurückgeben
6.7 Arrays und Zeiger
6.8 Der new - Operator
6.9 Der delete - Operator
6.10 Dynamische Arrays
6.11 const zusammen mit Zeigern verwenden
6.12 Arrays mit Zeigern und Zeiger auf Arrays
6.13 Zeiger auf Zeiger
6.14 Zeiger auf Funktionen
6.15 NUL, NULL und VOID

Beispiel 6.1 Zeigerwerte ausgeben
Beispiel 6.2: Referenzen verwenden
Beispiel 6.3 Referenzen sind Aliase
Beispiel 6.4 Zeiger sind Adressen
Beispiel 6.5: Zeiger dereferenzieren
Beispiel 6.6: Referenzieren ist das Gegenteil von Dereferenzieren
Beispiel 6.7: Eine Referenz zurückgeben
Beispiel 6.8: Verwendung einer Funktion im Arraysubscript
Beispiel 6.9: Traversieren eines Arrays mit einem Zeiger
Beispiel 6.10: Adressen von Arrayelementen untersuchen
Beispiel 6.11: Mustererkennung (Pattern Matching)
Beispiel 6.12: Einsatz dynamischer Arrays
Beispiel 6.13: Konstante Zeiger, Zeiger auf Konstanten und konstante Zeiger auf Konstanten
Beispiel 6.14: Indirektes Bubble-Sort

6.1 Einführung

Bei der Deklaration einer Variablen werden drei grundlegende Attribute mit dieser Variablen verknüpft:
  1. ihr Name
  2. ihr Typ und
  3. ihre Adresse im Arbeitsspeicher.
Die Deklaration

int n;


verknüpft zum Beispiel den Namen n, den Typ int und die Adresse der Speicherstelle, an der der Wert von n gespeichert wird, mit einer Variablen. Wenn wir annehmen, dass die Adresse 0x3fffd14 (dabei handelt es sich um eine hexadezimale Notation) lautet, dann lässt sich der Vorgang wie folgt veranschaulichen:


Das Kästchen stellt die Speicherposition der Variablen dar. Der Name der Variablen steht links, die Adresse über und der Typ unter dem Kästchen.

Wenn der Wert der Variablen bekannt ist, dann wird er im Kästchen wiedergegeben:


Auf den Variablenwert wird über den Namen zugegriffen. Der Wert von n lässt sich zum Beispiel mit dieser Anweisung ausgeben:

cout << n;


Auf die Adresse einer Variablen können Sie mit Hilfe des Adressoperators & zugreifen. Die Adresse von n lässt sich zum Beispiel mit dieser Anweisung ausgegeben:

cout << &n;


Der Adressoperator
Er hat die Vorrangebene 15 und befindet sich damit auf einer Ebene mit dem Operator für den logischen NOT-Operator ! und dem Präfix-Inkrementoperator ++.

Beispiel 6.1: Zeigerwerte ausgeben


// Dieses Beispiel zeigt, wie sich sowohl der Wert als auch
// die Adresse einer Variablen ausgeben lassen:


int main()
{
      int n = 33;
      cout << "n = " << n << endl; // Wert von n ausgeben
      cout << "&n = " << &n << endl; // Die Adresse von n ausgeben

getch();
return 0;
}

n = 33
0012FF88


Dass es sich bei der zweiten Ausgabe 0012FF88 um eine Adresse handelt, können Sie am Präfix "00" des Hexadezimalformats erkennen. Diese Adresse entspricht der Dezimalzahl 1245064.

Die Adresse einer Variablen auf diese Art und Weise auszugeben ist nicht besonders nützlich. Der Adressoperator & bietet andere wichtige Einsatzmöglichkeiten. Einen haben Sie bereits im Kapitel 4 kennengelernt:

Der Adressoperator & kennzeichnet Referenzparameter in Funktionsdeklarationen.


Dieser Einsatzzweck steht mit einem anderen in enger Verbindung:

der Deklaration von Referenzvariablen

6.2 Referenzen

Eine Referenz ist ein Alias, ein Synonym für eine andere Variable.

Sie wird deklariert, indem man dem Referenztyp das logische Und-Zeichen & (Ampersand) nachstellt.

Beispiel 6.2: Referenzen verwenden

int main()
{
      int n = 33;
      int& r = n;    // r ist eine Referenz auf n

      cout << "n = " << n << ", r = " << r << endl;
      --n;
      cout << "n = " << n << ", r = " << r << endl;
      r *=2;
      cout << "n = " << n << ", r = " << r << endl;

getch();
return 0;
}

n = 33, r = 33
n = 32, r = 32
n = 64, r = 64


Die beiden Bezeichner n und r sind verschiedene Namen für diesselbe Variable:

Sie haben immer denselben Wert


Beispiel 6.3: Referenzen sind Aliase

// Hier wird gezeigt, dass r und n diesselbe Speicheradresse haben:

int main()
{
      int n = 33;
      int& r = n;    // r ist eine Referenz auf n

      cout << "&n = " << &n << ", &r = " << &r << endl;
getch();
return 0;
}

&n = 0012FF88, &r = 0012FF88


Das folgende Diagramm verdeutlicht die Funktionsweise von Referenzen:


Der Wert 33 wird nur ein einziges Mal gespeichert. Die beiden Bezeichner n und r sind symbolische Namen für diesselbe Speicherstelle 0012FF88.
Referenzen müssen wie Konstanten bei ihrer Deklaration initialisiert werden. Diese Voraussetzung sollte einsichtig sein: Referenzparameter wurden in Kapitel 4 für Funktionen definiert. Nun werden wir sehen, dass sie genauso aus Referenzvariablen fungieren: Im wesentlichen sind sie Synonyme für andere Variablen. Ein Referenzparameter für eine Funktion ist wirklich nur eine Referenzvariable mit einem auf die Funktion beschränkten Gültigkeitsbereich.
Wir haben bereits erfahren, dass das Ampersand-Zeichen & in C++ für verschiedene Zwecke eingesetzt wird. All diese Einsatzmöglichkeiten sind Variationen desselben Sachverhaltes:

Das Symbol & verweist auf eine Adresse, an der ein Wert gespeichert ist.

6.3 Zeiger (Pointer)

Eine effiziente Programmlogik erfordert es häufig, daß nicht mit den Daten selbst, sondern mit den Adressen der Daten im Hauptspeicher gearbeitet wird. Beispiele für sind
Ein Zeiger ist eine Variable, welche eine Speicheradressen enthält.


Stop. Lesen Sie das bitte noch einmal: Ein Zeiger ist eine Variable. Sie wissen, dass eine Variable einen Wert aufnehmen kann. Eine Integer-Variable nimmt eine ganze Zahl auf. Eine Zeichenvariable speichert einen Buchstaben. Ein Zeiger ist eine Variable, welche eine Speicheradresse aufnimmt.

Jede Variable eines bestimmten Typs befindet sich an einer eindeutig adressierbaren Speicherstelle. Die folgende Abbildung zeigt die Speicherung der Integer-Variablen var und die Speicherung der Zeiger-Variablen ptr. Wie Sie sehen, ist die Adressen der Integer-Variablen var in der Zeiger-Variablen ptr enthalten.


Die Adressierung des Speichers unterscheidet sich bei den verschiedenen Computer-Typen. Normalerweise braucht der Programmierer die konkrete Adresse einer bestimmten Variablen nicht zu kennen, da sich der Compiler um die einzelheiten kümmert.

Der Referenzoperator & gibt die Speicheradresse der Variablen zurück, auf die er angewendet wird. Wir haben diesen Umstand in Beispiel 6.1 Zeigerwerte ausgeben
zur Ausgabe der Adresse genutzt. Wir können die Adresse auch in einer anderen Variablen speichern. Variablen, die Adressen speichern, sind vom Typ Zeiger (Pointer).
Wenn die Variable den Typ int hat, dann muss die Zeigervariable zum Typ "Zeiger auf int" gehören, der als

int*

geschrieben wird.

Beispiel 6.4: Zeiger sind Adressen

int main()
{

      int n = 33;

      int* p = &n; // p hält die Adresse von n
      cout << "n = " << n << ", &n = " << &n << ", p = " << p << endl;
      cout << "&p = " << &p << endl;

getch();
return 0;
}

n = 33, &n = 0012FF88, p = 0012FF88
&p = 0012FF88


Die Zeigervariable p und der Ausdruck &n haben denselben Typ (Zeiger auf int) und denselben Wert (0012FF88). dieser Wert befindet sich an der Speicherstelle 0012FF88:



Beim Wert eines Zeigers handelt es sich um eine Adresse. Diese Adresse hängt von den jeweiligen Gegebenheiten des Computers ab, auf dem das Programm läuft. Meist ist der tatsächliche Wert dieser Adresse (0012FF88) für die Belange des Programmierers nicht relevant. Daher werden die bisher gezeigten Diagramme wie folgt dargestellt:



Diese Zeichnung stellt die wesentlichen Merkmale von p und n dar, einen Zeiger kann man sich als "Verweis" vorstellen: Er gibt darüber Auskunft, wo sich andere Werte befinden.

Um den Wert, auf den ein Zeiger verweist, zu ermitteln, muss man den Zeiger p häufig eigenständig verwenden. Das nennt man

"den Zeiger dereferenzieren"
.

Dazu verwendet man einfach das Sternchen-Symbol * (Asterisk) als Operator auf den Zeiger an.

Beispiel 6.5: Zeiger dereferenzieren

int main()
{

      int n = 33;

      int* p = &n;     // p zeigt auf n
      cout << "*p = " << *p << endl;

getch();
return 0;
}

*p = 33


Damit wird deutlich, dass *p ein Alias für n ist.

Der Adressoperator & und der Dereferenzoperator * (auch Indirektionsoperator) sind Gegenteil voneinander:

Es gilt n == *p, wenn p == &n


Das lässt sich auch in der Form n == *&n und p == &*p ausdrücken.

Beispiel 6.6: Referenzieren ist das Gegenteil von Dereferenzieren

int main()
{

      int n = 33;

      int* p = &n;     // p zeigt auf n
      int& r = *p;     // r ist eine Referenz für n
      cout << "r = " << r << endl;

getch();
return 0;
}

r = 33


6.4 Abgeleitete Typen

Im Beispiel 6.6 hat p den Typ "Zeiger auf int", und r hat den Typ "Referenz auf int". Diese Typen werden vom Typ int abgeleitet. Genauso wie Arrays, Konstanten und funktionen sind es abgeleitete Typen.

Es folgen nun einige Deklarationen abgeleiteter Typen:

int& r = n // r hat den Typ Referenz auf int
int* p = &n // p hat den Typ Zeiger auf int
int a[] = {33,66} // a hat den Typ Array aus int-Werten
const int C = 33 // C hat den Typ const int
int f() = { return 33; }; // f hat den Typ Funktion gibt int zurück

C++ - Typen werden entweder als fundamental oder abgeleitet klassifiziert. Zu den fundamentalen Typen zählen die Aufzählungstypen und alle nummerischen Typen. Alle abgeleiteten Typen basieren auf anderen Typen. Eine Variable, die laut Deklaration zu einem der oben dargestellten Typen (Konstante, Array, Zeiger und Funktion) zählt, basiert auf einem einzigen fundamentalen Typ.

Einen abgeleiteten Typ, der auf mehr als einem fundamentalen Typ aufbaut, nennt man strukturierten Typ. Zu diesen zählen die soäter noch vorzustellenden Typen struct, union und class.

6.5 Objekte und L-Werte

Ein Objekt ist ein Speicherbereich.

Ein L-Wert ist ein Ausdruck, der sich auf ein Objekt oder eine Funktion bezieht.


Ursprünglich bezogen sich die Begriffe "L-Wert" und "R-Wert auf die Teilausdrücke auf der linken bzw. rechten Seite von Zuweisungen. Mittlerweile wird "L-Wert" (Ivalue) allgemeiner verwendet.

Das einnfachste Beispiel für L-Werte sind Namen von Objekten, d.h. Variablen:

        int n;

        n = 44;         // n ist ein L-Wert

Das einfachste Beispiel für etwas, das kein L-Wert ist, sind Literale:

        44 = n;          // FEHLER: 44 ist ein L-Wert

Symbolische Konstanten sind aber L-Werte,

        const int MAX = 65535;          // MAX ist ein L-Wert

auch wenn sie nicht auf der linken Seite von Zuweisungen stehen können:

        MAX = 21024;          // FEHLER: MAX ist eine Konstante

Andere Beispiele modifizierbarer L-Werte umfassen Subscript-Variablen und dereferenzierte Zeiger:

     int a[8];
     a[5] = 22;     // a[5] ist ein modifizierter Wert<BR>
     int* p = &n;
     *p = 77;      // *p ist ein modifizierter Wert

Andere Beispiele nicht modifizierbarer L-Werte sind Arrays, Funktionen und Referenzen.

Im allgemeinen sind alle beliebigen Teilausdrücke L-Werte, wenn Sie auf deren Adressen zugreifen können.

Da Referenzvariablen bei ihrer Deklaration Adressen erfordern, bezeichnet die C++Syntaxanforderung einer solchen Deklaration einen L-Wert:

type& refname = lvalue;


Hier handelt es sich zum Beispiel um eine zulässige Deklaration einer Referenz:

int& r = n;    // OK: n ist ein L-Wert

Die folgenden Deklarationen sind aber im Gegensatz dazu nicht zulässig:

     int& r = 44;            // FEHLER: 44 ist kein L-Wert
     int& r = n++;          // FEHLER: n++ ist kein L-Wert
     int& r = cube(n);     // FEHLER: cube(n) ist kein L-Wert

6.6 Eine Referenz zurückgeben

Der Rückgabewert einer Funktion kann eine Referenz sein, sofern es sich beim zurückgegebenen Wert um einen L-Wert handelt, der nicht lokal zur Funktion ist. Diese Einschränkung bedeutet, dass der zurückgegebene Wer eine Referenz auf einen L-Wert ist, der auch nach Beendigung der Funktion weiterhin existiert. Demzufolge lässt sich dieser zurückgegebene Wert einsetzen, zum Beispiel auch auf der linken Seite einer Zuweisung.

Beispiel 6.7: Eine Referenz zurückgeben

int& max(int& m, int& n); // Der Rückgabewert ist eine Referenz auf int

int main(int argc, char* argv[])
{

      int m = 44, n = 22;

      cout << m << ", " << n << ", " << max(m,n) << endl;
      max(m,n) = 55;      // ändert den Wert von m von 44 auf 55

      cout << m << ", " << n << ", " << max(m,n) << endl;

gettch();
return 0;
}

int& max(int& m, int& n)
{
      return ( m > n ? m : n );      // m und n sind nichtlokale Referenzen
}

44, 22, 44
55, 22, 55


Die max()-Funktion gibt eine Referenz auf die größere der beiden ihr übergebenen Variablen zurück. Da der Rückgabewert eine Referenz ist, funktioniert der Ausdruck max(m ,n) im Beispiel wie eine Referenz auf m (weil m größer als n ist). Die Zuweisung von 55 an den Ausdruck max(m, n) ist daher gleichbedeutend mit einer direkten Zuweisung an m selbst.

Beispiel 6.8: Verwendung einer Funktion im Arraysubscript

float& component (float*, int );

int main()
{

float v[4];

      for (int k = 1; k <= 4; k++)
            component(v,k) = 1.0/k;

      for (int i = 0; i < 4; i++)
            cout << "v[" << i << "] = " << v[i] << endl;

getch();
return 0;
}

float& component (float* v, int k)
{
      return v[k-1];
}

v[0] = 1
v[1] = 0.5
v[2] = 0.33
v[3] = 0.25


Mit der component()-Funktion können sie auf Vektoren mit den wissenschaftlich "1-basierten Index" anstelle des vorgabemässigen "0-basierten Index" zugreifen. Daher handelt es sich bei der Zuweisung

component(v, k) = 1.0/k


eigentlich um die Zuweisung

v[k+1] = 1.0/k
.

Ein besseres Verfahren, das denselben Zwecke erfüllt, werden wir in Kapitel 9 kennen lernen.

6.7 Arrays und Zeiger

Obwohl Zeiger keine Integer-Typen sind, lassen sich einige der arithmetischen Operatoren für die Integer-Zahlen auf Zeiger anwenden. Die eigentliche Änderung der Adresse hängt von der Größe des fundamentalen Typs ab, der auf den Zeiger weist.

Zeiger lassen sich wie Integer-Werte inkrementieren und dekrementieren. Das Ausmaß der Wertänderun des Zeigers wird jedoch durch die Größe des Objektes bestimmt, auf das er weist.

Beispiel 6.9: Traversieren eines Arrays mit einem Zeiger

// Dieses Beispiel zeigt, wie sich ein Zeiger zur Traversierung eines Arrays einsetzen lässt

int main()
{

    const int SIZE = 3;
    short a[SIZE] = {22,33,44};

    cout << "a = " <<a << endl;
    cout << "sizeof(short) = " << sizeof(short) << endl;

    short* end = a + SIZE; // Konvertiert SIZE zu 6
    short sum = 0;

        for (short* p = a; p < end; p++) {
            sum += *p;
            cout << "\t p = " << p;
            cout << "\t *p = " << *p;
            cout << "\t sum = " << sum << endl;
        }

    cout << "end = " << end << endl;

getch();
return 0;
}

a = 0012FF88
        p = 0012FF00    *p = 22    sum = 22
        p = 0012FF02    *p = 33    sum = 55
        p = 0012FF04    *p = 44    sum = 55
end = 0012FF08


Die zweite Zeile der Ausgabe zeigt, dass Integer-Werte vom Typ short auf diesem Rechner 2 Byte belegen. Da p ein Zeiger auf eine short-Variable ist, rückt er bei jeder Inkrementierung um 2 Byte vor, so dass er auf den nächsten short-Wert im Array weist. Auf diese Weise bildet sum += *p die Summe der Integer-Werte.

Wenn ein Zeiger auf double-Wert wäre und sizeof(double) das Ergebnis 8 Byte liefern würde, dann würde p bei jeder Inkrementierung um 8 Byte vorrücken.

Beispiel 6.9 zeigt, dass der Wert eines Zeigers um jeweils die Anzahl SIZE (in Byte) inkrementiert wird, die der Größe des Objekts entspricht, auf das er zeigt.

Zum Beispiel:

float a[8];

float* p = a; // p zeigt auf a[0]
++p; // erhöht den Wert von p um sizeof(float)

Wenn float-Werte 4 Byte belegen würden, dann erhöht ++pp; den Wert von p um 4 und p+= 5; um 20.

Auf diese Weise können Sie ein Array travestieren: Zeiger lassen sich auch zum direkten Zugriff auf Arrayelemente einsetzen. Zum Beispiel können Sie auf a[5] zugreifen, indem Sie den Zeiger auf a[0] initialisieren und dann 5 hinzuaddieren.

float* p = a; // p zeigt auf a[0]
p += 5; // nun zeigt p auf a[5]

Nach der Initialisierung eines Zeigers auf die Anfangsadresse des Arrays arbeitet er also wie ein Index.

Warnung: Mit C++ können Sie sogar auf nicht zugeordnete (nicht "alloziierte") Speicherbereiche zugreifen und diese ändern. Das ist riskant und sollte generell vermieden werden.

Zum Beispiel:

float a[8];
float* p = a[7];     // p zeigt auf das letzte Arrayelement
++p;             // nun zeigt p auf eine Speicherstelle hinter dem letzten Element
*p = 22.2         // Ärger !

Das nächste Beispiel zeigt eine noch engere Verbindung zwischen Zeigern und Arrays:
Dieses Beispiel zeigt auch, dass sich Zeiger vergleichen lassen.

Beispiel 6.10: Adressen von Arrayelementen untersuchen

int main()
{

      short a[] = {22,33,44,55,66};

      cout << "a = " << a << ", *a = " << *a << endl;

            for (short* p = a; p < a + 5; p++)
                  cout << "p = " << p << ", *p = " << *p << endl;

getch();
return 0;
}

a = 0012FF80, *a = 22
p = 0012FF80, *p = 22
p = 0012FF82, *p = 33
p = 0012FF84, *p = 44
p = 0012FF86, *p = 55
p = 0012FF88, *p = 66


Anfangs sind a und p identisch: Sie zeigen beide auf einen short-Wert, und sie haben denselben Wert (0012FF80). Da a ein konstanter Zeiger ist, lässt er sich nicht für das Traversieren des Arrays inkrementieren.

Statt dessen inkrementieren wir p und benutzen die Fortsetzungsbedingung

p < a + 5

zur Beendigung der Schleife. Diese berechnet a + 5 als Hexadezimaladresse

0012FF80 + 5*sizeof(short) = 0012FF80 + 5*2,

so dass die Schleife so lange fortgesetzt wird, wie p < 0012FF90 ist.

Der Subscriptoperator [] für Arrays entspricht dem Dereferenzoperator *. Beide ermöglichen gleichermassen den direkten Zugriff auf Arrayelemente:

        a[0] == *a
        a[1] == *(a+1)
        a[2] = *(a+2), usw.

Also könnte das Array a wie folgt traversiert werden:

        for ( int i = 0; i < 8; i++)
                cout<< *(a+i) << endl;

Das nächste Beispiel verdeutlicht den gemeinsamen Einsatz von Zeigern mit Integer-Zahlen zur Vor- und Rückwärtsbewegung im Arbeitsspeicher.

Beispiel 6.11: Mustererkennung (Pattern Matching)

short loc(short* a1, short* a2, int n1, int n2);

int main()
{
        short a1[9] = {11, 11, 11, 11, 11, 22, 33, 44, 55};
        short a2[5] = {11, 11, 11, 22, 33};

        cout << "Der Array a1 beginnt am Ort\t" << a1 << endl;
        cout << "Der Array a2 beginnt am Ort\t" << a2 << endl;

        short* p = loc(a1, a2, 9, 5);

                if (p){
                        cout << "Array a2 wurde am Ort\t" << p << " gefunden" << endl;
                                for (int i = 0; i <= 5; i++)
                                        cout << "\t" << &p[i] << ": " << p[i]
                                         << "\t" << &a2[i] << ": " << a2[i] << endl;
}
                else cout << "Nicht gefunden.\n";

getch();
return 0;
}

short loc(short* a1, short* a2, int n1, int n2)
{
        short* end1 = a1 + n1;

                for (short* p1 = a1; p1 < end1; p1++)
                        if (*p1 == *a2)
                {
                        for (int j = 0; j <= n2; j++)
                                if ( j == n2 ) return p1;
                 }
return 0;
}

Der Algorithmus zur Mustererkennung verwendet zwei Schleifen. Die äußere Schleife wird vom Zeiger p1 gesteuert, der auf Elemente des Arrays a1 zeigt, ab denen die innere Schleife prüft, ob Übereinstimmungen mit Arrays a2 vorliegen. Die innere Schleife wird wird von der int-Varaiblen j gesteuert, die für den Vergleich der korrespondierenden Elemente der beiden Arrays benutzt wird. Wenn keine Übereinstimmung entdeckt wird, wird die innere Schleife beendet, und die äußere Schleife wird mit der Inkrementierung von p1 fortgesetzt, um dann nach einer Übereinstimmung zu suchen, die mit dem nächsten Element von a1 beginnt. Wenn die inneren Schleife beendet werden kann, dann ist die Bedingung ( j == n2 ) wahr, und es wird die Position zurückgegeben, auf die p1 aktuell zeigt.

Im Testprogramm prüfen wir, ob tatsächlich eine Übereinstimmung vorliegt, indem wir die tatsächlichen Adressen miteinander vergleichen.

6.8 Der new - Operator

Wenn Zeiger deklariert werden,

float* p;       // p ist ein Zeiger auf einen float

fordern sie lediglich den Speicher für den Zeiger selbst an.

Der Wert des Zeigers ist dann irgendeine Speicheradresse, aber der Speicher dieser Adresse wurde bisher noch nicht alloziiert. Das bedeutet, dass dieser Speicher durchaus bereits von einer irgendeiner anderen Variablen benutzt werden könnte.

In diesem Fall ist p nicht initialisiert:
*p = 3.14155        // FEHLER: p* wurde kein Speicher zugeteilt.

Ein guter Ansatz, um dieses Problem zu vermeiden, ist die Initialisierung von Zeigern bei ihrer Deklaration.

float x = 3.14159       // x enthält den Wert 3.14159
float* p = &x;       // p enthält die Adresse von x
cout << *p;       // OK, denn p *wurde alloziert

Dann ist der Zugriff auf *p kein Problem, weil der für die Speicherung des float-Wertes 3.14159 benötigte Platz automatisch bei der Deklaration von x angefordert worden ist. p zeigt auf denselben zugeteilten Speicher.

Eine andere Möglichkeit zur Vermeidung des Problems "hängender" Zeiger (dangling pointers) ist die ausdrückliche Anforderung von Speicher für den Zeiger selbst.

Zu diesem Zweck dient der Operator new:

float* q;
q = new float;       // fordert Speicher für einen float an
*q = 3.14159       // OK: *p wurde Speicher zugeteilt

Der new-Operator Die ersten beiden dieser Zeilen lassen sich kombinieren,, wodurch q bei seiner Deklaration initialisiert wird.

float * q = new float;


Beachten Sie, dass mit dem new-Operator bei der Initialisierung von q nur der Zeiger selbst, nicht aber der Speicher, auf den er zeigt, initialisiert wird. Es ist möglich, beides in der Deklaration des Zeigers zu erledigen:

float* q = new float(3.14159);
cout << *q;       // OK, sowohl q als auch *q wurden initialisiert

Im unwahrscheinlichen Fall, dass nicht genügend Speicher zur Alllozierung eines freien Blocks der erforderlichen Größe zur Verfügung steht, gibt der new-Operator 0 (den NULL-Zeiger) zurück.

double* p = new double;
if ( p == 0 ) abort();       // Alllozierung wegen Speichermangel fehlgeschlagen
else *p = 3.141592658979324;

Dieser umsichtige Quelltext ruft eine Funktion namens abort() auf, um dem Dereferenzieren des NULL-Zeigers vorzubeugen.

Betrachten Sie noch einmal beide Alternativen der Speicheranforderung:

float x = 3.14159;       // alloziert benannten Speicher
float* p = new float(3.14159);       // alloziert unbenannten Speicher

Im ersten Fall wird während der Kompilierung Speicher für die benannte Variable x allloziert. Im zweiten Fall wird zur Laufzeit Speicher für ein unbenanntes Objekt allloziert, auf das mit *p zugegriffen werden kann.

6.9 Der delete-Operator

Der Operator delete erledigt die gegenteilige Aufgabe wie der new-Operator und gibt allozierten Speicherplatz wieder frei.

Er sollte nur auf Zeiger angewendet werden, für mit dem new-Operator ausdrücklich Speicher alloziert wurde.

float* q = new float(3.14159);
delete q       // gibt den Speicher von q frei ("deallloziert" q)
*q = 2.71828       // FEHLER *q ist kein Speicher zugeteilt

Nach der Freigabe (Deallozierung) von q wird der Speicherblock sizeof(float) Byte wieder für die Zuteilung an andere Objekte verfügbar. Anschliessend sollte q nicht mehr verwendet werden, es sei denn, es wird erneut Speicher für q angefordert. Freigegebene Zeiger, die auch hängender Zeiger genannt werden, sind wie nicht initialisierte Pointer und zeigen auf gar nichts.

Zeiger auf Konstanten lassen sich nicht löschen:

const int* p = new int;
delete p;       // FEHLER: kann kein Zeiger auf Konstante löschen

Diese Beschränkung entspricht dem allgemeinen Prinzip, dass sich Konstanten nicht ändern lassen. Der Einsatz des delete-Operators für fundamentale Tapen (char, int, float, double, usw.) ist generell nicht zu empfehlen, weil der Nutzen im Vergleich zu den möglichen verheerenden Fehlern gering ist.

float x = 3.14159;         // x enthält den Wert 3.14159
float* p = &x;         // p erhält die Adresse von x
delete p;         // RISKANT: p wurde nicht mit new angefordert

Diese Sequenz setzt den Speicher der Variablen x frei. Derartige Fehler lassen sich äußerst schwer aufspüren.

6.10 Dynamische Arrays

Ein Arrayname ist eigentlich nur ein konstanter Zeiger, der bei der Kompilierung alloziert wird.

float a[20];         // a ist ein konstanter Zeiger auf einen Block mit 20 float-Werten.

float* const p = new float a[20];       // dasselbe gilt für p

Hier sind sowohl a als auch p konstante Zeiger auf Blöcke mit 20 float-Werten. Die Deklaration von a unterliegt der sogenannten statischen Bindung, weil diese Bindung bei der Kompilierung erfolgt.
Im Gegensatz lassen sich hierzu nichtkonstante Zeiger einsetzen, um die Anforderung von Speicher in die Laufzeit des Programms zu verschieben. Dann spricht man im Allgemeinen von einer Bindung zur Laufzeit oder dynamischen Bindung:

float* p = new float[20];

Arrays, die auf diese Art und Weise deklariert werden,, nennt man dynamische Arrays. Vergleichen Sie die zwei Möglichkeiten zur Definition eines Arrays:

float a[20];       // statisches Array
float* p = new float[20]       // dynamisches Array

Das statische Array a wird bei der Kompilierung erzeugt. Sein Speicher bleibt während der gesamten Laufzeit des Programmes alloziert. Das dynamische Array p wird zur Laufzeit erzeugt. Ihm wird nur dann Speicher zugeteilt, wenn seine Deklaration ausgeführt wird.

Weiterhin wird der dem Array p zugeteilte Speicher wieder freigegeben (dealloziert) wenn der delete-Operator auf ihn angewendet wird.

delete [] p;       // gibt den Speicher des Arrays a frei

Beachten Sie, dass der Subscriptoperator [] wie angegeben verwendet werden muss, weil es sich bei p um einen Array handelt.

Beispiel 6.12 Der Einsatz dynamischer Arrays

// In diesem Beispiel erzeugt die get()-Funktion ein dynamisches Array

void print ( double* a, int n);
void get( double*& a, int& n );

int main()
{
      double* a;       // a ist einfach ein nicht allozierter Zeiger
      int n;
      get(a, n);       // Jetzt ist a wieder ein Array auf double-Werten
      print ( a, n);
      delete [] a;       // a ist wieder ein nicht allozierter Zeiger
      get(a, n);       // Jetzt ist a wieder ein Array auf double-Werten
      print ( a, n);

getch();
return 0;
}

void get( double*& a, int& n )
{
            cout << "Geben Sie die Anzahl der Felder an: ";
            cin >> n;

            a = new double[n];
            cout << "Geben Sie " << n << " Werte ein, einen pro Zeile\n";

                  for ( int i = 0; i < n; i++) {
                        cout << "\t" << i+1 << ": ";
                        cin >> a[i];
                  }
}

void print ( double* a, int n)
{

                  for ( int i = 0; i < n; i++)
                        cout << a[i] << " ";
                  cout << endl;
}

Geben Sie die Anzahl der Felder ein: 4
Geben Sie 4 Werte ein, einen pro Zeile:
      1: 44.4
      2: 77.7
      3: 22.2
      4: 88.8
44.4 77.7 22.2 88.8
Geben Sie die Anzahl der Felder ein: 2
Geben Sie 2 Werte ein, einen pro Zeile:
      1: 3.33
      2: 9.99
3.33 9.99


Wenn der Wert von n interaktiv abgefragt worden ist, fordert der new-Operator in der get()-Funktion Speicher für n double-Werte an. Das Array wird während der Laufzeit des Programms erzeugt.

Bevor Sie get() zur Erzeugung eines weiteren Arrays für a einsetzen können, muss der vom aktuellen Array belegte Speicher mit dem delete-operator freigegeben werden.

Beachten Sie, dass der Subscriptoperator [] bei der Freigabe eines Arrays mit angegeben werden muss.

Beachten Sie, dass der Arrayparameter a ein Zeiger ist, der als Referenz übergeben wird:

void get (double*& a, int& n)

Das ist notwendig, weil der Operator new den Wert von a ändert, bei dem es sich um die Adresse des ersten Elements des neu allozierten Arrays handelt.

6.11 const zusammen mit Zeigern verwenden

Ein Zeiger auf eine Konstante ist etwas anderes als ein konstanter Zeiger. Dieser Unterschied wird im folgenden Beispiel verdeutlicht.

Beispiel 6.13: Konstante Zeiger, Zeiger auf konstanten und konstante Zeiger auf Konstanten

Dieses Fragment deklariert vier Variablen:
int n = 44; // ein int
int* p = &n; // ein Zeiger auf ein int
++ (*p); // OK: inkrementiert int *p
++ p; // OK: inkrementiert den Zeiger p
int* const cp = &n; // ein konstanter Zeiger auf ein int
++ (*cp); // OK: inkrementiert int *cp
++ cp; // illegal: der Zeiger p ist konstant
const int k = 88; // ein const int
const int* pc = &k; // ein Zeiger auf eine const int
++ (*pc); // illegal: int *pc ist konstant
++ pc; // OK: inkrementiert den Zeiger pc
const int* const cpc = &k; // ein konstanter Zeiger auf eine const int
++ (*cpc); // illegal: int *cpc ist konstant
++ cpc; // illegal: Zeiger *cpc ist konstant

Beachten Sie, dass der Referenzoperator * in einer Deklaration mit oder ohne Leerzeichen auf seinen beiden Seiten verwendet werden kann. Daher sind die folgenden Deklarationen äquivalent:

int* p; // p hat den Typ int* (ein Zeiger auf int)
int * p; // Dieser Stil soll zuweilen für Klarheit sorgen
int *p; // alter C-Stil

6.12 Arrays mit Zeigern und Zeiger auf Arrays

Bei den Elementen eines Arrays kann es sich um Zeiger handeln. Hier handelt es sich um eine Array mit vier Zeigern auf den Typ double:

double* p[4];


Speicher für seine Elemente können Sie wie bei allen anderen Zeigern anfordern:

p[2] = new double(3.141592653589793)


Dieser Array lässt sich wie folgt darstellen:



Das nächste Beispiel verdeutlicht eine nützliche Anwendung für Zeigerarrays. Es zeigt, wie sich eine Liste indirekt sortieren lässt, wenn man Zeiger auf die Elemente ändert, anstatt die Elemente selbst zu verschieben.

Das entspricht dem indirekten Bubble-Sort aus Aufgabe 5.12

Beispiel 6.14: Indirektes Bubble-Sort

// Das indirekte Bubble-Sort sortiert das Zeigerarray;

void sort ( float* p[], int n)
{
      float* temp;
      for ( int i = 1; i < n; i++)
            for ( int j = 0; j < n-i; j++)
                  if ( *p[j] > *p[j+1]) {
                        temp = p[j];
                        p[j] = p[j+1];
                        p[j+1] = temp;
}

Wenn die float- Werte der benachbarten Zeiger nicht in der richtigen Reihenfolge vorliegen, werden die zeiger bei jeder Iteration der inneren Schleife vertauscht.

6.13 Zeiger auf Zeiger

Zeiger können auch auf andere Zeiger zeigen. Zum Beispiel:

char p = 't';
char* pc = &c;
char** ppc = &pc;
char*** pppc = &ppc;
***pppc = 'w'; // ändert den Wert von c auf 'w'

Diese Variablen nassen sich so darstellen:




Die Zuweisung ***pppc = 'w' bezieht sich auf den Inhalt der Adresse pc, auf die von der Adresse ppc gezeigt wird, auf die die Adresse pppc verweist.

6.14 Zeiger auf Funktionen

Wie Arraynamen sind Funktionsnamen eigentlcih konstante Zeiger. Wir können uns den

Wert eines Funktionsnamens als Adresse des Codes vorstellen, der die Funktion implementiert


Ein Zeiger auf eine Funktion hat als Wert die Adresse des Funktionsnamens.


Da dieser Zeiger selbst ein Zeiger ist, ist ein Zeiger auf eine Funktion einfach ein Zeiger auf einen konstanten Zeiger.

Zum Beispiel:

int f(int); // deklariert die Funktion f
int (*pf) (int); // deklariert den Funktionszeiger pf
pf = &f; // weist pf die Adresse von f zu

Ein Funktionszeiger lässt sich so darstellen:




Der Nutzen von Funktionszeigern besteht darin, dass sich mit ihnen Funktionen von Funktionen (sogenannte Funktionen höherer Ordnung) definieren lassen. Dazu wird einer Funktion der Funktionszeiger auf eine andere Funktion übergeben.

Beispiel 6.15: Die "Summe" einer Funktion

// Die sum()-Funktion hat zwei Parameter:



int sum( int (*)(int), int);       // Erstes Argument entspricht der Deklaration eines Funktionszeigers)
int square(int);
int cube(int);

int main()
{
      cout << sum(square,4) << endl;       // 1 + 4 + 9 + 16
      cout << sum(cube,4) << endl;       // 1 + 8 + 27 + 64

getch();
return 0;
}

// gibt f(0) + f(1) + f(2) + ... + f(n-1) zurück

int sum (int (*pf)(int k), int n)
{
      int s = 0;
            for (int i = 1; i <= n; i++)
                  s += (*pf)(i);
return s;
}

int square ( int k)
{
       return k*k;
}

int cube ( int k )
{
      return k*k*k;
}

30
100
Der Aufruf sum(square, 4) berechnet die Summe

square(1) + square(2) + square(3) + square(4)


und gibt diese zurück. Da square(k) den Ausdruck k*k berechnet und zurückgibt, gibt die Sum()-Funktion 1 + 4 + 9 + 16 = 30 zurück.

Die sum()-Funktion berechnet für alle int-Werte zwischen 1 und n die Funktion, auf die pf zeigt und gibt die Summe dieser n Werte zurück.

Beachten Sie, dass für die Deklaration des Funktionszeigers pf die Dummy-Variable k in der Parameterliste der sum()-Funktion erforderlich ist.

6.15 NUL, NULL und VOID

Die Konstante 0 (null) hat den Typ int. Ungeachtet dessen lässt sich dieses Symbol an alle fundamentalen Typen zuweisen:

char p = 0; // initialisiert c mit dem char '\0'
short d = 0; // initialisiert d mit dem short int 0
int n = 0; // initialisiert n mit dem int 0'
unsigned u = 0; // initialisiert u mit dem unsigned int 0
float x = 0; // initialisiert x mit dem float 0.0
double z = 0; // initialisiert z mit dem double 0.0

In allen Fällen wird das Objekt mit der Zahl 0 initialisiert. Beim Typ char wird das Zeichen c zum Null-Zeichen, das man als '\0' oder NUL schreibt, und bei dem es sich um das Zeichen mit dem ASCII-Code 0 handelt.

Die Werte von Zeigern sind Speicheradressen. Diese Adressen müssen mit Ausnahme der Adresse 0x0 innerhalb des Speicherberechs bleiben, der dem gerade ausgeführten Prozess zugeteilt wurde.

Diese Adresse nennt man NULL-Zeiger.

Dieselbe Konstante lässt sich auch für Zeiger verwenden, die aus anderen Typen abgeleitet werden:

char* pc = 0; // initialisiert pc auf NULL
short* pd = 0; // initialisiert pd auf NULL
int* pn = 0; // initialisiert pn auf NULL
unsigned* pu = 0; // initialisiert pu auf NULL
float* px = 0; // initialisiert px auf NULL
double* pz = 0; // initialisiert pz auf NULL

Der NULL-Zeiger lässt sich nicht dereferenzieren. Das ist ein verbreiteter schwere Fehler.


intR> *p = 22;       // FEHLER: NULL-Zeiger lassen sich nicht dereferenzieren.

Vor dem Versuch, einen Zeiger zu dereferenzieren ist es sinnvoll, diesen vorsichtshalber zu prüfen:

if (p) *p = 22;      // OK


Dies prüft die Bedingung (p != NULL), weil diese genau dann wahr ist, wenn p nicht null ist.

Der Name VOID kennzeichnet einen speziellen, fundamentalten Typ. Anders als alle anderen fundamentalen Typen lässt sich void nur in abgeleiteten Typen verwenden:

void x;      // Fehler: Objeke können nicht vom Typ void sein
void*p      // OK

Am häufigsten wird der Typ void wenn angegeben werden soll, dass eine Funktion keinen Wert zurückgibt:

void swap(double&, double&);

Eine andere Einsatzmöglichkeit von void ist die Deklaration eines Zeigers auf ein Objekt unbekannten Typs:

void* p = q;

Sie findet man am häufigsten in maschinennahen C-Programmen, die Hardware-Ressourcen direkt manipulieren.