C++ Tutorial

Spezielle Zeiger

Für die Verwaltung von dynamischen Daten und Objekten stehen drei spezielle Zeigertypen zur Verfügung: unique_ptr, shared_ptr, weak_ptr. Die Zeigertypen liegen ebenfalls im Namensraum std und sind in der Header-Datei <memory> definiert. Die ersten beiden Zeiger sind sogenannte smart pointer. Smart pointer kontrollieren die Lebensdauer und den Besitz des Datums, auf welches sie verweisen. D.h. wird ein smart pointer gelöscht, löscht er das über ihn referenzierte Datum.

unique_ptr

Ein unique_ptr übernimmt den Besitz des Objekts, auf das er verweist. D. h., zwei unique_ptr können niemals auf dasselbe Objekt verweisen. Um einen unique_ptr zu definieren, ist vorzugsweise das Funktionstemplate make_unique() zu verwenden, welches ebenfalls in der Header-Datei <memory> definiert ist:

std::unique_ptr<DTYP> ptr = std::make_unique<DTYP>([ARGS]);

DTYP gibt den Datentyp des Objekts an, auf das der Zeiger verweist. Da make_unique() das Objekt instanziiert, sind innerhalb der runden Klammern eventuell notwendige Konstruktorargumente ARGS des Objekts anzugeben.

Alternativ kann ein unique_ptr wie folgt definiert werden:

DTYP definierten wieder den Datentyp des Objekts, auf das der Zeiger verweist. Und das Argument ARG kann dann sein:

  • new DTYP, d.h., es wird ein neues Objekt erstellt und dem Zeiger zugewiesen,
  • ein Zeiger auf ein bestehendes Objekt,
  • ein nullptr oder entfallen, d.h., der Zeiger verweist auf kein Objekt.

Wird an einen unique_ptr ein Zeiger auf ein bestehendes Objekt übergeben, geht der Besitz des Objekts auf den unique_ptr über und das Objekt darf nicht mehr über den ursprünglichen Zeiger gelöscht werden.

Soll ein unique_ptr einem anderen unique_ptr zugewiesen werden, kann hierfür nicht der Zuweisungsoperator verwendet werden, da der Besitz des Objekts übertragen werden muss. Um den Objektbesitz von einem unique_ptr an einen anderen zu übertragen, ist das Funktionstemplate std::move() zu verwenden, das in der Header-Datei <utility> definiert ist.

uptr1 = std::move(uptr2);

Die Anweisung überträgt den Besitz des Objekts, welches durch uptr2 referenziert wird, an den Zeiger uptr1. Es versteht sich, dass beide Zeiger den gleichen Datentyp haben müssen. Nach der Ausführung der Anweisung enthält der Zeiger uptr2 einen nullptr, da er auf kein Objekt mehr verweist.

Um einem unique_ptr ein weiteres, neues Objekt zuzuweisen, ist hierfür die Methode reset([new object]) des unique_ptr zu verwenden. reset() übernimmt den Besitz des neuen Objekts object und löscht das bisherige Objekt. Wird reset() ohne Argument aufgerufen, wird das bisherige Objekt gelöscht und der unique_ptr enthält keinen Objektverweis.

Der Zugriff auf das über den unique_ptr referenzierte Objekt erfolgt auf die gleiche Art und Weise wie bei gewöhnlichen Zeigern, d.h. entweder per Dereferenzierungsoperator * oder per Zeigeroperator ->. Wird der Zeiger auf das Objekt selbst benötigt, liefert die Methode get() diesen zurück.

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

1: #include <iostream>
2: #include <print>
3: #include <memory>
4:
5: // Demo-Klasse mit Objektzaehler
6: class CAny
7: {
8:    int val; // Aktuelle Instanz
9:    inline static int counter = 0; // Objektzaehler
10: public:
11:   CAny(): val(counter++)
12:   {
13:      std::println("ctor CAny {}", val);
14:    }
15:   ~CAny()
16:   {
17:      std::println("dtor CAny {}", val);
18:   }
19:   void Print()
20:   {
21:      std::println("CAny Nr. {}", val);
22:   }
23: };
24: int main()
25: {
26:    // CAny-Objekt erzeugen und einem unique_ptr zuweisen
27:    // Alternativ: auto ptr1 = std::make_unique<CAny>();
28:    std::unique_ptr<CAny> ptr1 = std::make_unique<CAny>();
29:    std::cout << "1. Objekt angelegt: ";
30:    ptr1->Print();
31:    // Besitz von ptr1 an weiteren unique_ptr uebertragen
32:    std::unique_ptr<CAny> ptr2 = std::move(ptr1);
33:    std::cout << "Besitz uebertragen: ";
34:    ptr2->Print();
35:    // Bisheriges referenzierte Objekt loeschen und
36:    // neues Objekt in anlegen
37:    ptr2.reset(new CAny);
38:    std::cout << "Objekt mit neuem Objekt ueberschrieben: ";
39:    ptr2->Print();
40: }

ctor CAny 0
1. Objekt angelegt: CAny Nr. 0
Besitz uebertragen: CAny Nr. 0
ctor CAny 1
dtor CAny 0
Objekt mit neuem Objekt ueberschrieben: CAny Nr. 1
dtor CAny 1

