Überladen in C ++. Teil III. Überladen neuer / Löschanweisungen


Wir setzen die Serie "C ++, Graben in die Tiefe" fort. Der Zweck dieser Reihe ist es, so viel wie möglich über die verschiedenen Merkmale der Sprache zu erzählen, möglicherweise ganz besonders. In diesem Artikel geht es um das Überladen von Bedienern new/delete. Dies ist der dritte Artikel in der Reihe, der erste für Überladungsfunktionen und -vorlagen, der sich hier befindet , der zweite für Überladungsoperatoren, der sich hier befindet . Dieser Artikel schließt eine Serie mit drei Artikeln zum Überladen in C ++ ab.


Inhaltsverzeichnis


Inhaltsverzeichnis
  1. new/delete
    1.1.
    1.2.
    1.3.
  2. new/delete
    2.1.
    2.2.
      2.2.1. new/delete
      2.2.2. new/delete
      2.2.3.
      2.2.4.
      2.2.5. operator delete()
  3. new/delete
  4.
  5. -
  

1. Standardformulare für neue / gelöschte Operatoren


C ++ unterstützt mehrere Operatoroptionen new/delete. Sie können in Basisstandard, Zusatzstandard und Benutzerdefiniert unterteilt werden. In diesem Abschnitt und Abschnitt 2 werden Standardformulare erläutert. Benutzerdefinierte Formulare werden in Abschnitt 3 erläutert.

1.1. Grundlegende Standardformulare


Die wichtigsten Standardformen von Operatoren, new/deletedie beim Erstellen und Löschen eines Objekts und eines Arrays des Typs verwendet werden, sind Tfolgende:

new T(/*   */)
new T[/*   */]
delete ptr;
delete[] ptr;

Ihre Arbeit kann wie folgt beschrieben werden. Wenn der Operator aufgerufen wird new, wird dem Objekt zuerst Speicher zugewiesen. Wenn die Auswahl erfolgreich ist, wird der Konstruktor aufgerufen. Wenn der Konstruktor eine Ausnahme auslöst, wird der zugewiesene Speicher freigegeben. Wenn der Operator aufgerufen wird, deletegeschieht alles in umgekehrter Reihenfolge: Zuerst wird der Destruktor aufgerufen, dann wird der Speicher freigegeben. Der Destruktor sollte keine Ausnahmen auslösen.

Wenn der Bedienernew[]Beim Erstellen eines Arrays von Objekten wird zunächst Speicher für das gesamte Array zugewiesen. Wenn die Auswahl erfolgreich ist, wird der Standardkonstruktor (oder ein anderer Konstruktor, wenn es einen Initialisierer gibt) für jedes Element des Arrays ab Null aufgerufen. Wenn ein Konstruktor eine Ausnahme auslöst, wird für alle erstellten Elemente des Arrays der Destruktor in der umgekehrten Reihenfolge des Konstruktoraufrufs aufgerufen, und der zugewiesene Speicher wird freigegeben. Um ein Array zu löschen, müssen Sie den Operator aufrufen. delete[]Für alle Elemente des Arrays wird der Destruktor in umgekehrter Reihenfolge wie der Konstruktor aufgerufen. Anschließend wird der zugewiesene Speicher freigegeben.

Beachtung! Es ist notwendig, die richtige Form des Bedieners aufzurufendeleteabhängig davon, ob ein einzelnes Objekt oder ein Array gelöscht wird. Diese Regel muss strikt eingehalten werden, sonst kann es zu undefiniertem Verhalten kommen, dh es kann alles passieren: Speicherlecks, Absturz usw. Siehe [Meyers1] für Details.

In der obigen Beschreibung ist eine Klarstellung erforderlich. Für die sogenannten Trivialtypen (eingebaute Typen, Strukturen im C-Stil) darf der Standardkonstruktor nicht aufgerufen werden, und der Destruktor tut auf keinen Fall etwas.

Die Standardspeicherzuweisungsfunktionen lösen eine Typausnahme aus, wenn die Anforderung nicht erfüllt werden kann std::bad_alloc. Diese Ausnahme kann jedoch abgefangen werden. Dazu müssen Sie einen globalen Interceptor mithilfe eines Funktionsaufrufs installieren. set_new_handler()Weitere Informationen finden Sie unter [Meyers1].

