Du bist nicht angemeldet.

Stilllegung des Forums
Das Forum wurde am 05.06.2023 nach über 20 Jahren stillgelegt (weitere Informationen und ein kleiner Rückblick).
Registrierungen, Anmeldungen und Postings sind nicht mehr möglich. Öffentliche Inhalte sind weiterhin zugänglich.
Das Team von spieleprogrammierer.de bedankt sich bei der Community für die vielen schönen Jahre.
Wenn du eine deutschsprachige Spieleentwickler-Community suchst, schau doch mal im Discord und auf ZFX vorbei!

Werbeanzeige

David_pb

Community-Fossil

  • »David_pb« ist der Autor dieses Themas

Beiträge: 3 886

Beruf: 3D Graphics Programmer

  • Private Nachricht senden

1

16.03.2007, 20:29

Smartpointer Einführung

Da ich schon des öfteren bemerken musste das der Begriff Smartpointer für viele ein Fremdwort und für manche ein "schonmal gehörter" Begriff ist aber die meisten nicht wirklich etwas mit dem Begriff anfangen konnten, habe ich mich entschieden eine kurze Einführung in die gängigsten Smartpointer Klassen (STL, Boost) zu schreiben.

Smartpointer

Einführung
Ein großes Problem vieler C++ Programmierer ist das korrekte verwalten des freien Speichers (freestore) und das vermeiden von Speicherlecks. Viele – und gerade Anfänger – werden diese „Anschuldigung“ verneinen, allerdings ist es sehr kompliziert ein gutes und stabiles System zu entwickeln, das keine Speicherlecks und ähnliche Probleme zulässt.
Es gibt viele Ansätze wie o.g. Problematik umgangen werden kann, zum einen kann ein Memory Manager alle Speicherreservierungen mit protokollieren. Beim beenden des Programms kann also genau festgestellt werden ob noch Speicherteile nicht freigegeben wurden und wo diese reserviert wurden. Eine weitere Möglichkeit ist es die Speicherfreigabe einfach dem Programm zu überlassen, sodass sich der Programmierer einfach gar nicht mehr darum zu kümmern hat. Wäre es nicht schön wenn es eine Möglichkeit gäbe Speicher auf vom Heap zu holen sich aber nicht um die Freigabe kümmern zu müssen? Genau das ist eine Funktion von Smartpointern.
Allerdings bieten Smartpointer weitaus mehr Funktionalität an, eine wichtige Funktion z.B. ist die Referenzzählung. Der Smartpointer zählt mit wie viel „Zeiger“ auf ein Speicherstück zeigen. Dieses Speicherstück wird erst dann freigegeben wenn keine Referenz mehr übrig ist, wenn der Zähler auf Null steht also. Somit wäre auch das Problem von dangling Pointern umgangen.
Allerdings steht man vor einem Problem. Verwendet ein Programm dynamische Speicherreservierung so liegt die volle Verantwortung der Verwaltung in den Händen des Programmierers. Zeiger die auf Speicheradressen im Heap zeigen haben also eine unbegrenzte Lebensdauer, es sei denn der Programmierer gibt den entsprechenden Speicherbereich zurück. Eine andere Art Speicher zu nutzen stellt der Stack dar. Sämtliche Objekte die auf dem Stack abgelegt werden haben eine begrenzte Lebensdauer. D.h. sie werden automatisch zerstört sobald das Programm den entsprechenden Gültigkeitsbereich verlässt. Und genau diese Eigenschaft lässt sich für Smartpointer nutzen. Die Idee dahinter ist recht simpel: Ein Smartpointer ist nichts weiter als ein Objekt das einen Zeiger beinhaltet welcher seinerseits auf ein Speicherstück im Heap zeigt. Außerdem sollte ein Smartpointer die gängige Zeigerarithmetik unterstützen, also z.B. das Dereferenzieren per „*“-Operator und den „->“-Operator. Durch diese Funktionalität können simple Zeiger problemlos durch Spartpointer ersetzt werden, im Idealfall ist nichts als die Deklaration zu ändern das komplette verhalten des Zeigers bleibt das gleiche.
Das ein Objekt einen realen Zeiger ummantelt bietet noch weitere Möglichkeiten. So ist es Möglich den Vorgang der Speicherreservierung und Freigabe zu beeinflussen. Die Eigentümerschaft des Speichers kann flexibel bestimmt werden und das Kopierverhalten ist beeinflussbar, um nur einige von vielen Möglichkeiten zu nennen.
Nun aber erstmal zu praktischen Beispielen. Es gibt viele Implementierungen von Smartpointern eine simple bietet schon die STL (Standard Template Library). Im Header memory findet sich das Klassentemplate auto_ptr.

