Destruktoren und Objekte als Verwalter einer Ressource

C++ gehört zu den Programmiersprachen, in der dynamisch belegter Speicher auch explizit wieder freigegeben werden muss. Eine sogenannte garbage collection, die dies automatisiert durchführt, steht normalerweise nicht zur Verfügung. Entsprechend gibt es passend zum Operator new auch den Operator delete in C++.

Dynamische Speicherflächen sind eine von vielen denkbaren Ressourcen. Wenn Ressourcen zu verwalten sind, ist es naheliegend, dies mit einem Objekt zu verknüpfen, d.h.

Ersteres erfolgt (wie bereits demonstriert) im Konstruktor, für letzteres gibt es Destruktoren.

Für diese Vorgehensweise gibt es auch einen Namen: resource acquisition is initialization (RAII). Sie wurde für C++ entwickelt, inzwischen aber auch von anderen Programmiersprachen übernommen. Ein besonderer Vorteil von RAII liegt auch in der exception safeness, einem Konzept, dem wir später noch begegnen werden.

So könnte ein Destruktor für den Datentyp Matrix aussehen:

struct Matrix {
   const std::size_t m; /* number of rows */
   const std::size_t n; /* number of columns */
   const std::size_t incRow;
   const std::size_t incCol;
   double* data;

   Matrix(std::size_t m, std::size_t n, StorageOrder order) :
         m(m), n(n),
         incRow(order == StorageOrder::ColMajor? 1: n),
         incCol(order == StorageOrder::RowMajor? 1: m),
         data(new double[m*n]) {
   }
   ~Matrix() {
      delete[] data;
   }

   /* ... */
};

Der Destruktor wird ebenfalls nach dem Datentyp benannt (hier Matrix), jedoch im Gegensatz zum Konstruktor mit einer Tilde (~) davor. Da die Speicherfläche mit einer Dimensionierung in eckigen Klammern belegt wurde, ist für die Freigabe die Angabe von [] beim delete-Operator notwendig.

Der Zeitpunkt des Aufrufs des Destruktors ergibt sich daraus, wie das Objekt angelegt wurde:

D.h. wenn ein Destruktor angegeben wurde, ist sichergestellt, dass dieser immer zu einem wohldefinierten Zeitpunkt aufgerufen wird.

Damit das alles gut geht, ist auch darauf zu achten, dass ein Zeiger oder eine Referenz auf eine Ressource nicht versehentlich vervielfältigt wird. So sehr wir auch darauf achten möchten, dass angelegte Ressourcen wieder freigegeben werden, ist es auch wichtig, dass kein Versuch unternommen wird, Ressourcen nach der Freigabe weiterzuverwenden oder mehrfach freizugeben.

Aufgabe

Ergänzen Sie in der Vorlage den Destruktor mit einer Testausgabe und ergänzen Sie die Vorlage um alle vier oben genannten Szenarien, d.h. erzeugen Sie ein globales, lokales, dynamisch erzeugtes und temporäres Objekt.

Vorlage

#include <cstddef> /* needed for std::size_t */
#include <cstdio> /* needed for printf */
#include <cassert> /* needed for assert */

enum class StorageOrder {ColMajor, RowMajor};

struct Matrix {
   const std::size_t m; /* number of rows */
   const std::size_t n; /* number of columns */
   const std::size_t incRow;
   const std::size_t incCol;
   double* data;

   Matrix(std::size_t m, std::size_t n, StorageOrder order) :
         m(m), n(n),
         incRow(order == StorageOrder::ColMajor? 1: n),
         incCol(order == StorageOrder::RowMajor? 1: m),
         data(new double[m*n]) {
   }

   ~Matrix() {
      delete[] data;
   }

   const double& operator()(std::size_t i, std::size_t j) const {
      assert(i < m && j < n);
      return data[i*incRow + j*incCol];
   }

   double& operator()(std::size_t i, std::size_t j) {
      assert(i < m && j < n);
      return data[i*incRow + j*incCol];
   }

   void init() {
      for (std::size_t i = 0; i < m; ++i) {
         for (std::size_t j = 0; j < n; ++j) {
            data[i*incRow + j*incCol] = j * n + i + 1;
         }
      }
   }

   void print() {
      for (std::size_t i = 0; i < m; ++i) {
         std::printf("  ");
         for (std::size_t j = 0; j < n; ++j) {
            std::printf(" %4.1lf", data[i*incRow + j*incCol]);
         }
         std::printf("\n");
      }
   }
};

int main() {
   Matrix A(7, 8, StorageOrder::ColMajor);
   A.init();
   std::printf("A =\n"); A.print();
}