======================================= Verwaltung von Ressourcen durch Objekte [TOC] ======================================= Da das Auf- und Abbauen von Objekten wohldefiniert erfolgt, liegt es nahe, die Verwaltung von Ressourcen mit dem Lebenszyklus eines Objekts zu verknüpfen, in dem Sinne, dass * bei Aufbau eines Objekts (Konstruktion) die Ressource akquiriert wird und * beim Abbau eines Objekts die Freigabe der Ressource erfolgt. Dieses Konzept ist unter dem Akronym RAII (_resource acquisition is initialization_) bekannt geworden. Ressourcen können dabei recht vielfältig sein: * Dynamisch belegter Speicher auf dem Heap * Datei- und Netzwerkverbindungen * Datenbanktransaktionen * Locks Diese Vorgehensweise bietet zwei wichtige Vorteile: * Die Freigabe wird nicht versehentlich vergessen, da sie mit dem Abbau des Objekts automatisiert erfolgt. * Auch im Falle einer Ausnahmenbehandlung (_exception handling_), bei auf der Suche nach einem Ausnahmenbehandler (_exception handler_) Teile des Stacks abgeräumt werden, werden die betroffenen Objekte sauber abgeräumt. Somit lässt sich auf diesem Wege ein wohldefiniertes Verhalten bei Ausnahmenbehandlungen sicherstellen (_exception-safeness_). Wenn die Objekte einer Klasse Ressourcen verwalten, müssen wir jedoch darauf achten, dass diese bzw. die Verweise darauf nicht unbeabsichtigt durch Kopieraktionen geklont werden. Würde dies geschehen, käme es zu mehrfachen Freigaben -- bei einer Freigabe belegten Speichers auf dem Heap kann dies katastrophale Konsequenzen haben. The Rule of Three ================= Bei Klassen von Objekten, die Ressourcen verwalten, sind folgende spezielle Methoden immer ohne Ausnahme explizit zu definieren bzw. mit Hilfe von `delete` zu deaktivieren, da die vom Übersetzer voreingestellten Fassungen zu unerwünschten Klonen führen: * Kopier-Konstruktor * Zuweisungs-Operator * Dekonstruktor Aufgabe ======= Gegeben sei folgende Klasse, die eine Sequenz ganzer Zahlen verwaltet. Die Sequenz ist zu Beginn leer und mit Hilfe der Methode _add_ können sukzessive Zahlen hinzugefügt werden. Damit der indizierte elementweise Zugriff mit dem Funktionsoperator immer effizient (d.h. mit konstantem Aufwand) möglich ist, werden alle Elemente der Sequenz immer in einem zusammenhängenden Speicherbereich auf dem Heap verwaltet. Einige Anmerkungen zum weiteren Verständnis dieser Klasse: * Bei der Datenstruktur steht _size_ für die Länge der Sequenz und _allocated_ für den Umfang der Speicherfläche, auf die _data_ verweist. Es gilt immer die Invariante `size <= allocated`. * Für die Zahl der Elemente, einen Index und die Größe der Speicherfläche ist der Datentyp `std::size_t` (aus ``) geeignet, da dieser die passende Größe für die Zielarchitektur aufweist. * Die Funktion `std::realloc` ist aus der C-Standardbibliothek, die auch Teil der C++-Standardbibliothek ist. Sie ist ebenfalls über `` zugänglich. Die Funktion erhält einen Zeiger auf eine Speicherfläche (hier _data_, darf ein `nullptr` sein) und eine neue gewünschte Größe, die größer oder kleiner als die alte sein darf. Dann wird die belegte Speicherfläche vergrößert und, falls notwendig, auch umkopiert. Das Umkopieren ist nur für triviale Objekte zulässig, bei `int` stellt das kein Problem dar. Zurückgeliefert wird ein Zeiger auf die neue Speicherfläche. Wenn der Speicher ausgeht, wird ein `nullptr` zurückgeliefert. (Statt mit `std::realloc` wäre es natürlich auch denkbar, mit `new` und `delete` zu operieren. Aber das würde bedeuten, dass wir ausnahmslos bei einer Vergrößerung umkopieren müssen. Bei `std::realloc` hat die Speicherverwaltung die Möglichkeit, die belegte Speicherfläche direkt an Ort und Stelle zu vergrößern, wenn der Bereich dahinter noch frei ist. In diesem Fall bleibt der Aufwand durch das Umkopieren uns erspart.) * Wenn die C-Funktionen `std::realloc` oder `std::malloc` benutzt werden, ist die benötigte Speicherfläche in Bytes anzugeben, d.h. die Zahl der Elemente muss hier mit der Größe eines Elements (also mit `sizeof(int)`) multipliziert werden. * Die Funktion `std::realloc` liefert einen Zeiger vom Typ `void*`, der mit Hilfe des `static_cast`-Operators in einen Zeiger des Typs `int*` konvertiert wird. * In C++ gehen wir im Normalfall davon aus, dass ausgehender Speicherplatz von Ausnahmen behandelt werden. Deswegen wird hier mit `throw std::bad_alloc` eine Ausnahmenbehandlung initiiert, falls `std::realloc` einen Nullzeiger liefert. Passen Sie die weiter unten angegebene Vorlage entsprechend der _Rule of Three_ an und ergänzen Sie das Testprogramm so, dass sowohl der Kopierkonstruktor als auch der Zuweisungsoperator zum Zuge kommt. Überprüfen Sie jeweils die Korrektheit mit dem Werkzeug _valgrind_. Hinweise: Mit `std::malloc` können Sie Speicher auf dem Heap belegen. Anzugeben ist die Zahl der benötigten Bytes (analog zum zweiten Parameter von `std::realloc`). Speicher, der mit `std::malloc` oder `std::realloc` belegt worden ist, sollte mit `std::free` freigegeben werden. Vorlage ======= :import: session03/step01/IntegerSequence.hpp :import: session03/step01/test_is.cpp Die Vorlage ist in dieser Form übersetzbar und lauffähig, aber _valgrind_ berichtet bereits, dass ein Speicherleck vorliegt: ---- SHELL (path=session03/step01,hostname=theon) -------- g++ -Wall -o test_is test_is.cpp echo 1 2 3 | valgrind ./test_is ---------------------------------------------------------- Beachten Sie hier die folgende Zeile: `definitely lost: 32 bytes in 1 blocks`. Auf unserer Architektur ist `sizeof(int) == 4` und zu Beginn werden 8 `int` angelegt, so dass es sich um 32 Bytes handelt. :navigate: up -> doc:index next -> doc:session03/page02