Zeigerautomatik mit auto_ptr

Martin Kompf

Das Template auto_ptr aus der STL hilft bei der Vermeidung von Speicherlecks.

Das Problem

C++ hat keine eingebaute Automatik zur Freigabe von auf dem Heap allokierten Speicher. Das bedeutet für den Programmierer, dass er alle mittels new konstruierten Objekte explizit durch Aufruf von delete auch wieder freigeben muss. Bei einem linearen Programmablauf stellt dies in der Regel kein Problem dar. In realen Programmen muss man aber jederzeit mit dem Auftreten von Fehlern und Ausnahmen (Exceptions) rechnen. In solchen Situationen kann die Programmausführung dann auf ziemlich verschlungenen Wegen erfolgen; um das ordnungsgemäße Aufräumen des Heaps in sämtlichen Fehlersituationen sicherzustellen, kann ein erheblicher Implementierungs- und Testaufwand erforderlich sein.

Das Problem kann insbesondere dann eskalieren, wenn Programmcode mit »vergessener« oder fehlerhafter Speicherbereinigung zyklisch immer wieder abgearbeitet wird. Dann vergrößert sich bei jedem Durchlauf das Speicherleck. Irgendwann erreicht es die Größe des zur Verfügung stehenden physikalischen Speichers, was in der Regel zur unproduktiven Selbstbeschäftigung des Computers mit Ein- und Auslagern von Speicherseiten auf den paging space der Festplatte führt. Im besten Fall kann man das fehlerhafte Programm dann noch beenden und neu starten - handelt es sich dabei um einem wichtigen Datenbank- oder Webserver, ist der Ärger bei den Anwendern vorprogrammiert.

Ein Beispiel

Schauen wir uns zur Demonstration des Problems zunächst die Implementierung einer Klasse namens Buffer an:

#include <exception>
#include <iostream>

class Buffer {
public:
    Buffer();
    ~Buffer();

    void transcode( int k);
    // ...
};

// implementation for demonstration only
Buffer::Buffer()
{
    std::cerr << "a new Buffer has been constructed\n";
}

Buffer::~Buffer()
{
    std::cerr << "the Buffer was destroyed\n";
}

void Buffer::transcode( int k)
{
    if (k < 0) throw std::exception();
    std::cerr << "Buffer transcoded (" << k << ")\n";
}

Man beachte, dass die Memberfunktion transcode unter bestimmten Bedingungen eine Exception wirft.

Diese Klasse wird jetzt in einem Programm verwendet:

using namespace std;

int main()
{
    try {
        Buffer* b = new Buffer();
        // ...
        b -> transcode( -2);
        // ...
        delete b;
    }
    catch (const exception& e) {
        cerr << "Program exception\n";
        exit(1);
    }
}

Auf den ersten Blick ein korrektes Programm: Der per new allokierte Buffer b wird am Ende des Anweisungsblocks ordnungsgemäß mittels delete wieder freigegeben. Lassen wir das Programm jedoch ablaufen, dann ist die folgende Ausgabe zu sehen:

a new Buffer has been constructed
Program exception

Speicherverschwendung

Der Destruktor vom Buffer b wird nie aufgerufen! Der Grund dafür ist, dass die Funktion transcode eine Exception wirft und somit die Programmausführung direkt im catch Block fortgesetzt wird. Alle im try Block nach transcode stehenden Anweisungen werden nicht mehr ausgeführt, so auch nicht die delete Anweisung. Als Folge wird auch der von b belegte Speicherplatz nicht freigegeben.

Als Verbesserungsvorschläge könnten jetzt kommen: Aber das Programm wird sowieso per exit verlassen, dann räumt das Betriebssystem den Speicher auf. Oder: Man allokiere b ausserhalb des try Blocks und gebe den Speicher erst nach dem catch wieder frei. Im Fall dieses zur Demonstration dienenden Beispiels mögen diese Vorschläge durchaus sinnvoll sein, jedoch lösen sie das Problem nicht grundsätzlich. Denn was ist, wenn der Programmcode viel komplizierter zu durchschauen ist? Oder wenn es sich bei dem Programm um einen hochverfügbaren Datenbankserver handelt, der nicht einfach so beim Auftreten von Exceptions beendet werden darf?

Die universellste Lösung wäre, dass der durch new allokierte Bereich auf dem Heap automatisch freigegeben wird, wenn der entsprechende Gültigkeitsbereich (hier: der try Block) verlassen wird - egal dies regulär oder per Exception passiert.

Ein Standard Template hilft!

Um dieses Verhalten zu erreichen, gibt es in der Standard C++ Library bereits das Template auto_ptr (»automatic pointer«). Dieses ist im Header <memory> definiert und bekommt als Templateargument den Typ des zu verwaltenden Objektes. Ein reales auto_ptr Objekt wird dann mittels eines Konstuktors erzeugt, der als Parameter den Zeiger auf den zu verwaltenden Bereich des Heaps bekommt. Dieser Bereich muss mittels new erzeugt werden (also weder new[] noch malloc() funktionieren hier!). Unser Demonstrationsprogramm kann nun unter Verwendung von auto_ptr umgeschrieben werden:

#include <memory>
#include <exception>
#include <iostream>

using namespace std;

int main()
{
    try {
        auto_ptr<Buffer> b (new Buffer());
        // ...
        b -> transcode( -4);
        // ...
    }
    catch (const exception& e) {
        cerr << "Program exception\n";
        exit(1);
    }
}

Wir sehen, dass das auto_ptr Objekt, nachdem es einmal erzeugt worden ist, genauso wie ein normaler Pointer verwendet werden kann. Lassen wir das Programm nun ablaufen, so sehen wir anhand der Ausgabe, dass tatsächlich der Destruktor von b immer aufgerufen (und natürlich auch auch der von b benutzte Speicher freigegeben) wird, wenn das Programm bei der Ausführung den Gültigkeitsbereich von b verlässt:

a new Buffer has been constructed
the Buffer was destroyed
Program exception

Leider nicht ganz problemlos

So bietet auto_ptr eine einfache und wirksame Hilfestellung bei der Verwaltung von Heapspeicher und damit der Vermeidung von Speicherlecks in C++ Programmen. Auch bei der Verwaltung anderer Ressourcen kann auto_ptr helfen, zum Beispiel wenn sichergestellt werden muss, dass bestimmte Dateien oder Netzwerkverbindungen immer ordnungsgemäß geschlossen werden. Allerdings muss der Entwickler beachten, dass in einigen Situationen die Verwendung von auto_ptr nicht erlaubt ist und zu Programmfehlern führen kann: