In neuen Versionen von C++ findet man eine Alternative zu
new/delete Funktionen, die die Effizienz dynamischer
Objekte mit dem Besitzverhalten eines Wertes oder Containers
kombinieren. Dadurch kann das korrekte Löschen der Objekte automatisert
werden, wenn keine Referenzen auf das Objekt mehr bestehen:
unique_ptr, die eindeutig auf ein
Objekt zeigen (und den Besitz dieses Objekts haben)shared_ptr, die ggf. mehrfach auf
ein Objekt zeigen (und alle den Besitz dieses Objekts teilen)weak_ptr, die aus einem
shared_ptr erzeugt werden (und keinen Besitz an diesem
Objekt haben)Dereferenzieren funktioniert, außer bei weak_ptr,
genauso wie bei einfachen Pointern, get() gibt einen
einfachen Pointer auf das Objekt zurück. Bei weak_ptr
erfolgt der Zugriff auf das Objekt durch Aufruf der Funktion
lock() , um zu testen, ob der zugehörige
shared_ptr noch gültig ist. Für die Nutzung der Smart
Pointer muss der Header <memory> eingebunden werden.
Die Smart Pointer sind Teil der Standartbibliothek std.
Wenn der Smart Pointer erstellt wird, wird Speicherplatz reserviert und
das Objekt konstruiert. Smart Pointer selber werden wie andere Daten
automatisch gelöscht, wenn sie die Gültigkeit verlieren. Gegenüber
klassischer Speicherverwaltung mit new und
delete haben Smart Pointer mehrere Vorteile:
delete sind nicht notwendig.
Allerdings sind ggf. Besitzzyklen zu vermeiden (siehe weak_ptr).Smart Pointer können auch in Feldern gespeichert werden, etwa zur Verwaltung mehrerer polymorpher Objekte. Während bei klassischen Pointern die Objekte des Feldes explizit gelöscht werden müssen, werden bei Smart Pointern die Destruktoren korrekt aufgerufen.
unique-pointer sind sehr ressourcenschonend: Sie
brauchen genauso viel Speicherplatz wie ein normaler Pointer. Wie der
Name schon verrät, hat das Objekt, auf den der unique-ptr
zeigt, genau einen Besitzer. Das heißt, dass es genau eine Instanz eines
Objekt gibt. Dementsprechend kann dieser Pointer auch nicht
einfach in einen anderen Pointer kopiert werden. Eine
Alternative ist der move()-Befehl. Dieser überträgt den
Wert des einen Pointers in den gewünschten neuen Pointer oder die
gewünschte Funktion. Der ursprüngliche Pointer ist danach ein
nullptr und wird nach Beendigung der Funktion/des Programms
gelöscht. Genauso kann man den Wert des Pointers (also das Objekt, auf
das gezeigt wird) nicht per call by value übergeben,
sondern muss per call by reference übergeben werden. Es ist
zu empfehlen, den Befehl make_unique() beim Erzeugen eines
Objektes zu benutzen. Wird der unique_ptr gelöscht, so wird
auch das Objekt zerstört.
Beispiel:
#include <memory>
#include <iostream>
int squareIt(std::unique_ptr<int> s) {
int squared = *s;
return squared*squared;
}
{
std::unique_ptr<int> number = std::make_unique<int>(15);
if(number){
std::cout << "Der Pointer `number` existiert noch und hat den Inhalt: " << *number << std::endl;
} else {
std::cout << "Der Pointer `number` existiert nicht mehr." << std::endl;
}
int numberSquared = squareIt(move(number));
std::cout << "Der Wert des Pointers wurde zur Berechnung übergeben: " << numberSquared << std::endl;
if(number){
std::cout << "Der Pointer `number` existiert noch und hat den Inhalt: " << *number << std::endl;
} else {
std::cout << "Der Pointer `number` existiert nicht mehr." << std::endl;
}
}
Man kann die Pointer auch in Containerklassen (siehe Abschnitt zu Containerklassen für mehr
Informationen darüber) speichern.
Ein Beispiel dafür:
#include <memory>
#include <string>
#include <iostream>
#include <vector>
class Animal{
private:
std::string species;
int age;
double weight;
public:
std::string getSpecies(){return species;}
int getAge(){return age;}
double getWeight() {return weight;}
Animal(){};
Animal(std::string species, int age, double weight);
};
Animal::Animal(std::string species, int age, double weight):
species(species), age(age), weight(weight)
{
}
{
std::vector<std::unique_ptr<Animal>> animalVector;
std::unique_ptr<Animal> cat = std::make_unique<Animal>("Cat", 7, 3.9);
std::unique_ptr<Animal> dog = std::make_unique<Animal>("Dog", 11, 36.4);
std::unique_ptr<Animal> turtle = std::make_unique<Animal>("Turtle", 52, 1.5);
animalVector.push_back(move(cat));
animalVector.push_back(move(dog));
animalVector.push_back(move(turtle));
if(cat){
std::cout << "Der Pointer `cat` existiert noch mit Inhalt: " << cat->getSpecies() << ", age: " << cat->getAge() << ", weight: " << cat->getWeight() << std::endl;
} else {
std::cout << "Der ursprüngliche Unique_Ptr `cat` ist jetzt nullpointer." << std::endl;
}
if(dog){
std::cout << "Der Pointer `dog` existiert noch mit Inhalt: " << dog->getSpecies() << ", age: " << dog->getAge() << ", weight: " << dog->getWeight() << std::endl;
} else {
std::cout << "Der ursprüngliche Unique_Ptr `dog` ist jetzt nullpointer." << std::endl;
}
if(turtle){
std::cout << "Der Pointer `turtle` existiert noch mit Inhalt: " << turtle->getSpecies() << ", age: " << turtle->getAge() << ", weight: " << turtle->getWeight() << std::endl;
} else {
std::cout << "Der ursprüngliche Unique_Ptr `turtle` ist jetzt nullpointer." << std::endl;
}
for(auto& i : animalVector){
std::cout << "Species: " << i->getSpecies() << ", Age: " << i->getAge() << " years, Weight: " << i->getWeight() << " kg" << std::endl;
}
}
shared_ptr sind ähnlich zu unique_ptr,
haben aber den großen Unterschied, dass ein Objekt mehr als einen
Besitzer haben kann. Das heißt allerdings im Umkehrschluss, dass
shared_ptr einen höheren Speicherbedarf und
Verwaltungsaufwand haben als unique_ptr (Es ist
also empfehlenswert, unique_ptr überall zu nutzen, wo es
möglich ist). Die Anzahl der Referenzen/Besitzer, die auf das
Objekt zeigen, werden entsprechend hoch- und runtergezählt. Wenn dieser
Counter auf 0 gefallen ist (es zeigt kein shared_ptr mehr
auf dieses Objekt), wird das Objekt gelöscht. Daduruch können
shared_ptr auch kopiert und ohne den
move()-Befehl an Funktionen oder Container übergeben
werden. Das heißt, dass shared_ptr ihren Wert auch per
call by value übergeben können (bspw. an Funktionen) und
nicht nur per pass by reference. Es ist zu empfehlen, den
Befehl make_shared() zum Erzeugen eines Shared-Pointers zu
benutzen.
Das obige Beispiel sieht nun so aus:
#include <iostream>
#include <memory>
int squareIt(std::shared_ptr<int>& s) {
return (*s)*(*s);
}
{
std::shared_ptr<int> i_number = std::make_shared<int>(15);
if(i_number){
std::cout << "Der Pointer `i_number` existiert noch und hat den Inhalt: " << *i_number << std::endl;
} else {
std::cout << "Der Pointer `i_number` existiert nicht mehr." << std::endl;
}
int numberSquared = squareIt(i_number);
std::cout << "Der Wert des Pointers wurde zur Berechnung übergeben: " << numberSquared << std::endl;
if(i_number){
std::cout << "Der Pointer `i_number` existiert noch und hat den Inhalt: " << *i_number << std::endl;
} else {
std::cout << "Der Pointer `i_number` existiert nicht mehr." << std::endl;
}
}
Genauso lassen sich shared_ptr ebenfalls in
Containerklassen (siehe Abschnitt zu
Containerklassen für mehr Informationen darüber) speichern.
Das obige Beispiel sieht mit shared_ptr so aus:
#include <memory>
#include <string>
#include <iostream>
#include <vector>
class Animal{
private:
std::string species;
int age;
double weight;
public:
std::string getSpecies(){return species;}
int getAge(){return age;}
double getWeight() {return weight;}
Animal(){};
Animal(std::string species, int age, double weight);
};
Animal::Animal(std::string species, int age, double weight):
species(species), age(age), weight(weight)
{
}
{
std::vector<std::shared_ptr<Animal>> animalVector;
std::shared_ptr<Animal> cat = std::make_shared<Animal>("Cat", 7, 3.9);
std::shared_ptr<Animal> dog = std::make_shared<Animal>("Dog", 11, 36.4);
std::shared_ptr<Animal> turtle = std::make_shared<Animal>("Turtle", 52, 1.5);
animalVector.push_back(cat);
animalVector.push_back(dog);
animalVector.push_back(turtle);
if(cat){
std::cout << "Der Pointer `cat` existiert noch mit Inhalt: " << cat->getSpecies() << ", age: " << cat->getAge() << ", weight: " << cat->getWeight() << std::endl;
} else {
std::cout << "Der ursprüngliche Pointer `cat` ist jetzt nullpointer." << std::endl;
}
if(dog){
std::cout << "Der Pointer `dog` existiert noch mit Inhalt: " << dog->getSpecies() << ", age: " << dog->getAge() << ", weight: " << dog->getWeight() << std::endl;
} else {
std::cout << "Der ursprüngliche Pointer `dog` ist jetzt nullpointer." << std::endl;
}
if(turtle){
std::cout << "Der Pointer `turtle` existiert noch mit Inhalt: " << turtle->getSpecies() << ", age: " << turtle->getAge() << ", weight: " << turtle->getWeight() << std::endl;
} else {
std::cout << "Der ursprüngliche Pointer `turtle` ist jetzt nullpointer." << std::endl;
}
for(auto& i : animalVector){
std::cout << "Species: " << i->getSpecies() << ", Age: " << i->getAge() << " years, Weight: " << i->getWeight() << " kg" << std::endl;
}
}
weak_ptr sind eine spezielle Art
shared_ptr: Sie existieren nur, wenn sie mit einem
zugehörigen shared_ptr verbunden sind. Sie ermöglichen
Zugriff auf ein Objekt, erhöhen aber nicht den Referenzzähler des
shared_ptr; sie haben also keine Besitzrechte an dem
Objekt. Man benutzt weak_ptr, um sogenannte Besitzzyklen in
Datenstrukturen zu vermeiden. Wenn keine shared_ptr mehr
auf das Objekt zeigen, wird es gelöscht, selbst wenn noch
weak_ptr darauf zeigen.
Das folgende Beispiel zeigt den sinnvollen Einsatz der
weak_ptr:
#include <memory>
#include <string>
#include <list>
#include <vector>
#include <iostream>
class Person
{
public :
Person() = delete ;
Person(const std::string& name , std::shared_ptr<Person> father = nullptr , std::shared_ptr<Person> mother = nullptr) ;
virtual ~Person() ;
void setSibling(std::weak_ptr<Person> sibling);
private :
std::shared_ptr<Person> p_father;
std::shared_ptr<Person> p_mother;
const std::string p_sName;
std::vector<std::weak_ptr<Person>> p_siblings;
};
Person::Person(const std::string& name, std::shared_ptr<Person> father, std::shared_ptr<Person> mother) :
p_sName(name), p_father(father), p_mother(mother)
{
}
void Person::setSibling(std::weak_ptr<Person> brosis)
{
p_siblings.push_back(brosis);
}
Person::~Person()
{
p_siblings.clear();
std::cout << "Geloescht: " << p_sName << std::endl;
}
void test1 ()
{
auto m1 = std::make_shared<Person>("Josef");
auto f1 = std::make_shared<Person>("Maria");
auto m2 = std::make_shared<Person>("Peter", m1 , f1 );
auto f2 = std::make_shared<Person>("Birgit", m1 , f1 );
m2->setSibling(f2);
f2->setSibling(m2);
}
{
std::cout << "Anfang" << std::endl;
test1();
std::cout << "Ende" << std::endl;
}
Wenn man statt weak_ptr im Vektor p_siblings
shared_ptr speichert, wird der Speicher im Programm nicht
automatisch freigegeben, da Peter und Birgit gegenseitig aufeinander
zeigen. Sie können gerne an entsprechenden Stellen im Beispiel (in der
Funktion setSibling() und im Vektor
p_siblings) die weak_ptr durch
shared_ptr ersetzen und schauen, was passiert. Erst wenn
man durch weak_ptr den zyklischen Besitz auflöst, werden
die Destruktoren wie erwartet aufgerufen.
Beim weak_ptr kann man abfragen, ob der zugehörige
shared_ptr noch definiert ist. Dies geschieht durch die
Funktion lock(). Sie liefert einen Nullzeiger, wenn das
Objekt nicht mehr existiert. Diese Funktion muss eingesetzt werden, wenn
man auf den Inhalt des weak_ptr zugreifen möchte. Ein
weak_ptr in der Parameterliste (wie bei
setSibling) macht deutlich, dass man beim Aufruf
shared_ptr dort nicht mit move() einen
Besitzwechsel initiieren sollte.
Ein Beispiel mit der Funktion lock():
#include <iostream>
#include <memory>
void doesWeakExistProperly(std::weak_ptr<int> weak){
if(weak.lock()){
std::cout << "Der Pointer wurde erfolgreich gelocked und hat den Inhalt: " << *weak.lock() << std::endl;
} else {
std::cout << "Der Pointer konnte nicht gelocked werden." << std::endl;
}
}
{
std::weak_ptr<int> weak;
doesWeakExistProperly(weak);
{
auto shared = std::make_shared<int>(42);
weak = shared;
std::cout << "Der Pointer `weak` ist jetzt mit `shared` gepaired." << std::endl;
doesWeakExistProperly(weak);
}
std::cout << "Der Pointer `shared` ist jetzt out of scope." << std::endl;
doesWeakExistProperly(weak);
}
Übungsaufgaben zu diesem Kapitel finden Sie hier.