C++ Kurs

Spezielle Zeiger

Die Themen:

Für das Arbeiten mit dynamischen Objekten (und auch Daten) stellt C++ drei spezielle Zeigertypen zur Verfügung: den unique_ptr, den shared_ptr und den weak_ptr. Die Definitionen dieser Zeigertypen liegen im Namensraum std und sind in der Header-Datei memory definiert. Die ersten beiden Zeiger sind sogenannte smart pointer. smart pointer verhalten sich wie normale Zeiger, kontrollieren aber die Lebensdauer und den Besitz des Objekts auf welches sie verweisen. D.h. wird ein smart pointer gelöscht, so löscht er auch das über ihn referenzierte Objekt.

HinweisVerwenden Sie diese Zeiger auch nur dann, wenn dies sinnvoll ist. Es ist keine gute Idee, einfach alle bisherigen Zeiger durch diese neuen Zeigertypen zu ersetzen. In der Regel werden die neuen Zeigertypen dann eingesetzt, wenn es gilt Ressourcen sicher zu allokieren und zu verwalten.

unique_ptr

Ein unique_ptr übernimmt den Besitz des Objekts, das ihm zugewiesen wird. Daraus folgt, dass zwei unique_ptr niemals auf dasselbe Objekt verweisen können.

Um einem unique_ptr ein Objekt zuzuweisen, wird bei der Definition des unique_ptr das Objekt an diesen als Argument übergeben.

std::unique_ptr<T> ptr(ARG);

T definierten den Datentyp des Objekts auf das der Zeiger verweisen soll, in der Regel ist dies die Klasse des Objekts. Das ARG kann sein:

Zu beachten ist, dass, wenn dem unique_ptr ein Zeiger auf ein bereits bestehendes Objekt übergeben wird, der Besitz des Objekts auf den unique_ptr übergeht und das Objekt deshalb nicht mehr über den ursprünglichen Zeiger gelöscht werden darf.

Wenn ein unique_ptr einem anderen unique_ptr zugewiesen werden soll, so kann hierfür nicht der normale Zuweisungsoperator verwendet werden, da ja der Besitz des Objekts übertragen werden muss. Um den Objektbesitz von einem unique_ptr an einen anderen zu übertragen, muss der Move-Zuweisungsoperator verwendet werden, welcher später noch erklärt wird.

Soll ein bestehender unique_ptr auf ein anderes, neues Objekt verweisen, so ist hierfür die Memberfunktion reset(new object) des unique_ptr zu verwenden. reset() übernimmt dann den Besitz des neuen Objekts und zerstört anschließend das alte, bisherige Objekt. Wird reset() ohne Argument aufgerufen, so wird das bisherige Objekt gelöscht und der unique_ptr enthält danach keinen Objektverweis mehr.

Der Zugriff auf das im unique_ptr abgelegte Objekt erfolgt auf die gleiche Art und Weise wie bei gewöhnlichen Zeigern, d.h. entweder per * oder per ->.

Das nachfolgende einfache Beispiel zeigt den Einsatz eines unique_ptr. Das Beispiel zeigt auch, wie prinzipiell mit Hilfe des Move-Zuweisungsoperators der Besitz eines Objekts von einem unique_ptr auf einen anderen übertragen wird. Die Definition des Move-Zuweisungsoperators selbst ist im Beispiel nicht dargestellt, da dieser Operator erst später behandelt wird.

PgmHeader// Demo-Klasse mit Objektzaehler
class Any
{
    int val;               // Aktuelle Instanz
    static int counter;    // Objektzaehler
  public:
    Any(): val(counter++)
    {
        cout << "ctor Any " << val << endl;
    }
    ~Any()
    {
        cout << "dtor Any " << val << endl;
    }
    void Print()
    {
        cout << "Any Nr. " << val << endl;
    }
};
int Any::counter = 0;

int main()
{
    // Objekt erzeugen und Besitz an ptr1 uebertragen
    std::unique_ptr<Any> ptr1(new Any);
    ptr1->Print();
    // Neues Objekt erzeugen und bisheriges Objekt freigeben
    ptr1.reset(new Any);
    ptr1->Print();
    // Besitz an ptr2 uebertragen
    std::unique_ptr<Any> ptr2 = std::move(ptr1);
    ptr2->Print();
}
Programmausgabector Any 0
Any Nr. 0
ctor Any 1
dtor Any 0
Any Nr. 1
Any Nr. 1
dtor Any 1

Außer Objekte können in unique_ptr auch Felder abgelegt werden. Dazu überlädt der unique_ptr den Index-Operator [] entsprechend. Und auch hier gilt: wird der unique_ptr gelöscht, so wird auch das über ihn referenzierte Feld gelöscht.

PgmHeaderint main()
{
    // Feld mit 10 ints anlegen und Besitz an unique_ptr uebertragen
    constexpr int ISIZE=10;
    std::unique_ptr<int[]> ptr2(new int[ISIZE]);
    // Zugriff auf das int-Feld ueber den unique_ptr
    for (int i=0; i<ISIZE; i++)
        ptr2[i] = i*2;
}

unique_ptr enthält noch eine Reihe weiterer Methoden und überladener Operatoren, auf die aber hier nicht weiter eingegangen werden soll.

shared_ptr

