Bei Smart Pointern handelt es sich um ein modernes C++-Feature, das automatische Speicherverwaltung bietet und dabei hilft, Speicherlecks und Dangling Pointer zu vermeiden, die häufig im Zusammenhang mit Raw Pointern auftreten. Sie fungieren als Wrapper um Raw Pointer und fügen zusätzliche Funktionalität hinzu, etwa die automatische Speicherfreigabe, wenn der Pointer nicht mehr benötigt wird.
Smart Pointer werden typischerweise als Klassenvorlagen in der C++-Standardbibliothek implementiert.
Die beiden am häufigsten verwendeten Smart Pointer sind std::unique_ptr und std::shared_ptr.
std::unique_ptr ist ein Smart Pointer, der das Objekt exklusiv besitzt.
Er stellt sicher, dass zu jedem Zeitpunkt nur ein std::unique_ptr-Objekt die Ressource besitzt.
Wenn der besitzende std::unique_ptr zerstört oder zurückgesetzt wird, zerstört er die Objekte automatisch und gibt ihren Speicher frei.
#include <memory>
// Declaring and defining a unique pointer
auto rightful_king_of_england = std::make_unique<std::string>("Excalibur");
// Unique pointers cannot be copied or assigned
auto mordred = rightful_king_of_england; // Error: Cannot copy a unique_ptr
std::make_unique()
Wenn du einen std::unique_ptr erstellst, ist es besser, std::make_unique() zu verwenden, anstatt direkt new zum Reservieren von Speicher zu benutzen.
std::make_unique() bietet mehrere Vorteile:
std::make_unique() garantiert Ausnahmesicherheit.
Wenn während der Konstruktion des Objekts eine Ausnahme ausgelöst wird, wird Speicher automatisch freigegeben, wodurch Speicherlecks vermieden werden.std::make_unique() macht den Code klarer und prägnanter.
Sie macht es überflüssig, den Typ, der reserviert wird, explizit anzugeben, da die Template-Argumente automatisch abgeleitet werden.std::make_unique() effektiver zu optimieren als eine manuelle Speicherreservierung mit new, was potenziell zu einer besseren Leistung führt.std::unique_ptr manuell konstruiert wird, kann die zugrunde liegende Ressource gelöscht werden.
Das würde zu undefiniertem Verhalten führen, wenn der std::unique_ptr versucht, sie am Ende seines Gültigkeitsbereichs zu löschen.std::shared_ptr ist ein Smart Pointer, der es mehreren std::shared_ptr-Objekten ermöglicht, sich das Eigentum an derselben Ressource zu teilen.
Er verfolgt, wie viele Shared Pointer auf die Ressource verweisen, und gibt den Speicher erst frei, wenn der letzte Shared Pointer, der die Ressource besitzt, seinen Gültigkeitsbereich verlässt oder zurückgesetzt wird.
// Declaring and defining a shared pointer to a dynamically allocated string
auto martian_congressional_republic = std::make_shared<std::string>("protomolecule");
// Creating more shared pointer that shares ownership
auto outer_planets_alliance = martian_congressional_republic;
auto united_nations = martian_congressional_republic;
In C++17 und darunter wird die Verwendung von std::shared_ptr mit Arrays über std::make_shared<T[]> nicht direkt unterstützt.
Es ist zwar möglich, Arrays mit std::make_shared<T[]> zu reservieren, aber das direkte Erstellen von Shared Pointern daraus kann zu undefiniertem Verhalten führen, weil sich die Speicherverwaltung zwischen einzelnen Objekten und Arrays unterscheidet.
Stattdessen kannst du std::vector oder benutzerdefinierte Löschfunktionen verwenden, um Arrays mit Shared Pointern effektiv zu verwalten.
Achte immer auf die Kompatibilität mit deinem Compiler und deiner Standardbibliotheksimplementierung, wenn du in C++17 mit Array-Reservierungen und Shared Pointern arbeitest.
std::make_shared()
Ähnlich wie std::make_unique() bietet std::make_shared() Vorteile wie verbesserte Speichereffizienz, Ausnahmesicherheit und Lesbarkeit.
Es kombiniert die Speicherreservierung für den Kontrollblock und das verwaltete Objekt in einem einzigen Vorgang, was die Effizienz erhöht und das Risiko von Speicherlecks verringert.
Außerdem vereinfacht die automatische Ableitung der Template-Argumente den Code und verbessert die Lesbarkeit.
Die Verwendung von std::make_shared() fördert saubereren, sichereren und effizienteren Code, wenn du in C++ mit std::shared_ptr-Objekten arbeitest.
std::weak_ptr ist eine Begleitklasse zu std::shared_ptr, die eine nicht besitzende, „schwache“ Referenz auf ein Objekt bereitstellt, das von einem Shared Pointer verwaltet wird.
Er ermöglicht den Zugriff auf die Ressource, ohne deren Lebensdauer zu beeinflussen.
Weak Pointer sind in Szenarien nützlich, in denen Zirkelbezüge aufgelöst werden müssen, um Speicherlecks zu vermeiden.
// Creating a shared pointer
auto your_account = std::make_shared<std::string>("secret_subscription_password");
// Creating a shared pointer that shares ownership
auto your_flatmates_account = your_account;
// Creating a weak pointer from the shared pointer
auto your_flatmates_boyfriends_account = your_flatmates_account;
// if your_account and your_flatmates_account are deleted, there is no more reference to the shared pointer.
// your_flatmates_boyfriends_account will be a null pointer and cannot use the associated object any longer.
std::weak_ptr und zyklisches Eigentumstd::weak_ptr wurde entwickelt, um das Problem des zyklischen Eigentums zu lösen, auch bekannt als Zirkelbezüge, die bei der Verwendung von std::shared_ptr auftreten können.
In einem Szenario mit zyklischem Eigentum verweisen zwei oder mehr std::shared_ptr-Objekte aufeinander und bilden einen Zyklus, in dem keines der Objekte gelöscht werden kann, weil sie starke Referenzen aufeinander halten, was zu Speicherlecks führt.
std::weak_ptr bietet eine Lösung für dieses Problem, indem er schwache Referenzen auf gemeinsam genutzte Objekte ermöglicht, ohne zu deren Referenzzähler beizutragen.
Das bedeutet, dass er das gemeinsam genutzte Objekt beobachten und darauf zugreifen kann, aber nicht verhindert, dass es gelöscht wird.
Wenn alle starken Referenzen auf das gemeinsam genutzte Objekt freigegeben werden, wird das Objekt zerstört, und Weak Pointer, die es beobachten, werden automatisch auf nullptr zurückgesetzt.
Hier ist ein kurzes Beispiel, das zeigt, wie du std::weak_ptr verwendest, um zyklisches Eigentum aufzulösen:
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::weak_ptr<Node> next;
};
// Creating two nodes
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Creating a cycle by making each node point to the other
node1->next = node2;
node2->next = node1;
// Release strong references to nodes
node1.reset();
node2.reset();
// Nodes are automatically destructed due to weak references
„Node constructed“ wird zweimal ausgegeben, gefolgt von zwei Zeilen mit „Node destructed“.
Wenn next kein Weak Pointer, sondern ein std::shared_ptr ist, entsteht eine Situation mit zyklischem Eigentum, die zu Speicherlecks führt, weil beide Knoten starke Referenzen aufeinander halten und dadurch ihre Zerstörung verhindern.
struct Node {
Node() { std::cout << "Node constructed\n"; }
~Node() { std::cout << "Node destructed\n"; }
std::shared_ptr<Node> next; // Change to shared_ptr
};
In diesem Szenario halten sowohl node1 als auch node2 über std::shared_ptr starke Referenzen aufeinander und bilden einen Zyklus.
Wenn das Programm beendet wird und versucht, die starken Referenzen mit reset() freizugeben, bleiben die Referenzzähler beider Knoten ungleich null, weil jeder Knoten eine starke Referenz auf den anderen hält.
Daher werden die Knoten nicht zerstört, was zu Speicherlecks führt.
„Node constructed“ wird zweimal ausgegeben, aber es folgt keine Zeile mit „Node destructed“.
std::weak_ptr
Dangling Pointer entstehen, wenn ein Pointer auf ein Objekt verweist, das gelöscht wurde. Beim Dereferenzieren des Pointers führt das zu undefiniertem Verhalten.
std::weak_ptr hilft, Dangling Pointer zu vermeiden, indem er eine nicht besitzende, schwache Referenz auf ein Objekt bereitstellt, das von std::shared_ptr verwaltet wird.
So vermeidest du Dangling Pointer mit std::weak_ptr:
Erstelle einen std::weak_ptr: Anstatt direkt einen std::shared_ptr zu halten, erstellst du einen std::weak_ptr auf das gemeinsam genutzte Objekt.
Prüfe die Gültigkeit: Bevor du den std::weak_ptr verwendest, prüfe seine Gültigkeit mit der Funktion expired().
Diese Funktion gibt true zurück, wenn das zugehörige gemeinsam genutzte Objekt gelöscht wurde.
Sperre den std::weak_ptr: Um sicher auf das gemeinsam genutzte Objekt zuzugreifen, verwendest du die Funktion lock(), die einen std::shared_ptr zurückgibt, der auf dasselbe Objekt zeigt, wenn es noch gültig ist.
Wenn das Objekt gelöscht wurde, gibt lock() einen leeren std::shared_ptr zurück.
Hier ist ein Codebeispiel, das zeigt, wie du std::weak_ptr verwendest, um Dangling Pointer zu vermeiden:
auto node1 = std::make_shared<Node>();
auto node2 = std::make_shared<Node>();
// Create weak pointers
node1->next = node2;
node2->next = node1;
// Check validity and lock
if (auto lockedNode2 = node1->next.lock()) {
// Use lockedNode2 safely
} else {
// Handle case where node2 has been deleted
}
In diesem Beispiel sind node1->next und node2->next std::weak_ptr-Objekte.
Bevor auf das gemeinsam genutzte Objekt zugegriffen wird, wird seine Gültigkeit mit der Funktion expired() geprüft.
Dann wird lock() verwendet, um sicher auf das gemeinsam genutzte Objekt zuzugreifen.
Dieser Ansatz stellt sicher, dass Dangling Pointer vermieden werden, wenn du mit std::weak_ptr arbeitest.
Verwende standardmäßig Smart Pointer: std::unique_ptr für exklusives Eigentum und std::shared_ptr für gemeinsames Eigentum.
Raw Pointer solltest du für nicht besitzende Referenzen oder für die Schnittstelle zu Legacy-Code reservieren.
In den meisten Fällen reicht std::unique_ptr für exklusives Eigentum aus, da er eine leichtgewichtige Speicherverwaltung ohne den Overhead der Referenzzählung bietet.
std::shared_ptr sollte sparsam eingesetzt werden, da er Overhead und Komplexität mit sich bringt, es sei denn, echtes gemeinsames Eigentum ist erforderlich.
std::weak_ptr ist darauf spezialisiert, zyklische Abhängigkeiten aufzulösen oder gemeinsam genutzte Objekte zu beobachten, wird aber nicht häufig verwendet.