Jede Form von Operatordeletesicher auf Nullzeiger anwenden.

Beim Erstellen eines Arrays mit einem Operator kann die new[]Größe auf Null gesetzt werden.

Beide Formen des Operators newermöglichen die Verwendung von Initialisierern in geschweiften Klammern.

new int{42}
new int[8]{1,2,3,4}

1.2. Zusätzliche Standardformulare


Beim Verbinden der Header-Datei <new>stehen 4 weitere Standardoperatorformulare zur Verfügung new:

new(ptr) T(/*  */);
new(ptr) T[/*   */];
new(std::nothrow) T(/*   */);
new(std::nothrow) T[/*   */];

Die ersten beiden werden als newnicht zuweisende Platzierung bezeichnet new. Ein Argument ptrist ein Zeiger auf einen Speicherbereich, der groß genug ist, um eine Instanz oder ein Array aufzunehmen. Außerdem muss der Speicherbereich eine geeignete Ausrichtung haben. Diese Version des Operators newweist keinen Speicher zu, sondern ruft nur den Konstruktor auf. Mit dieser Option können Sie die Phasen der Speicherzuweisung und Initialisierung von Objekten trennen. Diese Funktion wird in Standardcontainern aktiv verwendet. Der Operator deletefür auf diese Weise erstellte Objekte kann natürlich nicht aufgerufen werden. Um ein Objekt zu löschen, müssen Sie den Destruktor direkt aufrufen und dann den Speicher auf eine Weise freigeben, die von der Methode zum Zuweisen von Speicher abhängt.

Die zweiten beiden Optionen werden als Operator bezeichnet, der keine Ausnahmen newauslöst (nothrow new). Sie unterscheiden sich darin, dass sie zurückkehren nullptr, aber keine Typausnahme auslösen , wenn die Anforderung nicht erfüllt werden kann std::bad_alloc. Das Löschen eines Objekts erfolgt mit dem Hauptoperator delete. Diese Optionen gelten als veraltet und werden nicht zur Verwendung empfohlen.

1.3. Speicherzuordnung und freie Funktionen


Standardformen von Operatoren new/deleteverwenden die folgenden Zuordnungs- und Freigabefunktionen:

void* operator new(std::size_t size);
void operator delete(void* ptr);
void* operator new[](std::size_t size);
void operator delete[](void* ptr);
void* operator new(std::size_t size, void* ptr);
void* operator new[](std::size_t size, void* ptr);
void* operator new(std::size_t size, const std::nothrow_t& nth);
void* operator new[](std::size_t size, const std::nothrow_t& nth);

Diese Funktionen werden im globalen Namespace definiert. Die Speicherzuweisungsfunktionen für die Host-Anweisungen newbewirken nichts und kehren einfach zurück ptr.

C ++ 17 unterstützte zusätzliche Formen von Speicherzuweisungs- und Freigabefunktionen, die auf die Ausrichtung hinweisen. Hier sind einige davon:

void* operator new (std::size_t size, std::align_val_t al);
void* operator new[](std::size_t size, std::align_val_t al);

Diese Formulare sind für den Benutzer nicht direkt zugänglich. Sie werden vom Compiler für Objekte verwendet, deren Ausrichtungsanforderungen überlegen __STDCPP_DEFAULT_NEW_ALIGNMENT__sind. Das Hauptproblem besteht also darin, dass der Benutzer sie nicht versehentlich ausblendet (siehe Abschnitt 2.2.1). Denken Sie daran, dass es in C ++ 11 möglich wurde, die Ausrichtung von Benutzertypen explizit festzulegen.

struct alignas(32) X { /* ... */ };

2. Überladen der Standardformulare für neue / gelöschte Operatoren


Das Überladen der Standardformen von Operatoren new/deletebesteht darin, benutzerdefinierte Funktionen zum Zuweisen und Freigeben von Speicher zu definieren, deren Signaturen mit den Standardsignaturen übereinstimmen. Diese Funktionen können im globalen Namespace oder in einer Klasse definiert werden, jedoch nicht in einem anderen als dem globalen Namespace. Die Speicherzuweisungsfunktion für eine Standardhostanweisung newkann nicht im globalen Namespace definiert werden. Nach einer solchen Definition werden sie von den entsprechenden Operatoren new/deleteverwendet, nicht von den Standardoperatoren.

