Operatoren zum Speichermanagement

Bjarne Stroustrup, der Erfinder der Programmiersprache C++, schreibt in seinem Buch „Design und Entwicklung von C++“, dass er ursprünglich die Konstruktoren new und die Destruktoren delete nennen wollte. Das ist meiner Ansicht nach auch recht logisch, trotzdem hat er es nicht gemacht. Warum eigentlich nicht?

Des Rätsels Lösung ist eigentlich recht einfach: new und delete sind Operatoren und noch dazu kann man diese Operatoren überladen.

Was machen die new und delete-Operatoren eigentlich ?

Die Konstruktoren konstruieren auf vorhandenem Speicher eine Klasseninstanz. Der Destruktor baut es wieder ab. Aber der Programmierer muss sich nicht darum bemühen, dass Speicher besorgt und freigegeben wird, denn genau dafür sind die Operatoren new und delete da.

In der Regel besteht kein Grund, die Operatoren zu überladen, möchte man jedoch die Speicherverwaltung optimieren, zum Beispiel weil man sehr viele, sehr kleine Instanzen in einem Memorypool unterbringen möchte, so kann man die operatoren new und delete entsprechend überladen.

lokale Überladung

Grundsätzlich lässt sich so eine beliebige Speicherverwaltung aufbauen, da es hier allerdings nur um die Überladung des Operators geht, überlade ich die Operatoren hier in der Form, dass sie zusätzlich einen Text auf den Bildschirm schreiben, wenn sie Speicher anfordern oder freigeben:

class Fraction
{
  ...
    inline void * operator new( size_t size )
    {
      void * address = ::operator new( size );
      printf( ">>> Speicher für Fraction angefordert: %d Bytes an %x\n", size, static_cast( address ) );
      return address;
    }
    inline void * operator delete( void * address )
    {
      printf( "<<< Speicher von Fraction freigegeben: Adresse %x\n", address );
      return ::operator delete( size );
    }
  ...
};

Für die Klasse 'Fraction' wird nun mit Hilfe dieser Operatoren Speicher besorgt und wieder freigegeben. Dabei muss zum einen klargestellt werden, dass operator new() und operator delete() nur dann aufgerufen werden, wenn auch wirklich Speicher angefordert oder freigegeben werden soll. Bei Objekten, die auf dem Stack konstruiert werden, wird kein Speicher angefordert. Hier ist die klare Unterscheidung zu Konstruktoren und Destruktoren.

Die operatoren arbeiten übrigens immer mit void-Zeigern. Die Konvertierung erfolgt später automatisch durch die Programmiersprache.

globale Überladung

Weiterhin fällt auf, dass die selbstgeschriebenen Operatoren für die Klasse Fraction globale Operatoren rufen. Es gibt also auch einen globalen operator new() und einen globalen operator delete(). Diese können natürlich auch überladen werden, trotzdem sollte dies nach Möglichkeit vermieden werden, so lässt sich zum Beispiel eine derartige Überwachung der angeforderten Speicherbereiche erreichen, so dass man am Ende des Programms herausfinden kann, welche Bereiche welcher Größe nicht mehr freigegeben wurden. (Hier bieten sich aber eher Tools wie valgrind an).

void * operator new( size_t size )
{
  void * result = malloc( size );
 
  if( result )
    return result;
  else
    printf( "Keinen Speicher erhalten\n" );
 
  return NULL;      
}

In einem Projekt, an dem mehrere Personen arbeiten, kann das Überladen globaler Funktionalität unerwartete Ergebnisse bei den Kollegen hervorrufen. Beschränken Sie sich also darauf, ihre eigenen Klassen zu spezialisieren oder bieten Sie Ihre Spezialisierung als eigene Klasse an, so dass jede Klasse die Spezialisierung erben kann, wenn sie von Ihrem Verfahren profitiert.

Welche Varianten gibt es?

Der New-Operator darf eine Exception werfen, wenn die Initialisierung fehlgeschlagen ist. Dies ist bad_alloc. new gibt es zum einen in einer Fassung, die Exceptions werfen darf und einer, die keine Exceptions werfen darf. Weiterhin gibt es für beide Varianten eigene Varianten für Arrays. Für jede Variante gibt es das entsprechende Delete.

void * operator new( size_t size ) throw( bad_alloc );         // ein Objekt, wirft Exception
void * operator new( size_t size, const nothrow & ) throw();   // ein Objekt, wirft keine Exception
void * operator new[]( size_t size ) throw( bad_alloc );       // Array, wirft Exception
void * operator new[]( size_t size, const nothrow & ) throw(); // Array, wirft keine Exception
 
void operator delete( size_t size ) throw();                   // löscht ein Objekt
void operator delete( size_t size, const nothrow & ) throw();  // löscht ein unvollständiges konstruiertes Objekt, wenn new(nothrow) fehlgeschlagen ist
void operator delete[]( size_t size ) throw();                 // löscht ein Objekt-Array
void operator delete[]( size_t size, const nothrow & ) throw();// löscht ein unvollständig konstruiertes Objekt-Array, wenn new(nothrow) [] fehlgeschlagen ist