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 ebenfalls das über ihn referenzierte Datum.
Verwenden Sie diese Zeiger nur dann, wenn dies auch wirklich sinnvoll ist. Es ist keine gute Idee, alle Zeiger durch diese neuen Zeigertypen zu ersetzen. In der Regel werden die neuen Zeigertypen eingesetzt, wenn es gilt Ressourcen sicher zu verwalten oder wenn dynamische Daten in Containern der C++-Standardbibliothek abgelegt werden.
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>
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:
std::unique_ptr<DTYP> ptr(ARG);
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 noch 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.
uniptr1 = std::move(uniptr2);
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 Funktionstemplates move() der Besitz eines Objekts von einem unique_ptr auf einen anderen übertragen wird.
#include <print>
#include <memory>
// Demo-Klasse mit Objektzaehler
class CAny
{
int val; // Aktuelle Instanz
inline static int counter = 0; // Objektzaehler
public:
CAny(): val(counter++)
{
std::println("ctor CAny {}", val);
}
~CAny()
{
std::println("dtor CAny {}", val);
}
void Print()
{
std::println("CAny Nr. {}", val);
}
};
int main()
{
// CAny-Objekt erzeugen und einem unique_ptr zuweisen
// Alternativ: auto ptr1 = std::make_unique<CAny>();
std::println("uniptr1 definieren und mit Objekt belegen:");
std::unique_ptr<CAny> ptr1 = std::make_unique<CAny>();
std::print("uniptr1 -> ");
ptr1->Print();
// Besitz von ptr1 an weiteren unique_ptr uebertragen
std::println("uniptr1 uebertragen an uniptr2:");
std::unique_ptr<CAny> ptr2 = std::move(ptr1);
std::print("uniptr2 -> ");
ptr2->Print();
std::println("uni1ptr1 enthaelt jetzt {}",
reinterpret_cast<void*>(ptr1.get()));
// Bisheriges referenzierte Objekt loeschen und
// neues Objekt in anlegen
std::println("uniptr2 mit neuem Objekt ueberschreiben:");
ptr2.reset(new CAny);
std::print("uniptr2 -> ");
ptr2->Print();
}
uniptr1 definieren und mit Objekt belegen:
ctor CAny 0
uniptr1 -> CAny Nr. 0
uniptr1 uebertragen an uniptr2:
uniptr2 -> CAny Nr. 0
uni1ptr1 enthaelt jetzt 0x0
uniptr2 mit neuem Objekt ueberschreiben:
ctor CAny 1
dtor CAny 0
uniptr2 -> CAny Nr. 1
dtor CAny
Außer auf ein Objekt kann ein unique_ptr auch auf ein Feld verweisen. Dazu stellt der unique_ptr den Index-Operator [] zur Verfügung. Und auch hier gilt: Wird der unique_ptr gelöscht, wird das über ihn referenzierte Feld gelöscht.
#include <memory>
#include <print>
class CAny
{
unsigned short objNum;
inline static unsigned short objCnt{0};
public:
CAny()
{
objNum = objCnt++;
std::println(">>> ctor Objektnummer: {}",objNum);
}
~CAny()
{
std::println("<<< dtor Objektnummer: {}",objNum);
}
void Print()
{
std::println(" Objektnummer {}",objNum);
}
};
int main()
{
// Feld mit 3 Objekten an unique_ptr uebertragen
constexpr int ISIZE=3;
std::unique_ptr<CAny[]> uniptr(new CAny[ISIZE]);
// Alternativ:
// auto uniptr = std::make_unique<CAny[]>(ISIZE);
// Zugriff auf das Feld ueber den unique_ptr
for (auto i=0; i<ISIZE; i++)
uniptr[i].Print();
} // Hier wird der unique_ptr geloescht und damit
// auch die Objekte im Feld
>>> ctor Objektnummer: 0
>>> ctor Objektnummer: 1
>>> ctor Objektnummer: 2
Objektnummer 0
Objektnummer 1
Objektnummer 2
<<< dtor Objektnummer: 2
<<< dtor Objektnummer: 1
<<< dtor Objektnummer: 0
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.
#include <memory>
#include <print>
class CAny
{
unsigned short objNum;
inline static unsigned short objCnt{0};
public:
CAny()
{
objNum = objCnt++;
std::println(">>> ctor Objektnummer: {}",objNum);
}
~CAny()
{
std::println("<<< dtor Objektnummer: {}",objNum);
}
void Print()
{
std::println(" Objektnummer {}",objNum);
}
};
int main()
{
// shared_ptr auf CAny-Objekt
std::println("shrptr1 definieren und mit Objekt belegen:");
auto shrptr1 = std::make_shared<CAny>();
shrptr1->Print();
std::println("Anzahl der CAny-Refenzen: {}", shrptr1.use_count());
// 2. shared_ptr auf das selbe CAny-Objekt
std::println("shrptr1 zuweisen an shrptr2:");
std::shared_ptr<CAny> shrptr2 = shrptr1;
shrptr2->Print();
std::println("Anzahl der CAny-Refenzen: {}", shrptr1.use_count());
// shrptr1 gibt Objekt frei,
// aber shrptr2 hat Objekt noch reserviert!
std::println("shrptr1 loescht Verweis auf CAny-Objekt");
shrptr1.reset();
std::println("Anzahl der CAny-Referenzen nach reset(): {}",
shrptr2.use_count());
std::println("Ende main()");
}
shrptr1 definieren und mit Objekt belegen:
>>> ctor Objektnummer: 0
Objektnummer 0
Anzahl der CAny-Refenzen: 1
shrptr1 zuweisen an shrptr2:
Objektnummer 0
Anzahl der CAny-Refenzen: 2
shrptr1 loescht Verweis auf CAny-Objekt
Anzahl der CAny-Referenzen nach reset(): 1
Ende main()
<<< dtor Objektnummer: 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.
#include <memory>
#include <print>
class CAny
{
unsigned short objNum;
inline static unsigned short objCnt{0};
public:
CAny()
{
objNum = objCnt++;
std::println(">>> ctor Objektnummer: {}",objNum);
}
~CAny()
{
std::println("<<< dtor Objektnummer: {}",objNum);
}
void Print()
{
std::println(" Objektnummer {}",objNum);
}
};
int main()
{
// weak_ptr ohne Verweis definieren
std::weak_ptr<CAny> wkptr;
{ // ** Block zur Begrenzung der Lebensdauer
// ** des nachfolgenden shared_pointers
// shared_ptr mit Referenz auf CAny-Objekt
std::println("CAny ueber shrptr1 definieren:");
std::shared_ptr<CAny> shrptr1 =
std::make_shared<CAny>();
shrptr1->Print();
std::println("Anzahl CAny Refenzen: {}", shrptr1.use_count());
// Dem weak_ptr eine Referenz auf das ueber den
// shared_ptr referenzierte Objekt zuweisen
std::println("shrptr dem wkptr zuweisen:");
wkptr = shrptr1;
std::println("wkptr Refenzen: {}", wkptr.use_count());
// Zugriff auf das referenzierte Objekt
// nur ueber einen weiteren shared_ptr moeglich
std::println("wkptr fuer Zugriff auf CAny:");
std::shared_ptr<CAny> shrptr2 = wkptr.lock();
shrptr2->Print();
std::println("Anzahl CAny Refenzen: {}", shrptr2.use_count());
// Gueltigkeit des Objekts im weak_ptr ausgeben
std::println("wkptr expired: {}", wkptr.expired());
// shared_ptr und das Objekt löschen
} // Hier wird das CAny Objekt geloescht!!
std::println("wkptr expired nach Loeschen der shrptr: {}", wkptr.expired());
}
CAny ueber shrptr1 definieren:
>>> ctor Objektnummer: 0
Objektnummer 0
Anzahl CAny Refenzen: 1
shrptr dem wkptr zuweisen:
wkptr Refenzen: 1
wkptr fuer Zugriff auf CAny:
Objektnummer 0
Anzahl CAny Refenzen: 2
wkptr expired: false
<<< dtor Objektnummer: 0
wkptr expired nach Loeschen der shrptr: true