2.1. Überladung im globalen Namespace


Angenommen, in einem Modul in einem globalen Namespace werden benutzerdefinierte Funktionen definiert:

void* operator new(std::size_t size)
{
// ...
}

void operator delete(void* ptr)
{
// ...
}

In diesem Fall werden die Standardfunktionen zum Zuweisen und Freigeben von Speicher für alle Operatoraufrufe new/deletefür alle Klassen (einschließlich Standardklassen) im gesamten Modul tatsächlich ersetzt ( ersetzt). Dies kann zu völligem Chaos führen. Beachten Sie, dass der beschriebene Substitutionsmechanismus ein spezieller Mechanismus ist, der nur für diesen Fall implementiert ist, und kein allgemeiner C ++ - Mechanismus. In diesem Fall wird es beim Implementieren der Benutzerfunktionen zum Zuweisen und Freigeben von Speicher unmöglich, die entsprechenden Standardfunktionen aufzurufen. Sie sind vollständig ausgeblendet (der Operator ::hilft nicht), und wenn Sie versuchen, sie aufzurufen, erfolgt ein rekursiver Aufruf der Benutzerfunktion.

Globale Namespace-definierte Funktion

void* operator new(std::size_t size, const std::nothrow_t& nth)
{
// ...
}

Es wird auch das Standardproblem ersetzen, es treten jedoch weniger potenzielle Probleme auf, da der Operator, der keine Ausnahmen newauslöst, selten verwendet wird. Das Standardformular ist aber auch nicht verfügbar.

Die gleiche Situation mit Funktionen für Arrays.

Das Überladen von Anweisungen new/deleteim globalen Namespace wird dringend empfohlen.

2.2. Klassenüberlastung


Das Überladen von Operatoren new/deletein einer Klasse weist keine der oben beschriebenen Nachteile auf. Das Überladen ist nur beim Erstellen und Löschen von Instanzen der entsprechenden Klasse wirksam, unabhängig vom Kontext des Aufrufs von Operatoren new/delete. Bei der Implementierung benutzerdefinierter Funktionen zum Zuweisen und Freigeben von Speicher mithilfe des Operators können ::Sie auf die entsprechenden Standardfunktionen zugreifen. Betrachten Sie ein Beispiel.

class X
{
// ...
public:
    void* operator new(std::size_t size)
    {
        std::cout << "X new\n";
        return ::operator new(size);
    }

    void operator delete(void* ptr)
    {
        std::cout << "X delete\n";
        ::operator delete(ptr);
    }

    void* operator new[](std::size_t size)
    {
        std::cout << "X new[]\n";
        return ::operator new[](size);
    }

    void operator delete[](void* ptr)
    {
        std::cout << "X delete[]\n";
        ::operator delete[](ptr);
    }
};

In diesem Beispiel wird die Ablaufverfolgung einfach zu Standardoperationen hinzugefügt. Nun, in Bezug auf new X()und new X[N]wird diese Funktionen verwenden, um Speicher zuzuweisen und freizugeben.

Diese Funktionen sind formal statisch und können als deklariert werden static. Im Wesentlichen handelt es sich jedoch um eine Instanz. Mit dem Funktionsaufruf operator new()beginnt die Erstellung der Instanz, und der Aufruf der Funktion operator delete()schließt das Löschen ab. Diese Funktionen werden niemals für andere Aufgaben aufgerufen. Darüber hinaus ist die Funktion, wie nachstehend gezeigt wird, im operator delete()Wesentlichen virtuell. Es ist also richtiger, sie ohne zu deklarieren static.

2.2.1. Zugriff auf Standardformulare für neue / gelöschte Operatoren


Operatoren new/deletekönnen beispielsweise mit einem zusätzlichen Operator für die Bereichsauflösung verwendet werden ::new(p) X(). In diesem Fall wird die operator new()in der Klasse definierte Funktion ignoriert und der entsprechende Standard verwendet. Auf die gleiche Weise können Sie den Operator verwenden delete.

2.2.2. Andere Formen von Neu- / Löschoperatoren ausblenden