Das Template auto_ptr
Die Smartpointer Variante welche von der STL angeboten wird beinhaltet schon einige Eigenheiten. Die grundsätzlichen Eigenschaften, wie Dereferenzierung und „->“-Operator, sind natürlich vorhanden. Des Weiteren hat auto_ptr aber ein recht seltsam anmutendes Kopierverhalten, dazu gleich mehr. Zuerst ein simples Beispiel:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <memory>

int main()
{
    std::auto_ptr< int > ptr( new int( 10 ) );

    std::cout << "Wert von ptr: " << *ptr << std::endl;
    *ptr = 100;
    std::cout << "Wert von ptr: " << *ptr << std::endl;

    std::cin.get();
} 


Dieses kleine Beispiel demonstriert die grundlegende Verwendung von auto_ptr. Zunächst wird ein Objekt angelegt, der Templateparameter gibt an von welchem Typ der Zeiger sein soll. Im Konstruktor kann dann ein Zeiger übergeben werden. Wird kein Zeiger übergeben so wird der interne Zeiger im Objekt mit „0“ initialisiert.
Es ist allerdings zu beachten das kein Zeiger übergeben werden darf der ungültig ist oder werden kann. Auto_ptr gibt den Speicher im Destruktor wieder an das System zurück, sollte dies vorher schon passiert sein so ist mit einem undefinierten Programmverhalten zu rechnen. Dazu ein Beispiel:

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <memory>

int main()
{
    int* p = new int( 10 );
    std::auto_ptr< int > ptr( p );

    std::cout << "Wert von ptr: " << *ptr << std::endl;
    *ptr = 100;
    std::cout << "Wert von ptr: " << *ptr << std::endl;

    delete p;

    std::cin.get();
}


Das Problem hierbei ist das der Smartpointer versucht Speicher freizugeben welcher aber bereits freigegeben wurde. Obiger Code lässt sich zwar einwandfrei kompilieren und mutet zunächst korrekt an, allerdings kann es zu bösen Überraschungen führen wenn das auto_ptr Objekt versucht den übergebenen Speicher freizugeben. Man sollte sich also merken die Speicherfreigabe immer dem Smartpointer zu überlassen, dieser macht das korrekt und zuverlässig!
Auto_ptr bietet außerdem drei Funktionen, nämlich get(), release() und reset(). Get() macht das selbe wie der „->“ Operator. Beide geben einen Rohzeiger auf den entsprechenden Speicherabschnitt zurück. Die Funktion Release() setzt den internen Zeiger auf Null gibt den Speicher aber nicht frei, dafür bekommt man einen Zeiger auf die entsprechende Speicherstelle geliefert.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <memory>

int main()
{
    std::auto_ptr< int > ptr( new int( 10 ) );

    std::cout << "Wert von ptr: " << *ptr << std::endl;
    *ptr = 100;
    std::cout << "Wert von ptr: " << *ptr << std::endl;

    int* p = ptr.release(); // setzt quasi ptr=0

    delete p; // Speicher muss wieder selbst freigegeben werden


    std::cin.get();
}

Nach einem Aufruf von release() liegt die Verantwortung also wieder beim Programmierer. Die letzte der drei Funktionen ist die Funktion reset(). Dieser Funktion kann ein Zeiger übergeben werden welcher dann als interner Zeiger des auto_ptr Objekts verwendet wird. Vorher wird allerdings der Speicher, auf den der alte Zeiger zeigt, freigegeben.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
#include <memory>