Ein shared_ptr dient dazu, wie der Name schon vermuten lässt, dass mehrere Zeiger auf dasselbe Objekt verweisen. Auch der shared_ptr übernimmt den Besitz des Objekts, genauso wie der unique_ptr. Das über den shared_ptr referenzierte Objekt wird jedoch erst dann gelöscht, wenn der letzte shared_ptr auf das Objekt gelöscht wird. Dies wird in der Regel dadurch erreicht, dass der shared_ptr intern einen entsprechenden Referenzzähler besitzt.

Ein shared_ptr kann auf zwei Arten definiert werden:

auto ptr = std::make_shared<T>(ARG); bzw.
std::shared_ptr<T> ptr(ARG);

T gibt wieder den Datentyp des Objekts an, das über den shared_ptr verwaltet werden soll. Und für das Argument ARG gelten die gleichen Aussagen wie für den unique_ptr.

Im Gegensatz zum unique_ptr können shared_ptr jedoch einander zugewiesen werden. Nach der Zuweisung verweisen beide Zeiger auf dasselbe Objekt. Die Anzahl der Verweise auf ein Objekt kann über die Memberfunktion use_count() des shared_ptr ermittelt werden.

Das weitere Verhalten eines shared_ptr entspricht prinzipiell dem des unique_ptr.

PgmHeaderclass Any
{
    .... // Definition siehe Beispiel unique_ptr
};


int main()
{
    // shared_ptr auf Any-Objekt
    std::shared_ptr<Any> ptr1 = std::make_shared<Any>();
    ptr1->Print();
    cout << "Refenzzaehler: " << ptr1.use_count() << endl;
    // 2. shared_ptr auf dasselbe Any-Objekt
    std::shared_ptr<Any> ptr2 = ptr1;
    ptr2->Print();
    cout << "Refenzzaehler: " << ptr1.use_count() << endl;
    // Achtung! ptr1.reset() loescht das Objekt,
    // d.h. der Referenzzaehler ist dann 0!

    ptr1.reset();
    cout << "Referenzzaehler nach reset(): " << ptr1.use_count()
         << endl;
}
Programmausgabector Any 0
Any Nr. 0
Refenzzaehler: 1
Any Nr. 0
Refenzzaehler: 2
Referenzzaehler nach reset(): 0
dtor Any 0

weak_ptr

Der weak_ptr stellt die loseste Kopplung zwischen Zeigern und dem referenzierten Objekt her. Ein weak_ptr übernimmt nicht den Besitz eines Objekts sondern stellt lediglich den Verweis auf ein Objekt her.

Ein weak_ptr wird wie folgt definiert:

std::weak_ptr<T> ptr(ARG);

wobei ARG nun entweder leer ist oder aber ein shared_ptr sein muss. Eine andere Option ist für ARG nicht zulässig. Wir ein weak_ptr mit einem shared_ptr initialisiert, so referenziert er zwar dasselbe Objekt wie der shared_ptr, der Referenzzähler des shared_ptr wird dabei aber nicht inkrementiert.

Um über einen weak_ptr auf das Objekt zugreifen zu können, muss vorher die Memberfunktion lock() des weak_ptr aufgerufen werden, welche dann einen entsprechenden shared_ptr zurückliefert. Ein Zugriff auf das Objekt über den weak_ptr selbst ist nicht möglich.

Da ein weak_ptr nicht Besitzer des Objekts ist, kann das über den Zeiger referenziert Objekt zwischenzeitlich auch gelöscht worden sein. Dieser Fall tritt dann ein, wenn der dem weak_ptr zugewiesene shared_ptr das Objekt gelöscht hat. Um festzustellen, ob das über den weak_ptr referenzierte Objekt noch existiert, wird die Memberfunktion expired() verwendet. Liefert sie true zurück, so wurde das Objekt zwischenzeitlich gelöscht.

PgmHeaderclass Any
{
    .... // Definition siehe unique_ptr
};


int main()
{
    // weak_ptr ohne Verweis definieren
    std::weak_ptr<Any> wptr;
    {
        // shared_ptr mit Referenz auf Any-Objekt
        std::shared_ptr<Any> ptr1 = std::make_shared<Any>();
        ptr1->Print();
        cout << "sp1 Refenzzaehler: " << ptr1.use_count() << endl;
        // Dem weak_ptr eine Referenz auf das ueber den
        // shared_ptr referenzierte Objekt zuweisen

        wptr = ptr1;
        cout << "wp Refenzzaehler: " << wptr.use_count() << endl;
        // Zugriff auf das ueber den weak_ptr referenzierte Objekt
        // nur ueber einen weiteren shared_ptr moeglich

        std::shared_ptr<Any> ptr2 = wptr.lock();
        ptr2->Print();
        cout << "sp2 Referenzzaehler: " << ptr2.use_count() << endl;
        // Gueltigkeit des Objekts im weak_ptr ausgeben
        cout << "wptr valid: " << ((wptr.expired()==false)?"yes":"no") << endl;
        // Hier wird der shared_ptr und damit das Any-Objekt zerstoert
    }
    cout << "wptr valid: " << ((wptr.expired()==false)?"yes":"no") << endl;

}
Programmausgabector Any 0
Any Nr. 0
sp1 Refenzzaehler: 1
wp Refenzzaehler: 1
Any Nr. 0
sp2 Referenzzaehler: 2
wptr valid: yes
dtor Any 0
wptr valid: no

Der weak_ptr wird hauptsächlich dann eingesetzt, wenn es gilt, auf Objekte zuzugreifen, die aus irgendwelchen Gründen zwischenzeitlich gelöscht sein können.