Wenn Xwir jetzt für die Klasse versuchen, Ausnahmen zu werfen oder nicht zu werfen new, erhalten wir eine Fehlermeldung. Tatsache ist, dass die Funktion operator new(std::size_t size)andere Formen verbirgt operator new(). Das Problem kann auf zwei Arten gelöst werden. Im ersten Schritt müssen Sie der Klasse die entsprechenden Optionen hinzufügen (diese Optionen sollten einfach die Operation der Standardfunktion delegieren). Im zweiten Fall müssen Sie beispielsweise einen Operator newmit einem Operator für die Bereichsauflösung verwenden ::new(p) X().

2.2.3. Standardbehälter


Wenn wir beispielsweise versuchen, die Instanzen Xin einem Standardcontainer zu platzieren std::vector<X>, werden wir feststellen, dass unsere Funktionen nicht zum Zuweisen und Freigeben von Speicher verwendet werden. Tatsache ist, dass alle Standardcontainer über einen eigenen Mechanismus zum Zuweisen und Freigeben von Speicher verfügen (eine spezielle Allokatorklasse, die ein Vorlagenparameter des Containers ist) und einen Platzierungsoperator zum Initialisieren der Elemente verwenden new.

2.2.4. Erbe


Funktionen zum Zuweisen und Freigeben von Speicher werden vererbt. Wenn diese Funktionen in der Basisklasse definiert sind, jedoch nicht in der abgeleiteten, werden die Operatoren für die abgeleitete Klasse überladen new/deleteund die in der Basisklasse definierten und zugewiesenen Funktionen werden zum Zuweisen und Freigeben von Speicher verwendet.

Betrachten Sie nun eine polymorphe Klassenhierarchie, in der jede Klasse Operatoren überlastet new/delete. Lassen Sie nun die Instanz der abgeleiteten Klasse mit dem Operator deleteüber einen Zeiger auf die Basisklasse löschen . Wenn der Destruktor der Basisklasse virtuell ist, garantiert der Standard, dass der Destruktor dieser abgeleiteten Klasse aufgerufen wird. In diesem Fall operator delete()ist auch der für diese abgeleitete Klasse definierte Funktionsaufruf garantiert . Somit ist die Funktion operator delete()tatsächlich virtuell.

2.2.5. Alternative Form der Operator-Funktion delete ()


In einer Klasse (insbesondere wenn Vererbung verwendet wird) ist es manchmal zweckmäßig, eine alternative Form der Funktion zu verwenden, um Speicher freizugeben:

void operator delete(void* p, std::size_t size);
void operator delete[](void* p, std::size_t size);

Der Parameter sizelegt die Größe des Elements fest (auch in der Version für das Array). In diesem Formular können Sie je nach abgeleiteter Klasse unterschiedliche Funktionen zum Zuweisen und Freigeben von Speicher verwenden.

3. Benutzeroperatoren neu / löschen


C ++ kann benutzerdefinierte Operatorformulare der newfolgenden Form unterstützen:

new(/*  */) T(/*   */)
new(/*  */) T[/*   */]

Damit diese Formulare unterstützt werden, müssen die geeigneten Funktionen zum Zuweisen und Freigeben von Speicher festgelegt werden:

void* operator new(std::size_t size, /* .  */);
void* operator new[](std::size_t size, /* .  */);
void operator delete(void* p, /* .  */);
void operator delete[](void* p, /* .  */);

Die Liste der zusätzlichen Parameter der Speicherzuweisungsfunktionen sollte nicht leer sein und nicht aus einem bestehen, void*oder const std::nothrow_t&ihre Signatur sollte nicht mit einem der Standardparameter übereinstimmen. Listen zusätzlicher Parameter in operator new()und operator delete()müssen übereinstimmen. An den Operator übergebene Argumente newmüssen zusätzlichen Parametern der Speicherzuordnungsfunktionen entsprechen. Eine benutzerdefinierte Funktion operator delete()kann auch im Formular mit einem optionalen Größenparameter vorliegen.

