Advanced Services | Autopointer |
Für die Variablen und Konstanten stehen einem Programm drei Bereiche zur Verfügung, Data, Stack und Heap. Im Data-Speicher werden die vereinbarten Konstanten abgelegt, im Stack die definierten Variablen. Diese beiden Bereiche verwaltet die Anwendung selbst. Belegt man Speicher auf dem Stack, so ist man auf der sicheren Seite, denn dieser wird automatisch freigegeben, wenn die Variable nicht mehr gebraucht wird.
Beispiel für Daten, die in der Data-Section abgelegt werden.
void data() { const int cint = 17; // Konstante wird angelegt in der Data-Section const char cchar = 'a'; // Konstante wird angelegt in der Data-Section const char *st = "foo"; // deprecated conversion from string constant to 'char*' // Stringkonstante wird in der Data-Section angelegt (meistens...) // Zeiger selbst auf dem Stack. st = "bar"; } // Stackspeicher wird nach dem Ende der Funktion freigegeben
Beispiel für Variablen, die im Stack abgelegt werden.
void stack() { int a=0; // Stack bool boo = true; // Stack int *ipoi; // Pointervariable wird auf dem Stack angelegt (nicht initialisiert) int iArr[17]; // iArr ist ein const-Pointer auf den Anfang eines Speicherblocks der Größe 17*sizeof(int) // Pointer und Speicherblock auf dem Stack char chArr[] = { 'a', 'b', 'b' }; // chArr ist ein const-Pointer auf den Anfang eines Speicherblocks der Größe 3*sizeof(char) // Pointer und Speicherblock auf dem Stack double dou = 12.34; // Stack double *dpoi = &dou; // Stack }
Beispiel für Variablen, die auf dem Heap abgelegt werden.
void stackandheap() { char *chpoi = new char[17]; // (Initialisierter) Pointer wird auf dem Stack angelegt // chpoi ist ein Pointer auf den Anfang eines Speicherblocks der Größe 17*sizeof(char) auf dem Heap int *ipoi = new int[17]; // Initialisierter Pointer, Pointer selbst wird auf dem Stack angelegt // ipoi ist ein Pointer auf den Anfang eines Speicherblocks der Größe 17*sizeof(int) auf dem Heap delete [] ipoi; // Speicher wird feigegeben delete [] chpoi; // Speicher wird feigegeben }
Dynamischer Speicher dagegen, der zur Laufzeit angefordert wird, wird auf dem Heap angelegt und muß vom Entwickler selbst freigegeben werden. Auch erfahrene Entwicklern können das in komplexen Situationen übersehen. Im Prinzip passiert dann folgendes.
void memoryleak() { char *chpoi = new char[17]; // (Initialisierter) Pointer wird auf dem Stack angelegt // chpoi ist ein Pointer auf den Anfang eines Speicherblocks der Größe 17*sizeof(char) auf dem Heap int *ipoi = new int[17]; // Initialisierter Pointer, Pointer selbst wird auf dem Stack angelegt // ipoi ist ein Pointer auf den Anfang eines Speicherblocks der Größe 17*sizeof(int) auf dem Heap // delete [] ipoi; // Speicher wird feigegeben } // delete [] chpoi; wurde vergessen, keine Möglichkeit das noch nachzuholen, da chpoi nicht mehr existiert.
Im obigen Beispiel ist leicht zu sehen, daß der Speicher, auf den chpoi zeigt, nicht freigegeben wurde, in Projekten in denen mehrere Entwickler arbeiten, ist das nicht so leicht zu sehen.
Das bisher gesagt gilt natürlich genauso für komplexe Variablen und Konstanten, also für Objekte.
Hier schauen C++-Entwickler etwas neidisch auf die Java- und C#-Kollegen, denen eine Laufzeitumgebung zur Verfügung steht, die den Speicher aufräumt. Aber in der Weiterentwicklung von C++ gibt es mittlerweile ähnliche Konzepte, besonders im neuen C++11 Standard. Ein erster Versuch in diese Richtung waren Autopointer. Sie sind zwar mit der Version C++11 deprecated, sollen aber trotzdem kurz besprochen werden. Aus den Nachteilen, die Autopointer haben, wird dann ersichtlich warum es seit C++11 neuere und bessere Konzepte gibt.
Ein Autopointer ist ein Objekt einer Klassenschablone, das man ähnlich wie einen normalen Pointer verwendet und das den angeforderten Heapspeicher selbst freigibt und auf diese Weise Memoryleaks verhindert. Für Autopointer braucht man das Include memory
Die folgenden Beispiel verwenden die einfache Klasse Test, die im Destruktor eine Konsolmelduing ausgibt. Auf diese Weise kann man die Speicherfreigabe durch den Autopointer einfach verfolgen.
class Test { public: int i; public: Test(int j = 0) : i(j) {} ~Test() { cout << "destructor test" << endl; } };
Das folgende Beispiel zeigt dsie prinzipielle Verwendung von Autopointern. In den spitzen Klammern wird der aktuelle Datentyp angegeben. ap(new Test(5)) ist der Konstruktoraufruf für die Instanz ap, der ein Zeiger auf ein Objekt vom Typ Test übergeben wird.
void autopointer1() { std::auto_ptr<Test> ap(new Test(5)); cout << p->i << endl; }
Der Ablauf:
Autopointer verfügt über eine Memberfunktion get(), die den originalen Pointer liefert: Die Operatoren ++ und << sind nicht überladen, * dagegen schon.
void autopointer2() { std::auto_ptr<int> ap (new int); //std::cout << ap << '\n'; // << nicht überladen cout << "Speicheradresse: " << ap.get() << '\n'; // Speicheradresse cout << "Wert: " << *ap << '\n'; // * überladen *ap.get()=17; cout << "Wert: " << *ap << '\n'; // *ap = 18; cout << "Wert: " << *ap << '\n'; // (*ap)++; cout << "Wert: " << *ap << '\n'; // //ap++; // ++ nicht überladen }
release() setzt den internen Pointer auf NULL, läßt aber das Objekt bestehen. Das Objekt kann dann über den Autopointer weder referenziert werden noch wird der Speicher freigegeben. Für das Aufräumen muß der Entwickler dann wieder selbst sorgen, was im nächsten Beispiel nicht mehr möglich ist.
void autopointer_release() { cout << "autopointer release" << endl; std::auto_ptr<Test> ap(new Test(17)); cout << ap->i << endl; // OK Test *tpoi = ap.get(); // liefert die Adresse ap.release(); tpoi = ap.get(); cout << tpoi << endl; // liefert jetzt NULL //cout << ap->i << endl; // Runtimeerror }
reset() zerstört das Objekt (ruft den Destruktor) und setzt anschließend den internen Pointer auf NULL wenn kein neuer Zeiger übergeben wird, bzw. übernimmt den Zeiger der übergeben wird.
void autopointer_reset1() { cout << "autopointer reset1" << endl; std::auto_ptr<Test> ap(new Test(17)); cout << ap->i << endl; // OK Test *tpoi = ap.get(); // liefert die Adresse ap.reset(); tpoi = ap.get(); cout << tpoi << endl; // jetzt NULL }
reset() wird ein neuer Pointer übergeben.
void autopointer_reset2() { cout << "autopointer reset2" << endl; std::auto_ptr<Test> ap(new Test(17)); cout << ap->i << endl; // OK Test *tpoi = ap.get(); // liefert die Adresse ap.reset(new Test(42)); tpoi = ap.get(); cout << tpoi << endl; // Adresse von new Test(42) }
Nun wird der Destruktor zweimal gerufen.
Die ungewöhnliche Überladung des Zuweisungsoperators hat letztendlich dazu geführt, daß Autopointer ab der Version C++ 11 deprecated gemacht worden sind. Nach einer Zuweisung ruft nämlich der überladenen Operator automatisch release() für den rechtsstehenden Autopointer auf, sodaß dieser nach dem Kopiervorgang auf NULL zeigt.
void transfer_of_ownership() { cout << "transfer_of_ownership\n" ; std::auto_ptr<Test> ap1(new Test(17)); cout << ap1->i << endl; // OK std::auto_ptr<Test> ap2 = ap1; Test *tpoi = ap1.get(); cout << tpoi << endl; // NULL cout << ap2->i << endl; // OK }
Der Grund hierfür ist, daß sonst zwei Autopointer existieren würden, die auf denselben Speicherbereich zeigen und so würde der Speicher zweimal freigegeben, was nicht zulässig ist. Der große Nachteil allerdings ist, daß nach der Übergabe des Autopointers an eine (Member-)Funktion der originale Pointer nicht mehr verwendbar ist, bei einer call-by-value Übergabe ja eine Kopie angelegt wird.
Ein weiterer Nachteil ist, daß Autopointer keine Arrays unterstützen.
Aus diesem Grund gibt es ab C++ 11 neue Pointertemplates, die sog. Smartpointer: shared pointer, unique pointer und weak pointer. Diese werden in den nächsten Abschnitten vorgestellt.