Als Motivation für das eigentliche Thema dieses Abschnitts werden ein paar gefährliche, aber durchaus übliche Programmfragmente gezeigt. Durch die starke Verdichtung auf das Wesentliche sind die Probleme hoffentlich leicht zu erkennen; aber in realem Quelltext werden sie oft nicht erkannt, wodurch Speicherlecks entstehen (allokierter Speicher wird nicht freigegeben) oder es wird illegal auf ungültigen Speicher zugegriffen (weil ein Objekt beispielsweise bereits vor dem Zugriff freigegeben wurde). Letzteres führt -falls man Glück hat- zu einem Programmabbruch, oder zu fehlerhaften Daten.
Relativ harmlos erscheint es meistens, in einem Block einen Zeiger auf ein dort bekanntes Objekt zu richten (beispielsweise auf neu allokierten Speicher), über den Zeiger etwas mit dem Objekt zu machen, und praktisch zeitgleich mit dem Ende der Lebensdauer des Zeigers auch das Objekt zu zerstören, auf das der Zeiger verweist:
{ Cwassweissich *ptr = new Cwassweissich; tuwas( ptr ); // ... delete ptr; // hier endet die Lebensdauer des allokierten Objekts } // hier endet die Lebensdauer von ptr
Der Programmierer dieses Quelltextes gibt den allokierten Speicher sauber frei, bevor der Block verlassen wird.
Probleme treten regelmäßig auf, wenn der Programmfluß nicht so schön linear verläuft wie in dem obigen Beispiel, oder wenn die Lebensdauer des Zeigers und des darauf verwiesenen Objekts unterschiedlich sind.
Beispiele für heikle Fälle:
{ Cwassweissich *ptr = new Cwassweissich; tuwas( ptr ); if( einProblem ) return; // ... delete ptr; }Hier wird leicht übersehen, den Speicher im if-Zweig freizugeben.
Analoges gilt für ein break:
while( schoeneswetter ) { Cwassweissich *ptr = new Cwassweissich; tuwas( ptr ); if( einProblem ) break; // ... delete ptr; // ggf. wegen break nicht ausgeführt! }
In beiden Fällen würde die Lösung natürlich darin bestehen, im if-Zweig den Speicher freizugeben. Bei mehreren Zeigern in einem etwas verwickelten Unterprogramm wird das aber schnell unübersichtlich und fehleranfällig.
{ Cwassweissich *ptr = new Cwassweissich; Cwassweissich objekt; tuwas( ptr ); // hier tritt eine Ausnahme auf // ... delete ptr; // ggf. wegen Ausnahme nicht ausgeführt! }Durch den Mechanismus des stack unwinding (siehe Ausnahmebehandlung) werden durch die nicht gefangene Ausnahme alle Objekte auf dem Stack aufgelöst, also hier objekt ebenso wie der Zeiger ptr. Aber das, worauf der Zeiger verweist (nämlich der mit new allokierte Speicher), liegt nicht auf dem Stack und wird hier auch nicht freigegeben!
Der gezeigte Block wird dann verlassen (bis auf dem Stack weiter oben ein passendes try...catch gefunden wird), und der allokierte Speicher wird nicht mehr freigegeben.
Das Problem läßt sich lösen, indem man um jeden solchen Programmteil, in dem allokiert wird, ein eigenes try baut:
{ Cwassweissich *ptr = NULL; try { ptr = new Cwassweissich; Cwassweissich objekt; tuwas( ptr ); // hier tritt eine Ausnahme auf // ... delete ptr; } catch(...) { delete ptr; throw; } }Dadurch wird zwar das ursprüngliche Problem gelöst, aber schön ist das nicht: der Quelltext wird unnötig aufgebläht, das delete ist jetzt an zwei Stellen nötig, und die Lebensdauer des Zeigers ist jetzt ungleich der Lebensdauer des allokierten Objekts, womit wir schon beim nächsten kritischen Fall wären:
Ein offensichtliches Beispiel, bei dem der Zeiger länger lebt als das Objekt:
{ Cwassweissich *ptr; // Achtung! // Hier existiert der Zeiger ptr schon, aber man darf über // *ptr auf nichts zugreifen! ptr = new Cwassweissich; // ab jetzt ist *ptr verwendbar tuwas( ptr ); // ... delete ptr; // jetzt ist *ptr tabu // Achtung! // Hier existiert der Zeiger ptr noch, aber man darf über // *ptr auf nichts zugreifen! } // jetzt ist auch ptr verschwunden!
Cwassweissich *up() { Cwassweissich *ret = new Cwassweissich; // up() allokiert // ... return ret; } void Aufrufer() { Cwassweissich *ptr = up(); tuwas( ptr ); // ... delete ptr; // Aufrufer gibt frei }Die Freigabe beim Aufrufer ist natürlich anfällig dafür, vergessen zu werden oder möglicherweise mehrfach versucht zu werden.
Alle beschriebenen Probleme (außer dem letzten) lassen sich überraschend einfach lösen, wenn man den Zeiger in eine eigene Klasse ,,verpackt``:
class Cwassweissich_ptr_t { public: // Konstruktor Cwassweissich_ptr_t() : ptr( NULL ) { } // Konstruktor mit einem Zeiger auf Cwassweissich: Cwassweissich_ptr_t( Cwassweissich *rechteSeite ) : ptr( rechteSeite ) { } // Destruktor ~Cwassweissich_ptr_t() { delete ptr; } // Zuweisung Cwassweissich_ptr_t = Cwassweissich* Cwassweissich_ptr_t & operator=( Cwassweissich *rechteSeite ) { // bei mehreren Zuweisungen nacheinander die // vorherigen Zeiger freigeben: delete ptr; // neuen Zeiger merken: ptr = rechteSeite; return *this; } // Damit man ein *Cwassweissich_ptr_t anstelle eines // *Cwassweissich verwenden kann, bauen wir eine // passende Typkonvertierung: operator Cwassweissich*() const { return ptr; } private: // der eigentliche Zeiger auf den allokierten Speicher: Cwassweissich * ptr; }; // class Cwassweissich_ptr_t
Die Verwendung ändert sich geringfügig, weil statt eines
Cwassweissich*
jetzt ein Cwassweissich_ptr_t
verwendet
werden muß, wodurch ein manuelles Freigeben überflüssig wird (und auch
gar nicht mehr möglich ist):
{ Cwassweissich_ptr_t ptr = new Cwassweissich; tuwas( ptr ); // Ausnahme? Kein Problem! if( einProblem ) return; // return? Kein Problem! // ... // spätestens hier endet die Lebensdauer von ptr, // allokierter Speicher wird im Destruktor automatisch // freigegeben! }
Durch diesen Kunstgriff meisterlicher Hand stören Unterbrechungen im linearen Programmfluß nicht mehr weiter:
~Cwassweissich_ptr_t()
aufgerufen.
Und dabei wird wie gewünscht mit einem delete der mit new allokierte Speicher freigegeben.
Durch das Einbetten des Zeigers in ein automatisches Objekt auf dem
Stack wird also die Lebensdauer eines Heap-Objekts
(Cwassweissich
) an die Lebensdauer
des Stackobjekts (Cwassweissich_ptr_t
) gebunden, und die
Speicherverwaltung wird -ohne weiteres Zutun des Aufrufers- wieder
konsistent.
Das letzte der geschilderten Probleme (Allokieren in einer
Funktion, nötige Freigabe beim Aufrufer) läßt sich mit wenig
Mehraufwand lösen. Wenn in einer Funktion ebenfalls ein
Cwassweissich_ptr_t
erzeugt und als Rückgabewert an den
Aufrufer geliefert wird, müssen an
der Klasse Cwassweissich_ptr_t
nur wenige Änderungen gemacht werden:
Cwassweissich
) muß immer
genau ein Cwassweissich_ptr_t
(von möglicherweise mehreren
gleichzeitig existierenden) den Besitz haben. Dazu existiert ein zusätzliches
Element vom Typ bool; beispielsweise soll dieses owner
heißen.
Die Idee hinter diesem Klassenelement ist, daß es zwar möglicherweise mehrere
Cwassweissich_ptr_t
-Objekte zu einem allokierten Speicher gibt,
aber die Methoden so konstruiert sind, daß immer genau eines auch
Besitzer ist.
Cwassweissich_ptr_t
benötigt einen copy-Konstruktor, der
gegebenenfalls den Besitz owner von der rechten Seite (dem
Initialisierer) auf die linke Seite (das zu initialisierende Objekt)
überträgt, und beim Initialisierer löscht.
operator=()
einen Übergang des
Besitzes.
Cwassweissich_ptr_t
gibt den für das
zugehörige Cwassweissich
allokierten Speicher
nur dann frei, wenn es auch den Besitz daran
hat. Weil immer genau ein Cwassweissich_ptr_t
Besitzer des
allokierten Speichers ist, werden auch genau einmal delete und
dadurch der Destruktor aufgerufen.
Mit einem so erweiterten Cwassweissich_ptr_t
kann man in einem Unterprogramm ein Cwassweissich
allokieren, und als Cwassweissich_ptr_t
verpackt
zurückgeben:
Cwassweissich_ptr_t up() { Cwassweissich_ptr_t ret = new Cwassweissich; // up() allokiert // ... // hier wird ein temporäres Cwassweissich_ptr_t-Objekt // geschaffen, was den Besitz am allokierten Speicher übernimmt. // Deshalb gibt der Destruktor von ret keinen Speicher frei! return ret; } void Aufrufer() { // bei der Initialisierung geht der Besitz am allokierten // Speicher an ptr über. // Anschließend wird das temporäre Objekt freigegeben, // und der allokierte Speicher nicht freigegeben, weil // das temporäre Objekt keinen Besitz mehr hat. Cwassweissich_ptr_t ptr = up(); tuwas( ptr ); // ... // spätestens hier endet die Lebensdauer von ptr, // allokierter Speicher wird im Destruktor automatisch // freigegeben, weil ptr den Besitz am allokierten Speicher // hat! }Für die Rückgabe aus dem Unterprogramm wird der Compiler ein temporäres Objekt vom Typ
Cwassweissich_ptr_t
schaffen und
mit einem copy-Konstruktor initialisieren; dabei geht der Besitz auf
das temporäre Objekt über.
Der Aufrufer wird das Ergebnis des Funktionsaufrufs an eine
automatische Variable vom Typ Cwassweissich_ptr_t
zuweisen; dabei geht
durch den operator=()
der Besitz an diese über.
Mit dem Ende der Lebensdauer der automatischen Variable wird dann auch der allokierte Speicher freigegeben.
Soweit so gut. Mit dem bisherigen Wissen kann man also alle geschilderten Probleme leicht umgehen, wenn man sich für jeden Zeiger auf irgendeinen Typ die Mühe macht, und eine passende Verpackung baut und statt des Zeigers verwendet.
Weil diese Verpackung aber außer dem geschilderten Mechanismus mit delete und dem Besitz am allokierten Speicher nichts machen muß, und insbesondere nichts über den verpackten Datentyp wissen muß, bietet sich dafür eine template-Klasse an (siehe Klassenschablonen (template-Klassen)).
Und weil die Problematik sehr gängig ist und die beschriebene Lösung
sehr hilfreich, gibt es eine
solche template-Klasse bereits in der STL (in <memory>
).
Die Klasse heißt auto_ptr<T>
und
hat genau das oben beschriebene Verhalten.
Beispielverwendung:
#include <memory> using namespace std; // ... auto_ptr<Cwassweissich> up() { auto_ptr<Cwassweissich> ret( new Cwassweissich ); // up() allokiert // ... return ret; } void Aufrufer() { auto_ptr<Cwassweissich> ptr = up(); tuwas( ptr ); // ... } // hier automatische Freigabe mit delete!
Achtung! Auch hier gibt es wieder eine Fallgrube. Durch die Tatsache, daß beim Anlegen einer Kopie der Besitz auf die Kopie übergeht, muß man das Erzeugen temporärer Kopien durch den Compiler vermeiden. Diese werden beispielsweise beim Aufruf von Unterprogrammen angelegt, wenn ein Parameter als Wert übergeben wird (call by value). Mit dem Löschen dieses temporären Objekts würde auch der allokierte Speicher freigegeben werden.
Beispiel:
void up( Cwassweissich parameter ) { // ... } // ... auto_ptr<Cwassweissich> a( new Cwassweissich ); // a hat Besitz // für die Übergabe wird eine temporäre Kopie von a erzeugt, // dafür wird der copy-Konstruktor aufgerufen und der Besitz // am allokierten Speicher geht auf die temporäre Kopie über. // Die Kopie wird in up() als Parameter verwendet. up( a ); // Mit dem Ende der Funktion up() wird die temporäre Kopie // freigegeben, und der oben mit new allokierte Speicher wird // freigegeben, weil sie den Besitz daran hat. // Ab hier darf nicht mehr auf a zugegriffen werden!
Aus diesem Grund dürfen auto_ptr<T>
-Objekte niemals als Wert an
ein Unterprogramm übergeben werden, sondern nur als Referenz!
Zum Erzeugen eines auto_ptr<T>
-Objekts existieren
folgende Konstruktoren:
auto_ptr<T>
enthält kein Objekt.
auto_ptr<T>
enthält das Objekt und hat den Besitz daran.
auto_ptr
darf auch eine vom Typ des zu initialisierenden
Objekts abgeleitete Klasse sein (oder es muß eine andere automatische
Typumwandlung bestehen).
Daneben existiert noch wie bereits oben erwähnt eine Zuweisung, die
ein auf der linken Seite eventuell vorhandenes Objekt freigibt (falls
die linke Seite den Besitz daran hat), dann das Objekt von der rechten
auf die linke Seite kopiert, und dann den Besitz
überträgt (falls auf der rechten Seite vorhanden).
Der Typ des auf der rechten Seite angegebenen
auto_ptr
darf auch eine vom Typ
Objekts auf der linken Seite abgeleitete Klasse sein (oder es muß eine andere
automatische Typumwandlung bestehen).
Mit einer Memberfunktion get() kann man einen Zeiger auf das verwaltete Objekt bekommen (oder NULL, wenn kein solches existiert).
release() löscht den Besitz eines auto_ptr
, ohne das
Objekt zu zerstören. Rückgabewert ist ein Zeiger auf das enthaltene
Objekt, oder NULL wenn kein solches existiert.
Mit operator*
und operator->
kann man auf das verwiesene
Objekt oder seine Elemente zugreifen.
AnyWare@Wachtler.de