Außer auf ein Objekt kann ein unique_ptr auch auf ein Feld verweisen. Dazu überlädt der unique_ptr den Index-Operator []. Und auch hier gilt: Wird der unique_ptr gelöscht, wird das über ihn referenzierte Feld gelöscht.

1: int main()
2: {
3:    // Feld mit 10 int anlegen und Besitz an
4:    // unique_ptr uebertragen
5:    constexpr int ISIZE=10;
6:    std::unique_ptr<int[]> ptr2(new int[ISIZE]);
7:    // Alternativ:
8:    // auto ptr2 = std::make_unique<int[]>(ISIZE);
9:    // Zugriff auf das int-Feld ueber den unique_ptr
10:   for (auto i=0; i<ISIZE; i++)
11:      ptr2[i] = i*2;
12: } // Hier wird das Feld geloescht

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

shared_ptr

Um ein und dasselbe Objekt über mehrere Zeiger zu referenzieren, kann ein shared_ptr eingesetzt werden. Auch er übernimmt den Besitz des Objekts, genauso wie der unique_ptr. Das über shared_ptr referenzierte Objekt wird jedoch erst dann gelöscht, wenn der letzte auf das Objekt verweisende shared_ptr gelöscht wird.

Ein shared_ptr kann ebenfalls auf zwei Arten definiert werden:

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

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

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

Das weitere Verhalten eines shared_ptr entspricht prinzipiell dem des unique_ptr.

1: class CAny
2: {
3:    ... // Definition siehe Beispiel unique_ptr
4: };
5:
6: int main()
7: {
8:    // shared_ptr auf CAny-Objekt
9:    auto ptr1 = std::make_shared<CAny>();
10:   ptr1->Print();
11:   std::printl("Refenzen: {}", ptr1.use_count());
12:   // 2. shared_ptr auf das selbe CAny-Objekt
13:   std::shared_ptr<CAny> ptr2 = ptr1;
14:   ptr2->Print();
15:   std::println("Refenzen: {}", ptr1.use_count());
16:   // ptr1 gibt Objekt frei,
17:   // aber ptr2 hat Objekt noch reserviert!
18:   ptr1.reset();
19:   std::println("Referenzen nach reset(): {}",
20:   ptr2.use_count());
21: }

ctor CAny 0
CAny Nr. 0
Refenzen: 1
CAny Nr. 0
Refenzen: 2
Referenzen nach reset(): 1
dtor CAny 0

weak_ptr

Der weak_ptr stellt die loseste Kopplung zwischen einem Zeiger und dem referenzierten Objekt her. Er ü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<DTYP> ptr(ARG);

wobei ARG entweder ein shared_ptr ist oder entfällt. Andere Optionen sind für ARG nicht zulässig. Wird ein weak_ptr mit einem shared_ptr initialisiert, referenziert er zwar dasselbe Objekt wie der shared_ptr, der Objektzähler des shared_ptr wird dabei aber nicht inkrementiert.

Um über einen weak_ptr auf das Objekt zugreifen zu können, ist vorher die Methode lock() des weak_ptr aufzurufen, die einen shared_ptr zurückliefert. Ein Zugriff auf das Objekt über den weak_ptr direkt ist nicht möglich.

Da ein weak_ptr nicht Besitzer des Objekts ist, kann ein über den Zeiger referenziertes Objekt zwischenzeitlich gelöscht worden sein. Um festzustellen, ob das über den weak_ptr referenzierte Objekt noch existiert, wird die Methode expired() verwendet. Liefert sie true zurück, wurde das Objekt gelöscht.

1: class CAny
2: {
3:    ... // Definition siehe unique_ptr
4: };
5:
6: int main()
7: {
8:    // weak_ptr ohne Verweis definieren
9:    std::weak_ptr<CAny> wptr;
10:   {
11:      // shared_ptr mit Referenz auf CAny-Objekt
12:      std::shared_ptr<CAny> ptr1 =
13:      std::make_shared<CAny>();
14:      ptr1->Print();
15:      std::println("sp1 Refenzen: {}", ptr1.use_count());
16:      // Dem weak_ptr eine Referenz auf das ueber den
17:      // shared_ptr referenzierte Objekt zuweisen
18:      wptr = ptr1;
19:      std::println("wp Refenzen: {}", wptr.use_count());
20:      // Zugriff auf das referenzierte Objekt
21:      // nur ueber einen weiteren shared_ptr moeglich
22:      std::shared_ptr<CAny> ptr2 = wptr.lock();
23:      ptr2->Print();
24:      std::println("sp2 Refenzen: {}", ptr2.use_count());
25:      // Gueltigkeit des Objekts im weak_ptr ausgeben
26:      std::println("wptr valid: {}", !wptr.expired());
27:      // shared_ptr und das Objekt löschen
28:   }
29:   std::println("wptr valid: {}", !wptr.expired());
30: }

ctor CAny 0
CAny Nr. 0
sp1 Refenzen: 1
wp Refenzen: 1
CAny Nr. 0
sp2 Refenzen: 2
wptr valid: true
dtor CAny 0
wptr valid: false

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


Copyright 2024 © Wolfgang Schröder
E-Mail mit Fragen oder Kommentaren zu dieser Website an: info@cpp-tutor.de
Impressum & Datenschutz