int main()
{
    std::auto_ptr< int > ptr( new int( 10 ) );

    std::cout << "Wert von ptr: " << *ptr << std::endl;
    *ptr = 100;
    std::cout << "Wert von ptr: " << *ptr << std::endl;

    ptr.reset( new int( 150 ) ); // Alten Speicher freigeben

    std::cout << "Wert von ptr: " << *ptr << std::endl;

    ptr.reset(); // Smartpointer auf NULL setzen


    std::cin.get();
}


Grundsätzlich sollten aber Methoden bei Smartpointern vermieden werden. Zum einen ist es gegen die Zeigersyntax den „.“-Operator zu verwenden. Zum anderen kann es sehr verwirrend sein wenn ein gezeigtes Objekt eine gleichnamige Methode hat.
Zuletzt bleibt noch das Kopierverhalten zu erklären das auto_ptr hat. Kopiert man zwei auto_ptr Objekte so verliert das Quellobjekt die Eigentümerschaft über den gezeigten Speicher. Um das zu verdeutlichen ein weiteres kleines Beispiel:


C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
#include <memory>

int main()
{
    std::auto_ptr< int > ptr1( new int( 10 ) );
    std::auto_ptr< int > ptr2; // Interner Zeiger = 0


    ptr2 = ptr1;
    std::cout << *ptr2 << std::endl;

    std::cin.get();
}

Durch den Kopiervorgang wird ptr2 der Eigentümer über den reservierten Speicher, ptr1 verliert das Eigentum. Somit ist gewährleistet das niemals mehr als ein auto_ptr Objekt auf ein bestimmten Speicherbereich zeigen kann.
Soweit so gut. Die Eigenschaft der automatischen Speicherfreigabe ist allerdings nicht nur ein nettes Feature sondern macht durchaus Sinn. Sollte im Programmverlauf z.B. eine Exception auftreten so ist man oftmals schnell dabei mit Speicherlöchern.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>

int main()
{
    try
    {
        int* p = new int( 500 );
        throw 3; // Oh, fliegende Exceptions

        delete p;
    }
    catch ( ... )
    {
        std::cout << "Ha, Exception gefangen!" << std::endl;
    }

    std::cin.get();
} 

Dieses Beispiel verdeutlicht wie leicht Speicherleaks entstehen können. Der Speicher für den Zeiger „p“ wird hier niemals freigegeben, da das Programm im Catch-Block landet. Die nahe liegende Lösung ist die Verwendung von Smartpointern.
Da das Programm automatisch alle Objekte zerstört die sich im Gültigkeitsbereich des Try-Blocks befinden und auf dem Stack abgelegt wurden wird auch das auto_ptr Objekt zerstört. Bei Aufruf des Destruktors wird der reservierte Speicher freigegeben und das Programm kann ohne Speicherleck weiterarbeiten.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
#include <memory>

int main()
{
    try
    {
        std::auto_ptr< int >p( new int( 500 ) );
        throw 3; // Oh, fliegende Exceptions

    }
    catch ( ... )
    {
        std::cout << "Ha, Exception gefangen!" << std::endl;
    }

    std::cin.get();
}


boost::scoped_ptr
Nachdem das Klassentemplate auto_ptr der STL vorgestellt wurde sollte die grundlegende Funktionalität von Smartpointern klar sein. Als nächstes werden andere Typen von Smartpointern vorgestellt die sich in ihrem verhalten zwar ähneln aber dennoch andere Verhaltensmuster aufweisen. Leider bietet die STL nicht mehr als dieses eine Klassentemplate, daher wird in den nächsten Abschnitten auf die Smartpointer Klassentemplates der Bibliothekssammlung boost zurückgegriffen. Boost bietet insgesamt sechs verschiedene Klassemtemplates die alle Smartpointer repräsentieren sich aber unterschiedlich verhalten.
Zunächst einmal soll, entsprechend der Abschnittsüberschrift das Template boost::scoped_ptr vorgestellt werden. Dieses Template entspricht weitgehend dem Template std::auto_ptr, hat aber dennoch einige Abweichungen. Das erstellen eines Smartpointers vom Typ boost::scoped_ptr ist äquivalent zu std::auto_ptr. Es wird wiederum ein Konstruktor geboten dem ein Zeiger übergeben werden kann. Außerdem werden die Operatoren „*“ (Dereferenzierung) und „->“ angeboten, die eine zeigerähnliche Syntax erlauben. Genau wie bei std::auto_ptr gibt es einige Methoden (get() und reset()) welche denen aus std::auto_ptr vom Verhalten her entsprechen.
Neu hingegen ist der Operator „bool“ und der Operator „!“ um den Zeiger zu testen.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
#include <boost/scoped_ptr.hpp>

