abstract
In C++ ist der Umgang mit dynamischem Speicher nicht unproblematisch.
Nicht freigegebener Speicher (Memory leaks) und Zugriffsverletzung können jederzeit einen Terminprobleme erzeugen und Probleme beim Kunden verursachen.
Die Verwendung von Smart-Pointern kann diese Problematik entschärfen, in dem sie die Verwendung von dynamisch angefordertem Speicher vereinfacht.
Smart-Pointer sind C++-Klassen,
Mit Hilfe von Klassen, die sich wie Zeiger verhalten, kann der Zugriff auf Speicher geprüft werden. Da sich das Programm selbst um die Freigabe des dynamischen Speichers kümmert, kann die Freigabe viel zuverlässiger durchgeführt werden.
C++-Klassen können wie Zeiger verwendet werden, wenn die Operatoren * und -> überschrieben worden sind.
Das Überladen der Operatoren * und -> sind keine außergewöhnliche C++-Tricks.
Diese Technik wird z.B. auch bei den Iteratoren der STL angewandt.
Damit können Algorithmen so formuliert werden, dass sie mit verschieden Container-Klassen und Feldern im C-Stils verwendet werden können.
So können die Objekte einer Smart-Pointer Klasse, überall dort eingesetzt werden, wo bisher Zeiger verwendet wurden.
Mit dem Begriff smart ist bei den Smart-Pointer, die dahinter liegende Speicherverwaltungsroutine gemeint.
Die Smart-Pointer verwenden das Resource Acqusiation is initialisation (RAII)-Prinzip für die Speicherverwaltung. Im Construktor einer Klasse wird eine Ressource angefordert und im Destructor wird sie wieder freigegeben.
Bei den Smart-Pointer liegt der Schwerpunkt im 2. Teil von RAII: Der Destructor sorgt für die Freigabe des Speichers!
Die meisten Smart-Pointer konzentrieren sich auf das Freigeben. Sie bekommen den (hoffendlich) frisch angeforderten Speicher im Construktor übergeben. Nur so kann garantiert werden, dass der Speicher immer freigegeben wird.
Um den Speicher korrekt verwaltet zu kann, müssen alle Smart-Pointer klar definierte Regeln für das Kopieren besitzen. (s.u.)
Smart-Pointer können den Umgang mit dynamischem Speicher vereinfachen und den Zugriff darauf absichern.
Computer erledigen stupide Aufgaben zuverlässiger als Menschen. Eine der stupiden und gleichzeitig schwierigsten Aufgaben beim Programmieren ist die korrekte Verwaltung von externen Ressource wie z.B. dynamischer Speicher. Die Regel dazu sind im Prinzip ganz einfach: Jeder Speicher der angefordert wurde, muss auch wieder freigegeben werden. Was hier so einfach anhört ist im Detail gar nicht so einfach. Der Programmverlauf muss nicht immer so ablaufen wie erwartet. Werden Smart-Pointer verwendet trägt der Compiler die Freigabe einfach überall ein. Dadurch, dass der Programmier den Speicher nicht mehr selbst freigeben muss, kann er es auch nicht mehr vergessen.
Ein weitere stupide und fehlerträchtige Programmieraufgabe sind die korrekten AddRef und Release- Aufrufe für COM-Pointer. Hier müssen passende Anzahl von AddRef- und Release-Aufrufen programmiert werden - oder man verwendet einfach Smart-Pointer. Es existieren Smart-Pointern Klassen, die die AddRef und Release-Aufrufe korrekt kapseln.
Der Speicher wird auch im Fehlerfall freigegeben. Auch wenn eine Funktion durch eine Exception oder einen anderen Notausgang (return; im Fehlerfall) vorzeitig verlassen wird, wird der Speicher zuverlässig freigegeben, da der Compiler in diesem Fall alle Destructoren (in diesem Fall der Smart-Pointer) aufruft.
Manchmal kann der richtige Zeitpunkt für die Freigabe von Objekten (welche dynamisch angelegt wurden) nicht eindeutig bestimmt werden. Wenn z.B. der Speicher an der einen Stelle im Programm erzeugt wird und an einer anderen Stelle freigegeben werden soll, ist dies oft mit einem nicht unerheblichen Verwaltungsaufwand verbunden. Oder wenn mehrere Programmteile das gleiche Objekt benötigen kann es ebenfalls schwierig zu bestimmen sein wann das Objekt wieder freigegeben werden kann. Hier darf das Objekt erst dann freigegeben werden, wenn es von niemandem mehr benötigt wird.
Bei der Verwendung von Speicher muss der Speicher auf Gültigkeit geprüft werden.
Ein Pointer in C++ sollte immer in einem der zwei folgenden Zustände befinden:
Nur durch das Befolgen dieser Regeln (und den zugehörenden Abfragen auf NULL) können Zugriffsverletzungen in C++ vermieden werden.
Einfache Zeiger werden in C++ jedoch nicht zwingend initialisiert. Wird im Programmcode die Initialisierung vergessen, steht der Zeiger auf einem zufälligen Wert. Damit kann nicht auf die Gültigkeit des Zeigers geprüft werden. Im Gegensatz dazu sind Smart-Pointer immer initialisiert.
null_ptr, welches statt dem Macro NULL verwendet werden kann.
Das neue Schlüsselwort wird bereits in den neuesten Compilern des Visual Studios (VS2010, VC10) und der GNU Compiler Collection (GCC 4.6) unterstützt.
Oder anders gefragt: Muss sich jetzt jeder seine eigene Smart-Pointer programmieren?
Auch wenn Smart-Pointer eigentlich ganz einfach programmiert werden könnten, liegen die Feinheiten im Detail. Daher sollte man einen fertigen Smart-Pointer verwenden.
Viele moderne (C++) Klassen Bibliotheken bringen Smart-Pointer mit:
Oder man verwendet einen aktuelle C++-Compiler:
Im zukünftigen C++-Standard (C++0x) sind verschiedene Smart-Pointer Teil der Standard-Bibliothek. Da der Standardisierungsprozess sehr langwierig ist, wurde ein so genannter Technical Report (TR1) veröffentlicht, der neue Bibliotheken für aktuelle Compiler beschreibt. Im TR1 ist eine Untermenge der C++0x Smart-Pointer enthalten.
Die Smart-Pointer können nach ihrem Kopierverhalten unterschieden werden. Dies sind:
Smart-Pointer die keine Kopie des Zeigers zulassen.
In dieser, der einfachsten Variante des Smart-Pointers, wird einfach das Kopieren und die Zuweisung verboten. Ohne Zuweisung und Kopie muss auch nichts verwaltet werden. Damit kann sich der scoped_ptr auf seine Grundaufgabe konzentrieren. Ohne den Verwaltungsaufwand ist ein scoped_ptr so effektiv wie Hand-Programmierter Code.
Der scoped_ptr wird zum Aufbewahren von Speicher verwendet, welcher beim Verlassen des Scopes wieder freigeben werden soll. Z.B.:
boost::scoped_ptr
Die boost-Version des scoped_ptr bringt eine nette Erweiterung mit. Mit der
reset-Funktion kann der Speicher ausgetauscht werden. Der vorhandene Speicher wird
dabei freigegeben. Mit reset(NULL) kann der Pointer zurückgesetzt werden.
Vereinfachter Code:
template<class T>
class scoped_ptr : noncopyable {
public:
explicit scoped_ptr(T * p = 0); // Constructor
~scoped_ptr(); // Destructor - Free the memory
void reset(T * p = 0); // Reset the pointer to other pointer or NULL (Free the memory)
T & operator*() const;
T * operator->() const;
T * get() const; // Zugriff auf rohen (raw) Speicher
private:
// private data
};
Beispiel für die Benutzung:
#include <boost/scoped_ptr.hpp>
class MyClass
{
public:
int f();
};
void main()
{
if(true) // Einen "scope" erzeugen
{
boost::scoped_ptr<MyClass> poA(new MyClass);
poA->f();
(*poA).f();
} // automatic free the memory
// MyClass-Instanz wurde freigegeben.
}
Smart-Pointer mit interner Referenz-Zählung
Bei den Smart-Pointer mit Referenz-Zählung können mehrere Smart-Pointer auf den gleichen Speicherbereich zeigen. Durch eine entsprechende Verwaltung wird sichergestellt, dass der Speicher freigegeben wird, sobald kein Zeiger mehr den Speicherbereich verwendet bzw. nicht freigegeben wird solange noch ein Zeiger auf den Speicher zeigt. ("Der letzte macht das Licht aus")
Die übliche Implementierungstechnik verwendet einen internen Zähler. Wegen dieser Implementierungstechnik werden sie üblicherweise als "Smart-Pointer mit (interner) Referenz-Zählung" bezeichnet. Dabei kann dieser Smart-Pointer-Typ auch ohne Zähler implementiert werden. (z.B. zyklische Kette, siehe Modern C++ Design Kapitel "Reference Linking".)
Im künftigen C++- Standard (C++0x) wird es ein Smart-Pointer mit dem Namen shared_ptr auf der Basis des gleichnamigen Boost-Pointers geben.
Da die shared_ptr-Klasse keine C++0x-Erweiterungen benötigt, ist sie auch im TR1 enthalten. (In diesem Fall im Namespace std::tr1.)
Ebenfalls im künftigen C++- Standard (C++0x) ist die Funktion make_shared.
Mit dieser Funktion kann die interne Verwaltungsstruktur sehr effizient erzeugt werden.
Für diese Funktion wird eine C++0x-Erweiterungen benötigt, daher ist diese Funktion nicht im TR1 enthalten.
Die shared_ptr können vielfältig verwendet werden:
Eigentlich können shared_ptr fast überall dort verwendet werden, wo bisher herkömmliche Zeiger Verwendung. Die einzige Ausnahmen sind Zeiger auf Felder.
Mit einem shared_ptr könnte auch ein scoped_ptr ersetzt werden. Hier kann es es jedoch semantisch hilfreich sein, wenn man mit der Verwendung eines scoped_ptr klar ausdrückt, dass dieser Speicher diesen scope (z.B. Funktion, Klasse) nie verläst.
Wenn in einem STL-Constainer Pointer gespeichert werden sollten nur Smart-Pointer mit Referenz-Zählung verwendet werden! (Empfehlung: boost::shared_ptr) Alles andere kann gut muss aber nicht. Sobald ein Algorithmus aus der STL z.B. std::sort auf diesem Container angewandt wird kann es zu einer Zugriffsverletzung kommen. (Zukünftige Alternative (C++0x): unique_ptr durch die move-Erweiterung.)
Da C++ oftmals wegen der Performance eingesetzt wird, sollte die Performance hier ebenfall betrachtet werden. Die shared_ptr werden über einen interne Verwaltungsstruktur (meistens Referenz-Zähler) verwaltet. Diese Verwaltung kostet Zeit. Wenn der Speicher von verschieden Zeigern unabhängig voneinander verwendet werden soll, ist eine Verwaltung nötig.
Wird die Verwaltung nicht benötig, kann ein scoped_ptr verwendet werden - dieser Smart-Pointer ist genauso performant wie Handgeschriebener C++-Code.
Es bleibt die Frage ob die interne Verwaltung durch einen shared_ptr oder eine Selbstgeschriebene Verwaltung durch der Programmierer verwendet werden soll.
Hier würde ich immer für die shared_ptr entscheiden.
boost::shared_ptr
class A {};
class B : public A {};
int main()
{
using namespace std;
if(true)
{
boost::shared_ptr<A> pA;
if (! pA)
{
cout << "pA is not initialized" << endl;
}
if(true)
{
boost::shared_ptr<B> pB(new B);
pA = pB;
}
// pB does not exist any more, but the memory is still accessible over pA
if (pA)
{
cout << "pA is still valid" << endl;
}
} // leaving the scope. pA was the last pointer, pointing to the the memory, so the memory will be freed.
return 0;
}
Die boost-Variante des shared_ptr bringt ein paar netter Zusätze mit:
boost::weak_ptr: Hilfe beim aufbrechen von zyklischen Referenzen.
Zyklische Referenzen können von shared_ptr nicht automatisch aufgelöst werden. Hier müssen die verwendeten Strukturen angepasst geändert werden. Durch die Verwendung von weak_ptr, einfacher Zeiger oder C++-Referenzen für Rückwärts-Referenzen können diese Kreisbeziehungen aufgelöst werden.
Achtung: Wenn der Besitzer zerstört wird können andere auto_ptr auf ungültigen Speicher zeigen.
std::auto_ptr die oben genannten Klassen (std::shared_ptr & std::unique_ptr) verwendet werden.
Smart-Pointer für Felder heißen z.B.: std::string, std::vector, CString, boost::array usw.
Naja, nicht ganz. Natürlich sind die aufgezählten Typen Klassen für Felder und keine Smart-Pointer. Jedoch sollte man sich überlegen ob man von einem Zeiger auf ein Feld gleich auf eine Feld-Klasse umsteigt, wenn bereits die Mühe macht und die Speicherverwaltung in eine intelligente Klasse kapselt.
Selbstverständlich gibt es spezielle Smart-Pointer für Felder z.B. boost::scoped_array und boost::shared_array. Diese Smart-Pointer geben den verwalteten Speicher mit 'delete[]' statt 'delete' frei. Intern halten diese Smart-Pointer nur den verwalteten Speicher, aber nicht die Größe des verwalteten Speichers.
Es wäre jedoch schön wenn die Smart-Pointer für Felder neben dem Speicher auch noch die Größe des allokierten Speichers und eventuell die Größe des belegen Speichers sich merken könnten.
Wenn man dies möchte, braucht man jedoch keine neuen Smart-Pointer erfinden. Klassen für Felder erfüllen genau diese Anforderungen.
std::vector::size.
std::vector::at. Wenn die Funktion std::vector::at mit einem unglütigen Index aufgerufen wird, wird eine entsprechende Exception ausgelöst.
Die Klasse std::vector eignet 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.
Zeiger auf ein char-Feld, das einen String repräsentieren, ersetzt man am sinnvollsten mit der Klasse std::string oder einer vergleichbaren String-Klasse.
char* fn = "a.txt"; int fh0 = open( fn, _O_RDONLY ); // open erwartet einen Zeiger auf ein Feld von char. //vector vor Zeichen std::vector<char> vecFilename(fn, fn+strlen(fn)); // Initialisieren mit den Anfang und Ende eines Feldes. int fh1 = open( &vecFilename[0], _O_RDONLY ); // Aufruf mit der Adresse des ersten Elementes // Für ein Feld von Zeichen ist die String-Klasse die bessere Alternative: std::string strFilename(fn) ; int fh2 = open( strFilename.c_str(), _O_RDONLY ); // c_str() gibt das Feld im "C"-Stil zurück
* und -> Operators sollte Vorteile bringen.
Einzelwerte integraler Datentypen sollten nicht auf dem Heap abgelegt werden.
Für Felder integraler Datentypen sind Container-Klassen sinnvoll.
Die Speicheranforderung sollte im Constructor des Smart-Pointers erfolgen.
Die bekannten und verbreiteten Smart-Pointer stellen keine Möglichkeiten für das Anlegen / Anfordern des Speichers zu Verfügung (z.B. mittels integriertes new im Contruktor.) Daher muss dem Smart-Pointer der Speicher übergeben werden.
Damit sichergestellt wird, dass der Speicher immer freigegeben wird, sollte der Speicher direkt im Contruktor angelegt werden.
class A
{
public:
A(int i); // Construktor
...
};
...
boost::scoped_ptr<A> pA(new A(9)); // richtig
// so nicht:
A* a = new A(11);
foo();
boost::scoped_ptr<A> pA2(a); // Wenn foo eine exception auslöst, wird A(11) nicht freigegeben.
In (STL-)Container sollten nur Smart-Pointer mit Referenz-Zählung verwenden (z.B. boost::shared_ptr). Alles andere ist gefährlich und nicht zukunftssicher. Sobald ein (STL-)Algorithmus auf diesen Container angewandt wird, wird es zu Problemen kommen.
Mit dem kommenden C++-Standard (C++0x) wird es auch möglich sein std::unique_ptr in STL-Containern zu verwenden.
Mit shared_ptr könnten eigendlich alle vorhandenden Pointer ersetzt werden. Durch die Verwendung von z.B. scoped_ptr kann man dem Pointer ein weiteres Attribut mit geben, das besagt dass dieser Zeiger diese Funktion / Klassen nie verlassen wird.
Damit hat man ein semantisches Mittel, dass die Verwendung des Zeigers beschreibt.
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.
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.
class MyClass {};
MyClass* createMyClass() {
MyClass* p = new MyClass;
return p;
}
...
createMyClass(); // ignoriert des Rückgabewert. Kein Fehler aber Memory Leak.
Eine saubere Lösung für dieses Problem können ebenfalls Smart-Pointer (mit Referenz-Zählung) sein. Der Smart-Pointer als Rückgabewert gibt den Speicher frei, auch wenn die Rückgabe ignoriert wird.
Auf diese Weise können Funktionen ohne Problem direkt in Abfragen eingesetzt werden.
// use a shared pointer as funtion return
shared_ptr<A> findObject(const std::string& name)
{
...
}
...
if(findObject("foo"))
{
// Hugo existiert.
}
Niemand muss sich um den Speicher "foo" kümmern. Dies wird automatisch durch den Smart-Pointer erledigt.
Sobald Felder dynamische angelegt werden sollen, sollte dies mit Hilfe der STL-Container erfolgen.
SmartPointer können nicht die Welt retten, aber die Verwendung von SmartPointer kann die Verwendung von dynamischen Speicher vereinfachen und sicherer manchen.