Sven Johannsen 16.1.2013 Version: 1.0 C++ User Group Meeting NRW |
sven@sven-johannsen.de |
In C++ ist der Umgang mit dynamischem Speicher nicht unproblematisch.
Nicht freigegebener Speicher (Memory leaks) und Zugriffsverletzung können jederzeit Probleme verursachen.
Die Verwendung von Smart-Pointern kann diese Problematik entschärfen, in dem sie die Verwendung von dynamisch angefordertem Speicher vereinfacht.
Die meisten Beispiele setzen die Klassen MyObject
, OtherObject
und die freie Funktion maybeThrowing()
voraus.
Die Rückgabe von getOtherObject()
muss mit delete
freigegeben werden.
Es ist nicht klar ob maybeThrowing()
eine Exception wirft oder nicht.
Speicherleck (englisch memory leak, gelegentlich auch Speicherloch oder kurz memleak) bezeichnet einen Fehler in einem Computerprogramm, der dazu führt, dass ein laufender Prozess einen Speicherbereich zwar belegt, diesen jedoch weder freigeben noch nutzen kann.
(Wikipedia)
Dynamischer Speicher (Speicher, der auf dem Heap allokiert wurde) muss wieder freigegeben werden.
Wenn p
nicht freigegeben wird, steht der Speicher dem Programm in Zukunft nicht mehr zu Verfügung.
Auch wenn p
überschrieben kann der ursprüngliche Speicher nicht freigegeben werden.
Jeder Aufruf klaut dem Programm (und System) freien Speicher.
Memory Leak a: Ein Programm allokiert Speicher und gibt ihm nicht mehr dem System zurück und
Memory Leak b: verliert die Referenz auf den Speicher und kann daher ihn nicht mehr dem System zurückgeben oder nutzen.
Eigentlich ist alles ganz einfach:
Jedem Aufruf von new
(new[]
, oder malloc
) muss ein Aufruf von delete
(delete[]
bzw. free
) gegenüberstehen:
Aber nicht alle Funktionen verlaufen linear…
Aber selbst, wenn man alles richtig macht, ist der Code nicht optimal.
(1) und (2) = doppelter Code, der nicht exakt gleich ist.
In C++ können Funktionsrückgaben ignoriert werden.
Die Funktion f()
gibt Speicher zurück, der freigegeben werden muss.
Dieser Rückgabewert kann ignoriert werden. -> Memory Leak!
Und manchmal sind die Datenstrukturen und der Code so komplex, dass nicht klar ist war, wann und wo den Speicher frei geben müsste.
Der Programmablauf ist nicht einfach zu durchschauen, wenn Exceptions ins Spiel kommen.
maybeThrowing
eine Exception?p
& q
freigegeben? (Nein!)Eine Behandlung der Exceptions macht das Programm nicht leserlicher.
Der Quellcode nicht einfacher zu lesen, man baut zusätzliche Abhängigkeiten ein, die man eigentlich durch die Verwendung von Exceptions vermeiden wollte und wieder wurde Code dupliziert.
Exceptions sollten für eine Trennung von Programmlogik und Fehlerbehandlung sorgen. Dies funktioniert bei (Raw-)Pointern nicht!
Der Destruktor einer Klasse wird nur dann aufgerufen, wenn die Klasse vollständig erzeugt wurde, also der Konstruktor vollständig durchlaufen wurde.
(1) Was passiert, wenn loadPixelData
eine Exception auslöst?
Pointer besitzen nur 2 gültige Zustände:
Der Zustand "nicht initialisiert" ist für Pointer nicht vorgesehen. Ein Zeiger, der nicht initialisiert wurde kann nicht auf Gültigkeit geprüft werden und darf nicht freigegeben werden!
delete
gibt nur den Speicher frei, verändert jedoch nicht den Zeiger.
Nach dem Aufruf von delete
muss der Zeiger auf 0 gesetzt werden.
What about crashes, buffer overflows, memory leaks?
These issues are mostly in past for modern C++ development. Smart pointers, STL, Boost and other decent programming tools allow to write safe code easily and fast.
http://cppcms.com/wikipp/en/page/rationale
Smart-Pointer sind
C++-Klassen können wie Zeiger verwendet werden, wenn
überschrieben worden sind.
Das Überladen der Operatoren *
und ->
sind keine außergewöhnliche C++-Tricks.
Gleiche Technik wie bei den Iteratoren der STL!
So können die Objekte einer Smart-Pointer Klasse, überall dort eingesetzt werden, wo bisher Zeiger verwendet wurden.
Und als Klasse sorgt der Konstruktor für die Initialisierung
Der Smart-Pointer ist der Besitzer für den allokierten Speicher
und damit Verantwortlich für die korrekt Freigabe.
Smart-Pointer verwenden das Resource Acqusiation is initialisation (RAII)-Prinzip um sicherzustellen, dass der Speicher wieder freigegeben wird.
Bei der Initialisierung einer Instanz (object) wird eine Ressource angefordert. Die Ressource wird bei der Zerstörung der Instanz wieder freigegeben.
Beispiel:
Umständlich: (old style) | Einfacher: (RAII) |
---|---|
Unter C++ wird der Destruktor immer, in einer definierten Reihenfolge und "sofort" aufgerufen. Das gilt für:
Damit wird sichergestellt, dass eine Ressource zuverlässig freigegeben wird.
Bei den Smart-Pointer liegt der Schwerpunkt im 2. Teil von RAII:
Der Destruktor sorgt zuverlässig für die Freigabe des Speichers!
Ein wichtiger Aspekt ist das Regeln der Besitzverhältnisse ("Ownership"): Also wer löscht den Speicher.
if(p){...}
) kann eine Konvertierung zu anderen Typen ermöglichen.Viele moderne (C++) Klassen Bibliotheken bringen Smart-Pointer mit:
oder
Die Smart-Pointer können nach ihrem Kopierverhalten unterschieden werden. Dies sind:
3 klassische Typen
1 speziellen Typ
(Kopie im Sinne von "flache Kopie". Es wird der Zeiger und nicht der Inhalt kopiert)
Smart-Pointer, die keine Kopien oder Zuweisung zulassen.
Ohne Zuweisung und Kopie muss auch "nichts" zusätzlich verwaltet werden: Ein Smart-Pointer kümmert sich um einen Speicherbereich. Damit ist ein scoped_ptr so effektiv wie Hand-Programmierter Code.
Verwendung:
Der scoped_ptr wird zum Aufbewahren von Speicher verwendet, welcher beim Verlassen des Scopes wieder freigeben werden soll. Z.B.:
Beispiele:
(scoped_ptr mit R-Value Unterstützung)
Objekt in STL Container müssen Kopierbar oder Verschiebbar sein. Der unique_ptr kann "verschoben" werden.
Verwendung: STL Container Funktionsrückgabe.
Smartpointer, die Kopien erlauben, bzw. sich die "Ownership" teilen.
Wenn der letzte Zeiger für eine Speicheradresse stirbt, wird der Speicher freigegeben. ("Der letzte macht das Licht aus!")
Die übliche Implementierungstechnik verwendet einen internen Zähler (Reference counter).
Dabei kann dieser Smart-Pointer-Typ auch ohne Zähler implementiert werden.
(Beispiel: zyklische Kette, siehe Modern C++ Design Kapitel "Reference Linking".)
Beispiele:
Die shared_ptr können vielfältig verwendet werden:
Eigentlich können shared_ptr fast überall dort verwendet werden, wo bisher herkömmliche Zeiger verwendet wurden. Die einzige Ausnahmen sind Zeiger auf Felder.
Der std::shared_ptr kann sehr universell und flexibel eingesetzt werden.
Dies bedingt einen gewissen Overhead:
Lösung :
Eine "hand-programmierte" Speicherwaltung bringt ebenfalls ihren Overhead und ist ggf. Fehlerbehaftet.
Verweist auf den gleichen Speicher wie ein shared_ptr
, ohne dafür Verantwortlich zu sein.
Indirekter Zugriff auf Speicher:
expired()
)lock()
)AddRef und Release müssen unterstützt werden.
Der Smart-Pointer gibt den Speicher selbst nicht frei – er ruft AddRef()
und Release()
korrekt auf.
Das COM-Object muss mit einem internen Zähler erkennen, dass kein Zeiger mehr das Objekt referenziert.
Beispiele:
http://msdn.microsoft.com/en-us/library/vstudio/hh279683.aspx
Besitz-Semantik: Der Besitzer gibt Speicher frei
Beim Kopieren wechselt der Besitz.
Wenn der Besitzer zerstört wird können andere auto_ptr auf ungültigen Speicher zeigen. (z.B. In STL Containeren nach dem Aufruf von std::sort)
Vorteil:
Wird mit jedem C++-Compiler ausgeliefert!
Anmerkung:
const auto_ptr
verhält sich fast wie ein Scoped-Pointer. Das const
verbietet die Kopie, aber nicht die Freigabe des Speichers!std::shared_ptr
& std::unique_ptr
verwendet werden.Smart-Pointer für Felder heißen
Naja, nicht ganz. Natürlich sind die aufgezählten Typen Klassen für Felder und keine SP.
Selbstverständlich gibt es spezielle Smart-Pointer für Felder. Diese geben den verwalteten Speicher mit 'delete[]' statt 'delete' frei.
(Echte) Smart-Pointer (für Felder) verwalteten nur den Speicher, aber nicht die Größe des verwalteten Speichers.
Container verwalten Speicher und Größe. (z.B. size(), capacity(), …)
std::vector
und std::string
müssen den Speicher am Stück allokieren. Daher eignen sie besonders gut sich für 1:1 Umstellungen, da die Adresse des 1. Element dem Zeiger auf den Buffer entspricht. API-Aufrufe, die einen Zeiger auf Felder erwarten, können so aufgerufen werden. (oder vector::data()
, C++11)
Zeiger auf ein char-Feld, das einen String repräsentieren, ersetzt man am sinnvollsten mit der Klasse std::string oder einer vergleichbaren String-Klasse.
Statt string::data()
sollte string.c_str()
verwendet werden (Null-Terminierung).
A zeigt auf B; B zeigt auf A
Der Speicher sollte von der gleichen C-Runtime freigegeben werden, die den Speicher allokiert hat.
Beispiele:
Dll A.dll verwendet stlport, Dll B.Dll verwendet Compiler STL.
Lösung: deleter übergeben.
(Vergleichbare Probleme gibt es auch für Exceptions.)
Beispiel:
In (STL-)Container nur Smart-Pointer mit Referenz-Zählung verwenden (z.B. boost::shared_ptr
). Alles andere ist gefährlich und nicht zukunftssicher.
Alternativen: (C++11: std::unique_ptr. Boost Pointer Container Library)
Container geben nur den eigenen Speicher frei, die Speicherfreigabe bei Container von Pointern ist fehlerträchtig. (Siehe Exception Safety)
Der shared_ptr könnte eigentlich alle vorhandenden Pointer ersetzen.
Durch die Verwendung eines scoped_ptr
/unique_ptr
kann man dem Pointer ein weiteres Attribut mit geben, das einen Hinweis gibt, dass dieser Zeiger diese Funktion / Klassen nie verlassen wird.
Damit hat man ein semantisches Mittel, dass die Verwendung des Zeigers beschreibt.
In C und C++ kann der Rückgabewert einer Funktion ignoriert werden. D.H. man ruft eine Funktion (Unterprogramm mit Rückgabewert) auf und behandelt die Funktion wie eine Prozedure bzw. Subroutine (Unterprogramm ohne Rückgabewert).
Mit diesem Wissen darf man eigentlich keine Funktion programmieren, welche dynamisch erzeugten Speicher als Rückgabewert zurückgibt. Wenn der Rückgabewert ignoriert wird, kann dieser Speicher nicht mehr freigegeben werden.
Die Verwendung von rohem Speicher (echte C++/C-Pointer) verzichten.
Mit der Verwendung von Smart-Pointer kann auch in größere Projekt komplett auf den Aufruf von delete verzichtet werden.
Seit dem C++11 (bzw. TR1) enhält C++ die wichtigsten Smart-Pointer.
Bis wenige Ausnahmen werden (für Smart-Pointer) keine weiteren Bibliotheken benötigt.
get()
, operator*()
, operator->()
: Gibt den internen Pointer nach aussen, dereferenziert (Pointer mimic)reset(p)
: Ersetzt den Pointer. (Gibt original Pointer frei.)swap(other)
: tauscht 2 Smart Pointer aus.swap
ruft Member-Funktion auf.operator bool() const
: p != nullptr
nullptr_t
: ruft reset()
auf.Functor-like Klasse um Benutzer definierte Freigabe-Funktion und Allokation zu steuern.
Der deleter wird von reset und dem Destruktor aufgerufen.
Mit dem Allocator wird z.B. der Speicher für das Zusatzobjekt (count) beim shared_ptr
angelegt.
Smart-Pointer ohne Kopie-Verwaltung (scoped_ptr), aber mit RValue Unterstützung.
~unique_ptr()
) gibt den Pointer nicht selbst frei, sondern ruft den deleter auf.release()
: Gibt den Pointer zurück, setzt dein internen Zustand auf nullptr, ohne den Speicher freizugeben.std::auto_ptr&&
initialisiert werden (z.B. Funktionen, die auto_ptr zurückgeben).delete[]
statt delete
auf.==
, !=
, <
, <=
, >
, >=
,) für 2 unique_ptr
sowie unique_ptr
und nullptr
.Smart-Pointer bei dem viele Instanzen auf den gleichen Speicher zeigen können.
bad_weak_ptr
: Wird geworfen, wenn ein shared_ptr von einem weak_ptr erzeugt wird.allocator
: Wird dem Konstruktor als Parameter übergeben.std::auto_ptr&&
und unique_ptr&&
initialisiert werden. Kann mit Funktionen, die einen unique_ptr (legacy: auto_ptr) zurückgeben initialisiert werden.release()
use_count()
: Anzahlt der shared_ptr
, die auf den gleichen Speicher zeigen.unique()
: use_count() == 1
owner_before()
für shared_ptr
und weak_ptr
:make_shared()
: Erzeugt Objekt und Counter mit einem Aufruf von newallocate_shared()
: wie make_shared()
, nur dass ein Allocator übergeben werden kann.==
, !=
, <
, <=
, >
, >=
,) für 2 shared_ptr
sowie shared_ptr
und nullptr
operator<<(basic_ostream& os, const shared_ptr& p)
ruft os << p.get();
auf.static_pointer_cast
:dynamic_pointer_cast
:const_pointer_cast
:Diesmal ist MyObject
die Basis Klasse von OtherObject
.
Teil sich mit einem shared_ptr den Speicher ohne Ownership.
expired()
: Hat der shared_ptr
den Speicher freigegeben?lock()
: Gibt einen shared_ptr
von einem weak_ptr
zurück.shared_from_this
: gibt einen shared_ptr
from this
zurück, wenn Klasse von enable_shared_from_this
abgeleitet ist.
Der Originale Zeiger muss bereite durch einen shared_ptr
verwaltet werden; shared_from_this
erzeugt keinen neuen shared_ptr
.
(1) :
d-tor : root
d-tor : first
d-tor : first
d-tor : second
d-tor : third
vorher: | nachher: |
---|---|
vorher: | nachher: |
---|---|
vorher: | nachher: |
---|---|
nachher: | vorher: |
---|---|
/
#