int main()
{
    boost::scoped_ptr< int > ptr( new int( 100 ) );

    if ( ptr )
    {
        std::cout << "Wert von ptr: " << *ptr << std::endl;
    }

    ptr.reset();

    if ( !ptr )
    {
        std::cout << "Zeiger ist leider NULL" << std::endl;
    }
    else
        std::cout << "Wert von ptr: " << *ptr << std::endl;

    std::cin.get();
}

Durch die Verwendung beider Operatoren ist das testen des Zeigers genau wie bei echten Zeigern möglich. Ein weiterer Unterschied ist das Kopierverhalten. Wo std::auto_ptr die Eigentümerschaft übertragen hatte verbietet boost::scoped_ptr einfach das Kopieren eines boost::scoped_ptr Objekts in ein anderes.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
#include <boost/scoped_ptr.hpp>

int main()
{
    boost::scoped_ptr< int > ptr1( new int( 100 ) );
    boost::scoped_ptr< int > ptr2;

    ptr2 = ptr1; // Das ist verboten


    std::cin.get();
}

Das Ergebnis ist aber eigentlich das gleiche. Es kann immer nur ein scoped_ptr Objekt auf einen bestimmten Speicherbereich zeigen. Zum Schluss sei aber noch angemerkt das es möglich ist ein Smartpointer vom Typ std::auto_ptr in einen Smartpointer vom Typ std::scoped_ptr zu kopieren.

boost::scoped_array
In den letzten Abschnitten wurden bereits zwei verschiedene Typen von Smartpointern vorgestellt. Allerdings wurden beide für einzelne Objekte auf dem Heap verwendet, niemals für Arrays. Das hatte auch seinen Sinn, da beide Smartpointer Typen zur Freigabe den delete-Operator verwenden, welcher zwangsläufig bei Zeigern auf Arrays (auf dem Heap) fehlschlagen würde. Daher bietet boost ein weiteres Template, nämlich boost::scoped_array.
Dieses Template entspricht vom Verhalten dem von boost::scoped_ptr mit zwei kleinen Unterschieden. Zum einen ist der Operator „[]“ implementiert, um per Index auf einzelne Arrayeinträge zuzugreifen. Zum anderen wird der Operator delete [] verwendet sodass die Speicherfreigabe korrekt für Arrays funktioniert.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
#include <algorithm>
#include <iterator>
#include <boost/scoped_array.hpp>

int main()
{
    boost::scoped_array< int > ptr( new int[ 10 ] );

    for ( int i = 0; i < 10; ++i )
    {
        ptr[ i ] = i; 
    }

    int* p = ptr.get();

    std::copy( p, p+10, std::ostream_iterator< int >( std::cout, " " ) );
    std::cin.get();
}


boost::shared_ptr und boost::shared_array
Im Folgenden werden gleich zwei Klassen vorgestellt, die Unterschiede beider sind die selben wie bei boost::scoped_ptr und boost::scoped_array, daher können beide Typen ruhig zusammengefasst werden.
Bei allen bisherigen Smartpointer-Typen war es nicht möglich das Speicher zwischen mehreren Smartpointern aufgeteilt wurde. Dies macht Sinn, da dadurch dangling Pointer vermieden werden können, also Zeiger die auf bereits freigegebene Speicherbereiche zeigen. Eine andere Lösung bietet aber die so genannte Referenzzählung. Hierbei wird einfach die Anzahl der Referenzen auf ein Stück Speicher mitgezählt. Das betreffende Stück Speicher wird erst dann freigegeben wenn keine Referenz mehr darauf zeigt. So wird der Speicher korrekt zurückgegeben und es entstehen keine vagabundierenden Zeiger.

C-/C++-Quelltext

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <boost/shared_ptr.hpp>

