Autopointer: std::auto_ptr, std::tr1::shared_ptr

Inhalt
Einleitung
std::auto_ptr
std::tr1::shared_ptr

Einleitung.

In C++ ist der Programmierer selbst für die Verwaltung des dynamischen Speichers verantwortlich. Manchmal kommt es dazu, dass ein Speicherbereich reserviert, aber nicht wieder freigegeben wird – es entstehen so genannte Memoryleaks.

Es können offensichtliche Fehler sein wie in diesem Beispiel.

// Beispiel 1

#include <iostream>
using namespace std;

char* NumberToString(int i)
{
    char* string = new char[20];
    _itoa(i, string, 10);
    return string;
}

int main(void)
{

    char* buf = NumberToString(5);
    cout << buf << endl;

    cin.get();

    // Das Programm ist zu Ende und wir haben 20 Bytes Speicher verloren
    return 0;
}

Es können aber auch etwa Fehler sein, die man nicht so leicht findet. Vor allem, wenn der Code viele Sprunganweisungen enthält. Ein gutes Beispiel an dieser Stelle ist die Verwendung von Exceptions.

// Beispiel 2

#include <iostream>
using namespace std;

class Calculator
{
public:
    Calculator()
    {
        cout << "Konstruktor aufgerufen!" << endl;
    }

    ~Calculator()
    {
        cout << "Destruktor aufgerufen!" << endl;
    }

    int Divide(int a, int b)
    {
        if(b == 0)
            throw "Division durch Null ist nicht erlaubt!";

        return a/b;
    }
};


int main(void)
{

    try
    {
        Calculator *calc = new Calculator;
        cout << "Divide-Ergebnis: " << calc->Divide(4, 0) << endl;
        delete calc;
    }
    catch(const char* str)
    {
        cout << "Calculator Fehler: " << str << endl;
    }

    cin.get();

    // Das Programm ist zu Ende und wir haben 20 Bytes Speicher verloren
    return 0;
}

Ist bei der Division der zweite Operand gleich Null, so wird der try-Block durch die throw-Anweisung verlassen, was zur Folge hat, dass die Instanz calc nicht zerstört wird. Memoryleaks sind somit im wahrsten Sinne des Wortes vorprogrammiert. Man kann es gut daran erkennen, dass der Destruktor nicht aufgerufen wird, was normalerweise beim delete-Aufruf erfolgt.

Fehlermeldung

Um sich das Leben etwas leichter zu machen, sollte man auf Autopointer zurückgreifen. Das sind template-Klassen, welche Verwaltung von Zeigern übernehmen und automatisch den reservierten Speicher freigeben, wenn der Ausführungsbereich (Scope) verlassen wird. C++ bietet dafür mehrere Klassen an. Zum Beispiel std::auto_ptr.

std::auto_ptr

Um std::auto_ptr benutzen können muss < memory > eingebunden sein. Die Verwendung von auto_ptr ist zunächst sehr einfach.

// Deklaration eines Zeigers
#include <memory>

std::auto_ptr< Pointer_Datentyp > PointerName ( new Datentyp/Konstruktor );

Auf das zweite Beispiel übertragen:

#include <memory>

std::auto_ptr< Calculator > calc ( new Calculator );

Ab hier übernimmt die Klasse auto_ptr die Verwaltung des Zeigers und gibt, wenn es nötig ist den reservierten Speicher frei. Somit ist ist die delete Anweisung nicht nur überflüssig, sondern führt bei einigen Compilern sogar zu Fehlern.

Führt man die Testanwendung noch ein mal aus, so sieht man dass der Speicher automatisch freigegeben wird ( dabei wird Destruktor aufgerufen).

keine Fehlermeldung

Das auto_ptr – Template besitzt drei Methoden:

  1. get – gibt den Zeigen auf das verwaltete Objekt zurück
  2. release– setzt den auto_ptr auf Null, gibt aber den Speicher nicht frei. Der Programmierer hat wieder die volle Kontrolle über das Objekt
  3. reset – der Autopointer wird mit einer neuer Adresse belegt. Der Speicher, auf den der alte Pointer zeigte, wird freigegeben

Diese Methoden können ganz normal, wie auch bei jeder anderen Klasse, benutzt werden.

Soweit so gut. Nur gibt es einige Besonderheiten die bei der Benutzung von std::auto_ptr zu beachten sind.
Kopiert man einen Autopointer, so wird der Quellenzeiger auf Null gesetzt, so dass nach dem Kopieren nur der Zielzeiger die Adresse des reservierten Speicherbereiches beinhaltet.

// auto_ptr - Kopierverhalten

std::auto_ptr< int > ptr_q( new int( 10 ) );
std::auto_ptr< int > ptr_z;

// Kopieren -> ptr_z erhält die Speicheradresse -> ptr_q = 0
ptr_z = ptr_q;

Man sagt auch, der Besitzer (die Speicheradresse) wird weiter gereicht oder transferiert. Dieses Verhalten stellt sicher, dass nie zwei Pointer auf den gleichen Speicherbereich zeigen, führt allerdings aber auch zu einigen Problemen. Will man Autopointer in einem STL-Container z.B. std::list verwalten, so wird es nicht funktionieren, weil bei einer internen Zuweisung der Zeiger auf Null gesetzt wird. Darüber hinaus gibt es weitere Punkte zu beachten.

Alle Punkte die bei der Verwendung von std::auto_ptr zu beachten sind:

  • auto_ptr nicht als Elemente von STL-Containern verwalten
  • beim Kopieren eines auto_ptr erfolgt ein Transfer der Speicheradresse
  • ein auto_ptr darf niemals auf ein Objekt zeigen, welches nicht mit new erzeugt wurde
  • niemals dürfen mehrere auto_ptr auf das selbe Objekt zeigen
  • auto_ptr dürfen nicht für Arrays verwendet werden

std::tr1::shared_ptr

Wie bereits erwähnt gibt es mehrere Implementierungen von Autopointern. Eine weitere ist shared_ptr und ist Bestandteil von boost–Bibliotheken. Diese Implementierung wird auch in der nächsten STL Version (C++ 0x) dabei sein. Eine vorläufige Version von STL kann bereits durch das Feature Pack für Visual C++ nachinstallieren werden. Wobei shared_ptr dort unter namespace std::tr1 zu finden ist.

Die Benutzung von shared_ptr ist die gleiche wie bei auto_ptr. Nur im Gegensatz zu auto_ptr können mehrere shared_ptr auf den gleichen Speicherbereich zeigen. Dazu wird intern die Anzahl der Referenzen mitgezählt. Die Klasse kapselt einen beliebigen Zeiger und gibt den Speicher dann frei, wenn der Ausführungsbereich verlassen wird und der Speicherbereich nicht mehr Referenziert wird.
Dadurch wird auch folgende Code ohne Probleme ausgeführt.

// shared_ptr
#include <iostream>
#include <memory>

using namespace std;
using namespace std::tr1;

int main(void)
{
    shared_ptr< int > ptr1( new int( 5 ) );

    {
        shared_ptr< int > ptr2( ptr1 );
        cout << " zeiger ptr2: " << *ptr2 << endl;
        *ptr2 = 10;
    }

    cout << " zeiger ptr1: " << *ptr1 << endl;

    cin.get();

    return 0;
}

Verwendet man in diesem Beispiel auto_ptr anstatt shared_ptr, so kommt es zu einem Laufzeitfehler.

Somit hat ein C++ Programmierer mit shared_ptr ein hilfreiches und standardisiertes Werkzeug um seine Programme vor Memoryleaks zu bewahren.

Schreibe einen Kommentar