[A] Überladung von Operatoren in C++ (Teil X) - new, delete & Co.
-
Überladung von Operatoren in C++ (Teil 4) - new, delete & Co.
Inhalt
- 1 operator new und der new-Operator
- 2 Überladung von operator new und operator delete
- 2.1 Standardüberladungen
- 2.2 Benutzerdefinierte Überladungen
- 3 Zu jedem Topf ein Deckel
- 4 Exceptionsicherheit
1 operator new und der new-Operator
Zum Grundlagenwissen in C++ gehört die Möglichkeit, mit new Speicher anzufordern und Objekte darin zu erzeugen sowie sie mit delete wieder zu zerstöen un den vorher angeforderten Speicher wieder freizugeben. Das sind jeweils zwei Aktionen als Folge von einem Token im Quelltext. Darin unterscheiden sich die Speicherverwaltungsoperatoren von den restlichen Operatoren in C++.
Im Folgenden sei der
XY-Operator jeweils das Token im Quelltext, währendoperator xydie zugehörige Funktion ist, die aufgerufen wird. Im Falle arithmetischen Operatoren wie z.B. +, - usw. korrespondieren beide direkt miteinander, d.h. der+-Operator im Quelltext wird vom Compiler direkt in einen Aufruf vonoperator+übersetzt. Bei den Speicherverwaltungsoperatoren ist das etwas anders:Betrachten wir folgenden Quelltext:
std::string* p1 = new std::string("gaga"); std::string* pN = new std::string[5] ("fivetimeswtf"); delete p1; delete[] pN;Gehen wir es Zeile für Zeile durch: in Zeile 1 steht der
new-Operator. Dies führt dazu, dass der Compiler zuerst die Funktionoperator newaufruft, die den nötigen Speicher beschafft, in dem Fall Speicher der Größesizeof(std::string). Danach ist er anders als bei den anderen Operatoren aber noch nicht fertig sondern ruft noch den Konstruktor für einstd::string-Objekt auf, dass in dem vonoperator newbeschafften Speicher entstehen soll. Dabei werden dem Konstruktor die übergebenen Argumente mitgegeben. Nachdem der Konstruktor abgearbeitet wurde wird der Zeiger auf den Speicherbereich und damit auf das neue Objekt zurückgegeben.In Zeile 2 passiert quasi das selbe, allerdings für mehrere Objekte gleichzeitig. Zuerst wird
operator new[]aufgerufen, um den nötigen Speicher für 5 strings zu beschaffen. Danach werden einer nach dem anderen die 5 strings im angeforderten Speicher initialisiert, und zwar alle mit dem selben übergebenen Argument. Nachdem das letzte Objekt initialisiert (d.h. bei Klassen konstruiert) wurde, wird ein Zeiger auf den beschafften Speicher, also auf das erste Objekt darin, zurückgegeben.Der
delete-Operator in Zeile 3 hat wie dernew-Operator zwei Aktionen zur Folge: Zuerst wird für das Objekt, auf das p1 zeigt, der Destruktor aufgerufen. Danach wird der Speicher, der vorher von new angefordert wurde, wieder freigegeben. Beimdelete[]-Operator in Zeile 4 ist das Verfahren ähnlich, nur dass hier der Destruktor fünfmal aufgerufen wird.2 Überladung von operator new und operator delete
Die Überladung von Operatoren in C++ bezieht sich einzig auf die zugrundeliegenden
operator-Funktionen, nicht jedoch auf das Verhalten, das der Compiler aus dem Auftauchen eines Operators im Quellcode macht. Im Falle vonnewunddeleteheißt das, dass das Verhalten vonoperator newundoperator deleteverändert werden kann, nicht jedoch die Tatsache, dass zum entsprechenden Zeitpunkt die Konstruktoren und Destruktoren aufgerufen werden. Das Selbe gilt natürlich füroperator new[]undoperator delete[]. Die beiden werden im Folgenden nicht weiter explizit erwähnt, es sei denn es gibt Besonderheiten im Vergleich zuoperator newundoperator delete.Für das Überladen von
operator newundoperator deletegelten bestimmte Regeln und Richtlinien:Ort der Überladung und gruppenweise Überladung
operator newundoperator deletedürfen nur als Methode einer Klasse oder im globalen Namespace überladen werden. Eine Überladung im globalen Namespace versteckt allerdings die bereits bekannten Standardimplementierungen, so dass darauf dann nicht mehr zugegriffen werden kann. In den meisten Fällen ist daher von der Überladung der globalen Funktionen abzuraten. Wirdoperator newals Klassenmethode überladen, dann wird die Überladung für alle Speicheranforderungen der Klasse sowie der abgeleiteten Klassen benutzt, d.h.operator newundoperator deletewerden vererbt.operator newundoperator deletesind implizit statische Methoden, d.h. der Compiler macht sie auch dannstatic, wenn das Schlüsselwort nicht mit angegeben wird.Wenn man
operator newüberlädt, dann tut man dies um ein bestimmtes Verhalten bei der Speicherbeschaffung zu erzielen. Im Normalfall bedeutet das, dass manoperator deleteauch überladen muss, um diesem Verhalten auch bei der Freigabe des Speichers gerecht zu werden. Zusätzlich wird in den meisten Fällen auch eine Überladung vonoperator new[]undoperator delete[]nötig sein, da das Verhalten bei der Allokation von Arrays nicht vom Verhalten bei der Allokation von Einzelobjekten abweichen sollte.Argumente und Rückgabewerte
Der Rückgabewert von
operator newist immervoid*und zeigt auf den allokierten Speicher oder istNULL. Das erste Argument ist vom Typstd::size_tund enthält die Größe des angeforderten Speicherbereichs. Beim Aufruf desnew-Operators wird das Argument nicht übergeben, es wird vom Compiler ermittelt, beinew T;ist es gleichsizeof(T). Rückgabewert und Argumente vonoperator new[]sind die selben, beinew T[N];berechnet der Compiler das erste Argument mitN * sizeof(T). Alle zusätzlichen, über dassize_thinausgehenden Argumente werden beim Aufruf im Quellcode nach demnew-Operator angegeben:MyType* p = new (zweitesArgument, drittesArgument) MyType(konstruktorArgument);Der Rückgabewert von
operator deleteistvoid, das erste Argument ist vom Typvoid*und zeigt auf den freizugebenden Speicherbereich. Gleiches gilt füroperator delete[].2.1 Standardüberladungen
Im Standard vorgegeben sind drei Überladungen von
operator new: Das "normale" new,nothrow-new und das sogenannte Placement-new. Füroperator deletegibt es nur zwei Standard-Versionen, weil es kein korrespondierendesdeletezum Palcement-newgibt. Die Syntax für die Überladung und Aufrufe sind wie folgt:class X { public: static void* operator new(std::size_t size); // (a) static void* operator new(std::size_t size, std::nothrow_t); // (b) static void* operator new(std::size_t size, void* ptr); // (c) static void operator delete(void* ptr); // (d) static void operator delete(void* ptr, std::nothrow_t); // (e) }; int main() { X* xptr1 = new X; // (1) X* xptr2 = new(std::nothrow) X; // (2) X* xptr3 = X::operator new(sizeof(X)); // (3) /*xptr3 =*/ new(xptr3) X; // (4) delete xptr1; // (5) delete(std::nothrow) xptr2; // (6) xptr3->~X(); // (7) operator delete(xptr3); // (8) }In (a) wird das "normale" new deklariert. Der Aufruf mittels
new-Operator ist in (1) zu sehen, wie in Abschnitt 1 erläutert wurde, wird hier direkt nach dem Aufruf desoperator newein Objekt vom Typ X konstruiert. Schlägt die Speicherbeschaffung endgültig fehl, so verlangt der Standard, dassoperator neweine Exception vom Typstd::bad_allocwirft. Was in diesem Kontext "endgültig" heißt, wird später erläutert.In (b) wird das
nothrow-new deklariert. Es akzeptiert ein Argument vom Typstd::nothrow_t. Der Standard definiert ein globales Objekt dieses Typs mit Namenstd::nothrow. Man braucht also kein eigenes Objekt des Typs zu schaffen sondern kann den Operator wie in (2) gezeigt mit dem globalen Objekt aufrufen. Der Parameter ist nur ein formaler Parameter und wird nicht ausgewertet (std::nothrow_tist eine leere Klasse). Im Gegensatz zum "normalen" new wirft dasnothrow-new keine Exception sondern liefert einen Nullpointer bei Nichterfolg. Das restliche Verhalten ist exakt das selbe wie beim "normalen"operator new. Dieses Verhalten existiert hauptsächlich aus Kompatibilitätsgründen mit Vorstandard-Versionen von C++, wo das "normale" new bei Nichterfolg einen Nullpointer zurücklieferte.In (c) wird das Standard-Placement-new deklariert. In vielen Fällen werden auch alle benutzerdefinierten Überladungen (d.h. mit anderen Parametern als
nothrowundvoid*) als Placement-new bezeichnet, ich werde der Klarheit wegen aber zwischen Placement-new und benutzerdefinierten Überladungen unterscheiden. Placement-new erwartet als zweiten Parameter einen Pointer, der auf einen bereits allokierten Speicherbereich zeigt, der groß genug ist um das zu erstellende Objekt darin zu konstruieren. Die Standard-Implementierung übernimmt keine weiteren Checks des Speicherblocks und ruft lediglich den Konstruktor für das neue Objekt auf. In (3) und (4) geschieht folgendes: In (3) wird die Funktionoperator newfür das "normale new" direkt aufgerufen, d.h. es wird nur der Speicher beschafft ohne die nachfolgende Konstruktion eines Objekts. In (4) wird danach mittels Placement-new in dem soeben beschafften rohen Speicher ein neues Objekt konstruiert.In (d) wird
operator deletedeklariert, in (e) dienothrow-Version. Die jeweiligen Aufrufe sind in (5) und (6) zu sehen. Dadeletesowieso keine Exception werfen sollte sind die zwei unterschiedlichen Versionen desoperator deleteeigentlich redundant. Tatsächlich ist im Standard auch festgelegt, dass beidedelete-Versionen Pointer entgegennehmen müssen, die entweder vom normalen oder vomnothrow-newerzeugt worden sind. Dies ist auch insofern konsequent, weil die beidennew-Versionen sich im Erfolgsfall nicht unterschiedlich verhalten sollten.2.2 Benutzerdefinierte Überladungen
Unter Berücksichtigung der oben genannten Anforderungen an die Rückgabetypen und den ersten Parameter sind alle möglichen Überladungen der Operatorfunktionen denkbar. Ein häufig genanntes Beispiel ist die Nutzung eines Memorymanagers. Der Memorymanager ist eine eigens definierte Klasse, die die Allokation von Speicher in großen Blöcken übernimmt und bei Bedarf kleinere Blöcke daraus für die Objekte zur Verfügung stellt. Zur Laufzeit wird eine oder mehrere Instanzen des Memorymanagers erstellt und bei den Operatoraufrufen jeweils mit übergeben. Beispiel:
class MemoryManager { public: /* Konstruktoren etc. */ void* get(size_t size); //besorgt Speicher der Größe N*size void release(void* there); //gibt den Speicher an der Stelle *there wieder frei. }; template <class T> struct Managed { static void* operator new(std::size_t size, MemoryManager& mm) { void* ptr = mm.get(size); if (ptr == NULL) throw std::bad_alloc(); //normales new wirft exception bei Fehlschlag return ptr; } static void* operator new(std::size_t size, std::nothrow_t&, MemoryManager& mm) { return mm.get(size); } static void operator delete(void* there, MemoryManager& mm) { mm.release(there); } static void operator delete(void* there, std::nothrow_t&, MemoryManager& mm) { mm.release(there); } static void operator new[](std::size_t size, MemoryManager& mm) { void* ptr = mm.get(size); if (ptr == NULL) throw std::bad_alloc(); //normales new wirft exception bei Fehlschlag return ptr; } // usw. };3 Zu jedem Topf ein Deckel
4 Exceptionsicherheit
Quellen/Links
http://www.informit.com/guides/content.aspx?g=cplusplus&seqNum=379