int main()
{
    boost::shared_ptr< int > ptr1( new int( 100 ) );
    
    std::cout << "Refcount: " << ptr1.use_count() << std::endl;
    
    if ( true )
    {
        boost::shared_ptr< int > ptr2 = ptr1; 
        std::cout << "Refcount: " << ptr2.use_count() << std::endl;
        std::cout << "Value of ptr2: " << *ptr2 << std::endl;
    }

    std::cout << "Refcount: " << ptr1.use_count() << std::endl;
    std::cout << "Value of ptr1: " << *ptr1 << std::endl;

    std::cin.get();
}


Das Objekt ptr1 bekommt zu Beginn Speicher übertragen. Durch die Funktion use_count() kann der Referenzzähler abgefragt werden. In einem weiteren Gültigkeitsbereich wird ein weiterer Smartpointer erzeugt und ptr1 in diesen kopiert. Dadurch erhöht sich der Referenzzähler um eins und ptr2 kann auf den selben Speicher zugreifen wie auch ptr1. Nachdem der Gültigkeitsbereich von ptr2 verlassen wird, wird ptr2 gelöscht nicht allerdings der Speicher auf den referenziert wurde da ja ptr1 immer noch aktiv ist und uneingeschränkt verwendet werden kann.
Dies ist zunächst der größte Unterschied zu den anderen Smartpointervarianten. Außerdem verfügt boost::shared_ptr über einige neue Methoden, zum einen die schon vorgestellt Methode use_count() zum anderen aber eine Methode namens unique(), welche nichts anderes macht als „wahr“ zu liefern wenn use_count() gleich 1 ist.
Als Äquivalent für Arrays bietet boost das Klassentemplate boost::shared_array an.

Schlusswort
Ich hoffe nun das ich einen groben Überblick über das Thema Smartpointer verschaffen konnte. Es gibt zahlreiche Dokumente und Artikel die das Thema vertiefen.

In Andrei Alexandrescu’s Modern C++ Design werden Policy Based Smartpointer im Detail behandelt im Verlauf des Kapitels wird ein Smartpointer Template entwickelt das Policy Basiert sein verhalten ändern kann. Die Kombination aus Ownership Policy (Eigentümerschaft), ConversionPolicy (Umwandlung), ChekingPolicy (Fehlerprüfung) und StoragePolicy (Speicherung) ergeben ein Mächtiges Template mit dem mehr als 300 verschiedene Smartpointervarianten erzeugt werden können. Außerdem wird über die Verwendung von Multithreading und Smartpointern diskutiert.

In Scott Meyer’s More Effective C++ findet sich eine Richtlinie über Smartpointer die im Detail Smartpointer analysiert und deren Vor- und Nachteile aufzeigt.

Und zuletzt noch ein Link (s. Anhang) zu einem Dokument das Exception sichere Smartpointerentwicklung behandelt.

Ich hoffe nun die Einführung in das Thema hat das Interesse des Lesers geweckt. Bei Fragen stehe ich unter der E-Mail: reinigdavid@hotmail.com zur Verfügung.

Anhang:

:arrow: Alle Tippfehler dürfen behalten werden! Rückgabe nicht erwünscht! :)

Linkliste:
http://std.dkuug.dk/jtc1/sc22/wg21/docs/papers/1994/N0555.pdf
http://bl4ckd0g.bl.funpic.de/docs/Smartpointer.pdf
@D13_Dreinig

Sheddex

unregistriert

2

17.03.2007, 10:51

Sehr schön :)

3

17.03.2007, 12:07

Ja doch ist brauchbar ;)
Devil Entertainment :: Your education is our inspiration
Der Spieleprogrammierer :: Community Magazin
Merlin - A Legend awakes :: You are a dedicated C++ (DirectX) programmer and you have ability to work in a team? Contact us!
Siedler II.5 RttR :: The old settlers-style is comming back!

Also known as (D)Evil

$nooc

Alter Hase

Beiträge: 873

Wohnort: Österreich / Kärnten

Beruf: Schüler

  • Private Nachricht senden

4

18.03.2007, 11:05

danke.. *pfff* echt danke.. d.h. ich kann mir jetz wieder 1-2 tage zeit nehmen um das durchzukauen :lol:
Am Anfang der Weisheit steht die eigene Erkenntnis, dass man selbst nichts weiß! - Sokrates

Werbeanzeige