[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 xy
die 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 new
aufruft, 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 new
beschafften 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 vonnew
unddelete
heißt das, dass das Verhalten vonoperator new
undoperator delete
verä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 new
undoperator delete
.Für das Überladen von
operator new
undoperator delete
gelten bestimmte Regeln und Richtlinien:Ort der Überladung und gruppenweise Überladung
operator new
undoperator delete
dü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 new
als Klassenmethode überladen, dann wird die Überladung für alle Speicheranforderungen der Klasse sowie der abgeleiteten Klassen benutzt, d.h.operator new
undoperator delete
werden vererbt.operator new
undoperator delete
sind 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 delete
auch ü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 new
ist immervoid*
und zeigt auf den allokierten Speicher oder istNULL
. Das erste Argument ist vom Typstd::size_t
und 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_t
hinausgehenden Argumente werden beim Aufruf im Quellcode nach demnew
-Operator angegeben:MyType* p = new (zweitesArgument, drittesArgument) MyType(konstruktorArgument);
Der Rückgabewert von
operator delete
istvoid
, 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 delete
gibt es nur zwei Standard-Versionen, weil es kein korrespondierendesdelete
zum Palcement-new
gibt. 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 new
ein Objekt vom Typ X konstruiert. Schlägt die Speicherbeschaffung endgültig fehl, so verlangt der Standard, dassoperator new
eine Exception vom Typstd::bad_alloc
wirft. 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_t
ist 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
nothrow
undvoid*
) 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 new
fü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 delete
deklariert, in (e) dienothrow
-Version. Die jeweiligen Aufrufe sind in (5) und (6) zu sehen. Dadelete
sowieso keine Exception werfen sollte sind die zwei unterschiedlichen Versionen desoperator delete
eigentlich redundant. Tatsächlich ist im Standard auch festgelegt, dass beidedelete
-Versionen Pointer entgegennehmen müssen, die entweder vom normalen oder vomnothrow-new
erzeugt 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