Verwaltung von Ressourcen durch Objekte
Content |
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 <cstdlib>) 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 <cstdlib> 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
#ifndef INTEGER_SEQUENCE_HPP #define INTEGER_SEQUENCE_HPP #include <cassert> #include <cstdlib> #include <new> class IntegerSequence { public: IntegerSequence() : data(nullptr), size(0), allocated(0) { } void add(int value) { if (size == allocated) { std::size_t newsize = allocated * 2 + 8; int* newdata = static_cast<int*>(std::realloc(data, newsize * sizeof(int))); if (!newdata) { throw std::bad_alloc(); } data = newdata; allocated = newsize; } data[size++] = value; } std::size_t length() const { return size; } int& operator()(std::size_t index) { assert(index < size); return data[index]; } const int& operator()(std::size_t index) const { assert(index < size); return data[index]; } private: int* data; std::size_t size; std::size_t allocated; }; #endif
#include <iostream> #include "IntegerSequence.hpp" int main() { IntegerSequence iseq; int value; while (std::cin >> value) { iseq.add(value); } for (std::size_t i = 0; i < iseq.length(); ++i) { std::cout << " " << iseq(i); } std::cout << std::endl; }
Die Vorlage ist in dieser Form übersetzbar und lauffähig, aber valgrind berichtet bereits, dass ein Speicherleck vorliegt:
theon$ g++ -Wall -o test_is test_is.cpp theon$ echo 1 2 3 | valgrind ./test_is ==22581== Memcheck, a memory error detector ==22581== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==22581== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info ==22581== Command: ./test_is ==22581== --22581-- WARNING: Serious error when reading debug info --22581-- When reading debug info from /lib/amd64/ld.so.1: --22581-- Can't make sense of .data section mapping 1 2 3 ==22581== ==22581== HEAP SUMMARY: ==22581== in use at exit: 10,288 bytes in 3 blocks ==22581== total heap usage: 4 allocs, 1 frees, 82,992 bytes allocated ==22581== ==22581== LEAK SUMMARY: ==22581== definitely lost: 32 bytes in 1 blocks ==22581== indirectly lost: 0 bytes in 0 blocks ==22581== possibly lost: 10,256 bytes in 2 blocks ==22581== still reachable: 0 bytes in 0 blocks ==22581== suppressed: 0 bytes in 0 blocks ==22581== Rerun with --leak-check=full to see details of leaked memory ==22581== ==22581== For lists of detected and suppressed errors, rerun with: -s ==22581== ERROR SUMMARY: 0 errors from 0 contexts (suppressed: 0 from 0) theon$
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.