Diese Funktionen können im globalen Namespace oder in einer Klasse definiert werden, jedoch nicht in einem anderen als dem globalen Namespace. Wenn sie im globalen Namespace definiert sind, ersetzen sie die Standardfunktionen zum Zuweisen und Freigeben von Speicher nicht, sondern überladen sie, sodass ihre Verwendung vorhersehbar und sicher ist und Standardfunktionen immer verfügbar sind. Wenn sie in der Klasse definiert sind, verbergen sie die Standardformulare. Der Zugriff auf Standardformulare kann jedoch über den Operator erfolgen ::. Dies wird in Abschnitt 2.2 ausführlich beschrieben.

Benutzerdefinierte Operatorformulare newwerden als newbenutzerdefinierte Platzierung bezeichnet new. Sie sollten nicht mit dem newin Abschnitt 1.2 beschriebenen Standard-Platzierungsoperator (nicht zuweisend) verwechselt werden .

Das entsprechende Operatorformular deleteexistiert nicht. Es newgibt zwei Möglichkeiten, ein Objekt zu löschen, das mit einem benutzerdefinierten Operator erstellt wurde . Wenn eine benutzerdefinierte Funktion operator new()eine Speicherzuweisungsoperation an Standardspeicherzuweisungsfunktionen delegiert, kann ein Standardoperator verwendet werden delete. Wenn nicht, müssen Sie den Destruktor und dann die benutzerdefinierte Funktion explizit aufrufen operator delete(). Der Compiler ruft die benutzerdefinierte Funktion operator delete()nur in einem Fall auf: Wenn der newKonstruktor während der Operation des benutzerdefinierten Operators eine Ausnahme auslöst .

Hier ist ein Beispiel (im globalen Bereich).

void* operator new(std::size_t size, int a, const char* b)
{
    std::cout << "new " << a << " + " << b << "\n";
    return ::operator new(size);
}
void operator delete(void* p, int a, const char* b)
{
    std::cout << "delete " << a << " + " << b << "\n";
    ::operator delete(p);
}

class X {/* ... */};
X* p = new(42, "meow") X(); // : new 42 + meow
delete p; //   ::operator delete()

4. Definition der Speicherzuweisungsfunktionen


In diesen Beispielen entsprechen Benutzerfunktionen operator new()und operator delete()delegierte Operationen den Standardfunktionen. Manchmal ist diese Option nützlich, aber das Hauptziel der Überladung new/deletebesteht darin, einen neuen Mechanismus zum Zuweisen / Freigeben von Speicher zu erstellen. Die Aufgabe ist nicht einfach, und bevor man sie übernimmt, muss man alles sorgfältig durchdenken. Scott Meyers [Meyers1] diskutiert mögliche Motive für eine solche Entscheidung (das wichtigste ist natürlich die Effizienz). Er erörtert auch die wichtigsten technischen Probleme, die mit der korrekten Implementierung benutzerdefinierter Funktionen zum Zuweisen und Freigeben von Speicher (unter Verwendung der Funktion) verbunden sindset_new_handler(), Multithread-Synchronisation, Ausrichtung). Guntheroth bietet ein Beispiel für die Implementierung relativ einfacher benutzerdefinierter Speicherzuweisungs- und Freigabefunktionen. Bevor Sie Ihre eigene Version erstellen, sollten Sie nach vorgefertigten Lösungen suchen. Als Beispiel können Sie die Pool-Bibliothek aus dem Boost-Projekt holen.

5. Allokatorklassen von Standardcontainern


Wie oben erwähnt, verwenden Standardcontainer spezielle Allokatorklassen zum Zuweisen und Freigeben von Speicher. Diese Klassen sind Vorlagenparameter von Containern, und der Benutzer kann seine Version einer solchen Klasse definieren. Die Motive für eine solche Lösung sind ungefähr die gleichen wie für Überlastungsbetreiber new/delete. [Guntheroth] beschreibt, wie solche Klassen erstellt werden.

Referenzliste


[Guntheroth]
Gunteroth, Kurt. Optimierung von Programmen in C ++. Bewährte Methoden zur Steigerung der Produktivität .: Per. aus dem Englischen - SPb .: Alpha-book LLC, 2017.
[Meyers1]
Meyers, Scott. Effektive Nutzung von C ++. 55 sichere Möglichkeiten zur Verbesserung der Struktur und des Codes Ihrer Programme .: Per. aus dem Englischen - M.: DMK Press, 2014.

All Articles