Heim > Backend-Entwicklung > C#.Net-Tutorial > Detaillierte Erläuterung der C++-Speicherverwaltung

Detaillierte Erläuterung der C++-Speicherverwaltung

黄舟
Freigeben: 2016-12-16 09:50:57
Original
1107 Leute haben es durchsucht

1. Die entsprechenden Neu- und Löschzeichen sollten in derselben Form vorliegen. Was ist an der folgenden Anweisung falsch?
string *stringarray = new string[100];
...
delete stringarray;

Alles scheint in Ordnung zu sein – ein neues entspricht einem Löschen – aber große Fehler sind versteckt : Das Verhalten des Programms ist nicht vorhersehbar. Mindestens 99 von 100 String-Objekten, auf die stringarray zeigt, werden nicht ordnungsgemäß zerstört, da ihre Destruktoren nie aufgerufen werden.

Bei der Verwendung von Neu passieren zwei Dinge. Zuerst wird Speicher zugewiesen (über die Funktion „Operator new“, siehe Punkt 7-10 und Punkt m8 für Details), und dann werden ein oder mehrere Konstruktoren für den zugewiesenen Speicher aufgerufen. Wenn Sie delete verwenden, passieren auch zwei Dinge: Zuerst werden ein oder mehrere Destruktoren aufgerufen, um den Speicher freizugeben, und dann wird der Speicher freigegeben (über die Operator-Delete-Funktion, siehe Punkte 8 und m8 für Details). Beim Löschen gibt es eine so wichtige Frage: Wie viele Objekte im Speicher müssen gelöscht werden? Die Antwort bestimmt, wie viele Destruktoren aufgerufen werden.

Diese Frage lautet einfach: Zeigt der zu löschende Zeiger auf ein einzelnes Objekt oder auf ein Array von Objekten? Nur Sie können das Löschen sagen. Wenn Sie delete ohne Klammern verwenden, geht delete davon aus, dass es auf ein einzelnes Objekt verweist. Andernfalls geht es davon aus, dass es auf ein Array zeigt:

string *stringptr1 = neuer String;
string * stringptr2 = neuer String [100];
...

delete stringptr1;//Ein Objekt löschen
delete [] stringptr2;//Ein Array von Objekten löschen

Wenn Sie Was wollen passiert, wenn „[]“ vor stringptr1 hinzugefügt wird? Die Antwort lautet: Das wäre unvorstellbar; was würde passieren, wenn Sie nicht „[]“ vor stringptr2 hinzufügen würden? Auch die Antwort lautet: unvorstellbar. Und bei festen Typen wie int sind die Ergebnisse unvorhersehbar, selbst wenn solche Typen keinen Destruktor haben. Daher ist die Regel zur Lösung dieser Art von Problem einfach: Wenn Sie beim Aufruf von new [] verwenden, sollten Sie beim Aufruf von delete auch [] verwenden. Wenn Sie beim Aufruf von new nicht [] verwenden, verwenden Sie beim Aufruf von delete nicht [].

Es ist besonders wichtig, diese Regel im Hinterkopf zu behalten, wenn Sie eine Klasse schreiben, die Zeigerdatenelemente enthält und mehrere Konstruktoren bereitstellt. Denn in diesem Fall müssen Sie in allen Konstruktoren, die Zeigerelemente initialisieren, dasselbe neue Formular verwenden. Welche Löschform wird ansonsten im Destruktor verwendet? Weitere Erläuterungen zu diesem Thema finden Sie unter Punkt 11.

Diese Regel ist auch sehr wichtig für Leute, die gerne typedef verwenden, da Programmierer, die typedef schreiben, anderen mitteilen müssen, welche Löschform verwendet werden soll, nachdem sie new verwendet haben, um ein Objekt des durch typedef delete definierten Typs zu erstellen . Zum Beispiel:

typedef string addresslines[4]; //Die Adresse einer Person, insgesamt 4 Zeilen, eine Zeichenfolge in jeder Zeile
//Da es sich bei „addresslines“ um ein Array handelt, verwenden Sie new:
string *pal = neue Adresszeilen; // Beachten Sie, dass „neue Adresszeilen“ eine Zeichenfolge* zurückgeben, die mit der Rückgabe von „neue Zeichenfolge[4]“ identisch ist.
Delete muss in Form eines entsprechenden Arrays erfolgen dazu:
delete pal; // Error!
delete [] pal;// Correct

Um Verwirrung zu vermeiden, ist es am besten, die Verwendung von Typedefs für Array-Typen zu vermeiden. Das ist eigentlich einfach, da die Standard-C++-Bibliothek (siehe Punkt 49) String- und Vektorvorlagen enthält und deren Verwendung den Bedarf an Arrays auf nahezu Null reduziert. Adresslinien können beispielsweise als Vektor von Zeichenfolgen definiert werden, d. h. Adresslinien können als Vektortyp definiert werden.

2. Rufen Sie delete für das Zeigerelement im Destruktor auf

In den meisten Fällen verwenden Klassen, die eine dynamische Speicherzuweisung durchführen, new, um Speicher im Konstruktor zuzuweisen, und verwenden Sie dann im Destruktor delete Speicher freigeben. Wenn Sie diese Klasse zum ersten Mal schreiben, ist dies sicherlich nicht schwierig, und Sie werden daran denken, am Ende für alle Mitglieder, denen in allen Konstruktoren Speicher zugewiesen ist, „delete“ zu verwenden.

Nachdem diese Klasse jedoch gewartet und aktualisiert wurde, wird die Situation schwierig, da der Programmierer, der den Code der Klasse geändert hat, nicht unbedingt die erste Person ist, die diese Klasse schreibt. Das Hinzufügen eines Zeigermitglieds bedeutet fast die folgende Arbeit:
·Initialisieren Sie den Zeiger in jedem Konstruktor. Wenn dem Zeiger bei einigen Konstruktoren kein Speicher zugewiesen werden soll, wird der Zeiger auf 0 initialisiert (d. h. ein Nullzeiger).
·Löschen Sie den vorhandenen Speicher und weisen Sie dem Zeiger über den Zuweisungsoperator neuen Speicher zu.
·Löschen Sie den Zeiger im Destruktor.

Wenn Sie vergessen, einen Zeiger im Konstruktor zu initialisieren oder ihn während des Zuweisungsvorgangs zu behandeln, tritt das Problem schnell und offensichtlich auf, sodass diese beiden Probleme in der Praxis nicht auftreten. Dann quälen Sie Sie. Wenn der Zeiger jedoch nicht im Destruktor gelöscht wird, zeigt er keine offensichtlichen äußeren Symptome. Stattdessen kann es sich lediglich um einen winzigen Speicherverlust handeln, der immer größer wird, bis er Ihren Adressraum auffrisst und zum Abstürzen des Programms führt. Da diese Situation oft nicht auffällt, müssen Sie sie sich jedes Mal klar merken, wenn Sie der Klasse ein Zeigermitglied hinzufügen.

Außerdem ist das Löschen des Nullzeigers sicher (da es nichts bewirkt). Daher zeigt beim Schreiben von Konstruktoren, Zuweisungsoperatoren oder anderen Mitgliedsfunktionen jedes Zeigermitglied der Klasse entweder auf den gültigen Speicher oder auf null. Dann können Sie sie in Ihrem Destruktor einfach löschen und löschen, ohne sich Gedanken darüber machen zu müssen, ob sie neu waren.

Natürlich sollte die Verwendung dieser Begriffe nicht uneingeschränkt gelten. Sie würden zum Beispiel sicherlich nicht „delete“ verwenden, um einen Zeiger zu löschen, der nicht mit „new“ initialisiert wurde, und genauso wie Sie ein Smart-Pointer-Objekt verwenden, ohne es löschen zu müssen, würden Sie niemals einen Zeiger löschen, der Ihnen übergeben wurde. Mit anderen Worten: Sofern das Klassenmitglied nicht ursprünglich new verwendet hat, ist die Verwendung von delete im Destruktor nicht erforderlich.

Apropos Smart Pointer: Hier ist eine Möglichkeit, das Löschen von Zeigermitgliedern zu vermeiden, indem diese Mitglieder durch Smart Pointer-Objekte wie auto_ptr in der C++-Standardbibliothek ersetzt werden. Um zu erfahren, wie es funktioniert, werfen Sie einen Blick auf die Abschnitte m9 und m10.

3. Bereiten Sie sich im Voraus auf unzureichenden Speicher vor
Der Operator new löst eine Ausnahme aus, wenn er die Speicherzuweisungsanforderung nicht abschließen kann (die vorherige Methode bestand darin, 0 zurückzugeben, und einige ältere Compiler tun dies immer noch. Sie können Richten Sie Ihren Compiler auch so ein, wenn Sie möchten. Ich werde die Diskussion dieses Themas auf das Ende dieses Artikels verschieben. Jeder weiß, dass der Umgang mit Ausnahmen, die durch unzureichendes Gedächtnis verursacht werden, eigentlich als moralischer Akt angesehen werden kann, in der Praxis jedoch so schmerzhaft sein wird wie ein Messerstich in den Hals. Manchmal lässt man es also in Ruhe, vielleicht ist es einem egal. Aber Sie müssen immer noch ein tiefes Schuldgefühl in Ihrem Herzen haben: Was ist, wenn mit dem Neuen wirklich etwas schief geht?
Sie werden natürlich über einen Weg nachdenken, mit dieser Situation umzugehen, nämlich zum alten Weg zurückzukehren und die Vorverarbeitung zu verwenden. In C ist es beispielsweise üblich, ein typunabhängiges Makro zum Zuweisen von Speicher zu definieren und zu prüfen, ob die Zuweisung erfolgreich ist. Für C++ könnte dieses Makro so aussehen:


#define new(ptr, type)
try { (ptr) = new type }
catch (std: :bad_alloc&) {assurance(0); }

("Langsam! Was macht std::bad_alloc?", fragen Sie sich vielleicht. bad_alloc ist der Ausnahmetyp, der ausgelöst wird, wenn der Operator new die Speicherzuweisungsanforderung nicht erfüllen kann. std ist der Name des Namensraums (siehe Punkt 28), in dem sich bad_alloc befindet, fragen Sie sich vielleicht: „Was ist der Nutzen von Assert?“ Wenn Sie sich die Standard-C-Header-Datei (oder deren Äquivalent) ansehen ), werden Sie feststellen, dass es sich bei „asser“ um ein Makro handelt. Dieses Makro prüft, ob der an es übergebene Ausdruck ungleich Null ist. Ist dies nicht der Fall, gibt es eine Fehlermeldung aus und ruft „abort“ nur dann auf, wenn der Standard nicht definiert ist Wenn das Makro ndebug verwendet wird, erfolgt dies im Debugging-Status, d. Behauptung)).

Das neue Makro weist nicht nur das oben erwähnte häufige Problem auf, nämlich die Verwendung von Assert, um gleichzeitig den Status zu überprüfen, der im veröffentlichten Programm auftreten kann (allerdings kann jederzeit ein unzureichender Speicher auftreten). , es wird auch in c + verwendet. Es gibt noch einen weiteren Fehler in +: Es berücksichtigt nicht die verschiedenen Arten, wie new verwendet werden kann. Wenn Sie beispielsweise ein Objekt vom Typ t erstellen möchten, gibt es im Allgemeinen drei gängige Syntaxformen. Sie müssen die Ausnahmen behandeln, die in jeder Form auftreten können:


new t;
new t(constrUCtor arguments);
new t[size];

Das Problem wird hier stark vereinfacht, da einige Leute auch den Operator new anpassen (überladen), sodass das Programm jede Syntax in neuer Form enthält .

Also, was tun? Wenn Sie eine sehr einfache Fehlerbehandlungsmethode verwenden möchten, können Sie Folgendes tun: Wenn die Speicherzuweisungsanforderung nicht erfüllt werden kann, rufen Sie eine zuvor angegebene Fehlerbehandlungsfunktion auf. Diese Methode basiert auf einer Konvention, das heißt, wenn der Operator new die Anforderung nicht erfüllen kann, ruft er eine vom Kunden angegebene Fehlerbehandlungsfunktion auf, bevor er eine Ausnahme auslöst – im Allgemeinen als New-Handler-Funktion bezeichnet. (Die eigentliche Arbeit des Operators new ist komplizierter, Einzelheiten finden Sie in Abschnitt 8.)

Bei der Angabe der Fehlerbehandlungsfunktion wird die Funktion set_new_handler verwendet. Sie ist in der Header-Datei grob wie folgt definiert:


typedef void (*new_handler)();
new_handler set_new_handler(new_handler p) throw();

Wie Sie sehen können, ist new_handler ein benutzerdefinierter Funktionszeigertyp, der auf zeigt a Eine Funktion, die Eingabeparameter entgegennimmt und keinen Wert zurückgibt. set_new_handler ist eine Funktion, die den Typ new_handler eingibt und zurückgibt.

Der Eingabeparameter von set_new_handler ist der Zeiger der Fehlerbehandlungsfunktion, die aufgerufen werden soll, wenn der Operator new keinen Speicher zuweisen kann, und der Rückgabewert ist der Zeiger der alten Fehlerbehandlungsfunktion, die bereits vor set_new_handler wirksam war wurde aufgerufen.

Sie können set_new_handler wie folgt verwenden:


// Funktion zum Aufrufen, wenn der Operator new nicht genügend Speicher zuweisen kann
void nomorememory()
{
cerr << "Anfrage nach Speicher konnte nicht erfüllt werden
";
abort();
}

int main()
{
set_new_handler(nomorememory);
int *pbigdataarray = new int[100000000];

...

}

Wenn der Operator new keinen Platz für 100.000.000 Ganzzahlen zuweisen kann, wird nomorememory aufgerufen und das Programm wird mit einer Fehlermeldung beendet. Dies ist besser, als den Systemkern einfach eine Fehlermeldung generieren zu lassen und das Programm zu beenden. (Übrigens, denken Sie darüber nach, was passieren wird, wenn cerr beim Schreiben der Fehlermeldung dynamisch Speicher zuweisen muss ...)

Wenn der Operator new die Speicherzuweisungsanforderung nicht erfüllen kann, ist die New-Handler-Funktion dies wird mehr als einmal aufgerufen, stattdessen wird es wiederholt, bis genügend Speicher gefunden wird. Den Code zum Implementieren wiederholter Aufrufe finden Sie in Punkt 8, wo ich zur Erklärung eine beschreibende Sprache verwende: Eine gut gestaltete New-Handler-Funktion muss eine der folgenden Funktionen implementieren.
·Generieren Sie mehr verfügbaren Speicher. Dadurch erhöht sich die Wahrscheinlichkeit, dass der nächste Versuch von Operator New, Speicher zuzuweisen, erfolgreich ist. Eine Möglichkeit, diese Strategie umzusetzen, besteht darin, beim Start des Programms einen großen Speicherblock zu reservieren und ihn dann beim ersten Aufruf von new-handler freizugeben. Die Freigabe wird von einigen Warnmeldungen an den Benutzer begleitet. Wenn beispielsweise die Speichermenge zu klein ist, schlägt die nächste Anfrage möglicherweise fehl, es sei denn, es ist mehr freier Speicherplatz vorhanden.
·Installieren Sie eine andere neue Handlerfunktion. Wenn die aktuelle New-Handler-Funktion nicht mehr verfügbaren Speicher erzeugen kann, weiß sie möglicherweise, dass eine andere New-Handler-Funktion mehr Ressourcen bereitstellen kann. In diesem Fall kann der aktuelle neue Handler einen anderen neuen Handler installieren, um ihn zu ersetzen (durch Aufruf von set_new_handler). Wenn der Operator new das nächste Mal den New-Handler aufruft, wird der zuletzt installierte verwendet. (Eine weitere Variante dieser Strategie besteht darin, dem New-Handler zu erlauben, sein eigenes Laufverhalten zu ändern, sodass er beim nächsten Aufruf etwas anderes tut. Dies geschieht, indem dem New-Handler erlaubt wird, statische Variablen zu ändern, die sich auf sein eigenes Verhalten auswirken Verhalten. oder globale Daten.
·Neuen Handler deinstallieren. Übergeben Sie also einen Nullzeiger an set_new_handler. Wenn der New-Handler nicht installiert ist, löst der Operator New eine Standardausnahme vom Typ std::bad_alloc aus, wenn die Speicherzuweisung fehlschlägt.
·Wirft std::bad_alloc oder andere Arten von Ausnahmen aus, die von std::bad_alloc fortgeführt werden. Solche Ausnahmen werden vom Operator new nicht abgefangen und daher an die Stelle gesendet, an der die Speicheranforderung ursprünglich gestellt wurde. (Das Auslösen von Ausnahmen unterschiedlichen Typs verstößt gegen die neue Ausnahmespezifikation des Operators. Das Standardverhalten in der Spezifikation besteht darin, Abort aufzurufen. Wenn der neue Handler also eine Ausnahme auslösen möchte, muss er sicherstellen, dass diese ab std::bad_alloc fortgesetzt wird. Für Weitere Informationen zu Ausnahmespezifikationen finden Sie unter Punkt m14.
·Keine Rückgabe. Der typische Ansatz besteht darin, Abort oder Exit aufzurufen. abort/exit finden Sie in der Standard-C-Bibliothek (und der Standard-C++-Bibliothek, siehe Punkt 49).

Die oben genannten Optionen bieten Ihnen große Flexibilität bei der Implementierung der New-Handler-Funktion.

Was bei der Behandlung von Speicherzuordnungsfehlern zu tun ist, hängt von der Klasse des zuzuweisenden Objekts ab:


Klasse x {
öffentlich:
statische Leere

outofmemory();

...

};

class y {
public:
static void outofmemory();

...

};

x* p1 = new x; // Wenn die Zuweisung erfolgreich ist, rufen Sie x::outofmemory
y* p2 = new y auf; / Wenn die Zuweisung nicht erfolgreich ist, rufen Sie y::outofmemory auf

C++ unterstützt die New-Handler-Funktion speziell für Klassen nicht und wird nicht benötigt. Sie können es selbst implementieren, indem Sie in jeder Klasse Ihre eigene Version von set_new_handler und den Operator new bereitstellen. Der set_new_handler einer Klasse kann einen neuen Handler für die Klasse angeben (genau wie der standardmäßige set_new_handler einen globalen neuen Handler angibt). Der Operator new einer Klasse stellt sicher, dass beim Zuweisen von Speicher für Objekte der Klasse der New-Handler der Klasse anstelle des globalen New-Handlers verwendet wird.

Angenommen, der Speicherzuweisungsfehler der Klasse x wird behandelt. Da der Operator new einem Objekt vom Typ x keinen Speicher zuordnen kann, muss die Fehlerbehandlungsfunktion jedes Mal aufgerufen werden, sodass ein statisches Mitglied des Typs new_handler in der Klasse deklariert werden muss. Dann sieht Klasse 🎜>
PRivate:
static new_handler currenthandler;
};

Statische Mitglieder einer Klasse müssen außerhalb der Klasse definiert werden. Da ich den Standardinitialisierungswert 0 des statischen Objekts übernehmen möchte, habe ich ihn bei der Definition von x::currenthandler nicht initialisiert.


new_handler x::currenthandler; //Der Standard-Currenthandler ist auf 0 (d. h. null) gesetzt
Die Funktion set_new_handler in der Klasse x speichert jeden an sie übergebenen Zeiger und gibt ihn beim Aufruf zurück Alle zuvor gespeicherten Zeiger. Genau das macht die Standardversion von set_new_handler:


new_handler x::set_new_handler(new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler;
}

Schauen Sie sich abschließend an, was der Operator new von x bewirkt:
1 Rufen Sie die Standardfunktion set_new_handler auf, und der Eingabeparameter ist die Fehlerbehandlungsfunktion von x. Dies macht die New-Handler-Funktion von x zu einer globalen New-Handler-Funktion. Beachten Sie, dass im folgenden Code das Symbol „::“ verwendet wird, um explizit auf den Standardbereich zu verweisen (die Standardfunktion „set_new_handler“ ist im Standardbereich vorhanden).

2. Rufen Sie den globalen Operator new auf, um Speicher zuzuweisen. Wenn die erste Zuweisung fehlschlägt, ruft der globale Operator new den New-Handler von x auf, da er gerade als globaler New-Handler installiert wurde (siehe 1.). Wenn es dem globalen Operator new schließlich nicht gelingt, Speicher zuzuweisen, löst er eine std::bad_alloc-Ausnahme aus, die vom x-Operator new abgefangen wird. Der x-Operator new stellt dann die ursprünglich ersetzte globale New-Handler-Funktion wieder her und kehrt schließlich zurück, indem er eine Ausnahme auslöst.

3. Unter der Annahme, dass der globale Operator new erfolgreich Speicher für das Objekt vom Typ x zuweist, ruft der Operator new von x den Standardsatz set_new_handler erneut auf, um die ursprüngliche globale Fehlerbehandlungsfunktion wiederherzustellen. Abschließend wird ein Zeiger auf den erfolgreich zugewiesenen Speicher zurückgegeben.
C++ macht das:


void * x::operator new(size_t size)
{
new_handler globalhandler = // Install new_handler of x
std: :set_new_handler (currenthandler);

void *memory;

try { // Versuchen Sie, Speicher zuzuweisen
memory = ::operator new(size);
}

catch (std::bad_alloc&) { // Den alten new_handler wiederherstellen
std::set_new_handler(globalhandler);
throw; // Ausnahme auslösen
}
std::set_new_handler(globalhandler ); // Den alten new_handler wiederherstellen
return memory;
}

Wenn Ihnen die wiederholten Aufrufe von std::set_new_handler oben nicht gefallen, können Sie Punkt m9 sehen, um sie zu entfernen.

Bei Verwendung der Speicherzuordnungsverarbeitungsfunktion der Klasse
x::set_new_handler(nomorememory);
// Nomorememory auf x setzen
// neue Verarbeitungsfunktion
x *px1 = new x;
// Wenn die Speicherzuweisung fehlschlägt,
// Nomorememory aufrufen
string *ps = new string;
// Wenn die Speicherzuweisung fehlschlägt, rufen Sie die globale New-Handling-Funktion auf

x::set_new_handler(0);
//Angenommen, die neue Handhabungsfunktion ist eine Behandlungsfunktion)

Sie werden feststellen, dass bei der Behandlung ähnlicher Situationen oben die Klasse nicht berücksichtigt wird , der Implementierungscode ist derselbe, daher ist es naheliegend, darüber nachzudenken, sie an anderen Stellen wiederzuverwenden. Wie Punkt 41 erklärt, können Fortsetzungen und Vorlagen verwendet werden, um wiederverwendbaren Code zu entwerfen. Dabei kombinieren wir beide Methoden passend zu Ihren Anforderungen.

Sie müssen lediglich eine Basisklasse im „Mixin-Stil“ erstellen, die es Unterklassen ermöglicht, eine bestimmte Funktion fortzusetzen – hier bezieht sich auf die Erstellung eines neuen Handlers der Klassenfunktion. Der Grund, warum eine Basisklasse entworfen wird, besteht darin, dass alle Unterklassen die Funktionen „set_new_handler“ und „operator new“ fortsetzen können, und die Vorlage ist so konzipiert, dass jede Unterklasse über unterschiedliche Currenthandler-Datenelemente verfügt. Das klingt kompliziert, aber Sie werden sehen, dass der Code tatsächlich sehr vertraut ist. Der einzige Unterschied besteht darin, dass es jetzt von jeder Klasse wiederverwendet werden kann.


template // Bietet Unterstützung für die Klasse set_new_handler
class newhandlersupport { // Basisklasse für „mixed style“
public:
static new_handler set_new_handler(new_handler p);

static void * Operator new(size_t size);

private:
static new_handler currenthandler;
};

template
new_handler newhandlersupport::set_new_handler( new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler;
}

template
void * newhandlersupport::operator new( size_t size)
{
new_handler globalhandler =
std::set_new_handler(currenthandler);
void *memory;
try {
memory = ::operator new(size);
}
catch (std::bad_alloc&) {
std::set_new_handler(globalhandler);
throw;
}

std::set_new_handler(globalhandler);
return Memory;
}
// Dies setzt jeden Currenthandler auf 0

template
new_handler newhandlersupport::currenthandler;
Fügen Sie mit dieser Template-Klasse die Funktion set_new_handler zur Klasse x hinzu Es ist ganz einfach: Lass es einfach hier vorziehen.)
class x: public newhandlersupport {

... // wie zuvor, aber keine Deklarationen für
}; // set_new_handler oder Operator new


Wenn Sie x verwenden, müssen Sie sich immer noch keine Gedanken darüber machen, was es hinter den Kulissen tut; Ihr alter Code funktioniert immer noch. Das ist großartig! Die Dinge, die Sie oft ignorieren, sind oft die vertrauenswürdigsten.

Die Verwendung von set_new_handler ist eine bequeme und einfache Möglichkeit, mit unzureichendem Speicher umzugehen. Das ist sicherlich viel besser, als jedes neue Modul in ein Try-Modul zu packen. Darüber hinaus erleichtern Vorlagen wie newhandlersupport das Hinzufügen eines bestimmten New-Handlers zu jeder Klasse. Die Fortsetzung im „gemischten Stil“ führt zwangsläufig zu mehreren Fortsetzungen. Sie müssen Punkt 43 lesen, bevor Sie zu diesem Thema übergehen.

Vor 1993 musste der Operator new immer 0 zurückgeben, wenn die Speicherzuweisung fehlschlug. Jetzt muss der Operator new die Ausnahme std::bad_alloc auslösen. Viele C++-Programme wurden geschrieben, bevor Compiler mit der Unterstützung der neuen Spezifikation begannen. Das C++-Standardkomitee wollte bestehenden Code, der der Return-0-Spezifikation folgte, nicht aufgeben und stellte daher alternative Formen von Operator New (und Operator New[] – siehe Punkt 8) bereit, die weiterhin Return-0-Funktionalität bereitstellen. Diese Formen werden „throwless“ genannt, weil sie keinen Throw verwenden, sondern ein Nothrow-Objekt am Einstiegspunkt mit new:


class widget { ... };

widget *pw1 = new widget; // Zuordnungsfehler löst std::bad_alloc if

if (pw1 == 0) ... // Diese Prüfung muss fehlschlagen
widget * pw2 = new (nothrow) widget; // Wenn die Zuordnung fehlschlägt, 0 zurückgeben

if (pw2 == 0) ... // Diese Prüfung kann erfolgreich sein

unabhängig davon, ob „normal“ verwendet wird (d. h. Wenn Sie die Form „Neu“ oder die „Nicht auslösende“ Form von „Neu“ verwenden, ist es wichtig, dass Sie auf einen Fehler bei der Speicherzuweisung vorbereitet sein müssen. Der einfachste Weg ist die Verwendung von set_new_handler, da dies für beide Formen funktioniert.

4. Befolgen Sie die Konventionen beim Schreiben von „Operator New“ und „Operator Delete“. Das Verhalten sollte mit dem Systemstandardoperator new übereinstimmen. Die eigentliche Implementierung ist: einen korrekten Rückgabewert haben; die Fehlerbehandlungsfunktion aufrufen, wenn der verfügbare Speicher nicht ausreicht (siehe Punkt 7); Vermeiden Sie außerdem, versehentlich die Standardform von new auszublenden, aber das ist das Thema von Punkt 9.

Der Teil über Rückgabewerte ist einfach. Wenn die Speicherzuweisungsanforderung erfolgreich ist, wird ein Zeiger auf den Speicher zurückgegeben. Wenn sie fehlschlägt, wird eine Ausnahme vom Typ std::bad_alloc ausgelöst, wie in Punkt 7 angegeben.

Aber so einfach liegen die Dinge nicht. Da der Operator new tatsächlich mehr als einmal versucht, Speicher zuzuweisen, muss er die Fehlerbehandlungsfunktion nach jedem Fehler aufrufen und erwartet außerdem, dass die Fehlerbehandlungsfunktion einen Weg findet, Speicher an anderer Stelle freizugeben. Der Operator new löst nur dann eine Ausnahme aus, wenn der Zeiger auf die Fehlerbehandlungsfunktion null ist.

Darüber hinaus verlangt der C++-Standard, dass der Operator new einen zulässigen Zeiger zurückgeben muss, selbst wenn er die Zuweisung von 0 Bytes Speicher anfordert. (Tatsächlich vereinfacht diese seltsam klingende Anforderung andere Teile der C++-Sprache)

Auf diese Weise sieht der Pseudocode des Operators new in Form eines Nicht-Klassenmitglieds wie folgt aus:
void * Operator New(size_t size) // Operator New kann auch andere Parameter haben
{

if (size == 0) { // Bei der Verarbeitung einer 0-Byte-Anfrage,
size = 1 ; // Behandle es als 1-Byte-Anfrage
}
while (1) {
Größe Bytes Speicher zuweisen;

if (Zuweisung erfolgreich)
return (Pointer to Memory);

// Wenn die Zuordnung nicht erfolgreich ist, ermitteln Sie die aktuelle Fehlerbehandlungsfunktion
new_handler globalhandler = set_new_handler(0);
set_new_handler(globalhandler);

if (globalhandler) (*globalhandler)();
else throw std::bad_alloc();
}
}

Der Trick bei der Handhabung einer Null-Byte-Anfrage ist um es als Anfrage zu behandeln. Bytes werden verarbeitet. Das mag seltsam erscheinen, aber es ist einfach, legal und effektiv. Und wie oft stoßen Sie auf eine Null-Byte-Anfrage?

Sie fragen sich vielleicht, warum die Fehlerbehandlungsfunktion im obigen Pseudocode auf 0 gesetzt und dann sofort wiederhergestellt wird. Dies liegt daran, dass es keine Möglichkeit gibt, den Zeiger direkt auf die Fehlerbehandlungsfunktion abzurufen. Daher muss er durch Aufrufen von set_new_handler gefunden werden. Die Methode ist dumm, aber effektiv.


Punkt 7 erwähnt, dass der Operator new intern eine Endlosschleife enthält. Der obige Code veranschaulicht dies deutlich – while (1) verursacht eine Endlosschleife. Die einzige Möglichkeit, aus der Schleife auszubrechen, besteht darin, dass die Speicherzuweisung erfolgreich ist oder der Fehlerhandler eines der in Punkt 7 beschriebenen Ereignisse abschließt: Es wird mehr verfügbarer Speicher abgerufen, ein neuer neuer -Handler (Fehlerhandler) wird installiert; handler; hat eine Ausnahme von std::bad_alloc oder seinem abgeleiteten Typ ausgelöst; Jetzt verstehen wir, warum der New-Handler eine dieser Aufgaben erledigen muss. Wenn Sie dies nicht tun, wird die Schleife im Operator new nicht beendet.

Eine Sache, die vielen Menschen nicht bewusst ist, ist, dass der Operator new oft von Unterklassen geerbt wird. Dies führt zu einigen Komplikationen. Im obigen Pseudocode weist die Funktion große Bytes Speicher zu (es sei denn, die Größe ist 0). Die Größe ist wichtig, da sie der an die Funktion übergebene Parameter ist. Die meisten für Klassen neu geschriebenen Operatoren (einschließlich des in Punkt 10) sind jedoch nur für eine bestimmte Klasse konzipiert, nicht für alle Klassen oder alle ihre Unterklassen. Das bedeutet für einen Operator neuer Klasse Aufgrund der Existenz einer Fortsetzung kann jedoch der Operator new in der Basisklasse aufgerufen werden, um Speicher für ein Unterklassenobjekt zu reservieren:
class base {
public:
static void * Operator new(size_t size);
...
};

Klasse abgeleitet: öffentliche Basis // Die abgeleitete Klasse deklariert nicht den Operator new
{ ... }; //

abgeleitet *p = neu abgeleitet; // Call base::operator new

Wenn sich der Operator new der Basisklasse nicht die Mühe machen möchte, diese Situation gezielt zu behandeln – was unwahrscheinlich ist – dann ist es am einfachsten, diese „falsche“ Anzahl von Speicherzuweisungsanforderungen an den Standardoperator Process zu übergeben , wie folgt:
void * base::operator new(size_t size)
{
if (size != sizeof(base)) // Wenn die Menge „falsch“ ist, lassen Sie den Standardoperator new
return ::operator new(size); // Diese Anfrage verarbeiten
//

... // Ansonsten diese Anfrage verarbeiten
}

"Stopp! „Ich habe Sie schreien hören: „Sie haben vergessen, eine Situation zu überprüfen, die unvernünftig, aber möglich ist – Größe kann Null sein!“ Ja, ich habe nicht nachgesehen, aber bitte rufen Sie das nächste Mal laut. Seien Sie nicht so förmlich, wenn es darum geht Zeit. :) Tatsächlich wird die Prüfung aber trotzdem durchgeführt, ist aber in die size != sizeof(base)-Anweisung integriert. Der C++-Standard ist seltsam. Einer davon ist, dass alle freistehenden Klassen eine Größe ungleich Null haben. Daher kann sizeof(base) niemals Null sein (auch wenn die Basisklasse keine Mitglieder hat). Wenn size Null ist, wird die Anfrage an ::operator new weitergeleitet, der die Anfrage auf angemessene Weise behandelt. (Interessanterweise kann sizeof(base) Null sein, wenn base keine unabhängige Klasse ist, siehe „Mein Artikel zum Zählen von Objekten“ für Details).

Wenn Sie die Speicherzuweisung eines klassenbasierten Arrays steuern möchten, müssen Sie die Array-Form von „Operator New“ implementieren – „Operator New[]“ (diese Funktion wird oft „Array New“ genannt, da dies nicht möglich ist). Denken Sie an „Operator new[“ ]“)Wie man ausspricht). Denken Sie beim Schreiben des Operators new[] daran, dass Sie es mit „rohem“ Speicher zu tun haben und keine Operationen an Objekten ausführen können, die noch nicht im Array vorhanden sind. Tatsächlich wissen Sie nicht einmal, wie viele Objekte sich im Array befinden, da Sie nicht wissen, wie groß jedes Objekt ist. Der Operator new[] der Basisklasse wird verwendet, um durch Fortsetzung Speicher für das Array von Unterklassenobjekten zu reservieren, und Unterklassenobjekte sind häufig größer als die Basisklasse. Daher kann nicht davon ausgegangen werden, dass die Größe jedes Objekts in base::operator new[] sizeof(base) ist. Mit anderen Worten, die Anzahl der Objekte im Array ist nicht unbedingt (Anzahl der angeforderten Bytes)/sizeof (Base). Eine ausführliche Einführung zum Operator new[] finden Sie in Klausel m8.

Das sind alle Konventionen, die beim Überschreiben von „Operator New“ (und „Operator New[]“ zu befolgen sind). Für den Operator delete (und seinen Partneroperator delete[]) ist die Situation einfacher. Sie müssen lediglich bedenken, dass C++ garantiert, dass das Löschen eines Nullzeigers immer sicher ist. Sie müssen diese Garantie also voll ausnutzen. Das Folgende ist der Pseudocode des Operators delete in Form von Nicht-Klassenmitgliedern:
void Operator delete(void *rawmemory)
{
if (rawmemory == 0) return; wenn der Zeiger null ist, Return
//

Gib den Speicher frei, auf den rawmemory zeigt;

return;
}

Die Klassenmitgliedsversion davon Die Funktion ist ebenfalls einfach, muss jedoch auch überprüft werden Die Größe des gelöschten Objekts. Angenommen, der Operator new der Klasse leitet die Zuordnungsanforderung der „falschen“ Größe an::operator new weiter, dann muss die Löschanforderung der „falschen“ Größe auch an ::operator delete:

weitergeleitet werden class base { // und das Gleiche wie zuvor, außer dass
public hier deklariert wird: // Operator delete
static void * Operator new(size_t size);
static void Operator delete(void *rawmemory, size_t size);
.. .
};

void base::operator delete(void *rawmemory, size_t size)
{
if (rawmemory == 0) return; // Auf Nullzeiger prüfen

if (size != sizeof(base)) { // Wenn size „falsch“ ist,
::operator delete(rawmemory); // Den Standardoperator verarbeiten lassen die Anfrage
return;
}

Speicher freigeben, der auf rawmemory;

return;
}

Es ist ersichtlich, dass die Vorschriften über „Operator new“ und „Operator delete“ (und ihre Array-Formen) sind nicht so problematisch, es ist wichtig, sich daran zu halten. Solange der Speicherzuweiser die New-Handler-Funktion unterstützt und Null-Speicheranforderungen korrekt verarbeitet, ist das alles; wenn der Speicher-Deallocator Nullzeiger verarbeitet, gibt es nichts anderes zu tun. Das Hinzufügen von Fortsetzungsunterstützung zur Klassenmitgliedsversion der Funktion wird bald erfolgen.
5. Vermeiden Sie es, die Standardform von new        auszublenden
Da der im inneren Bereich deklarierte Name denselben Namen im äußeren Bereich verbirgt, also für zwei Funktionen mit demselben Namen, der innerhalb der Klasse
und global deklariert ist Für f verbirgt die Mitgliedsfunktion der Klasse die globale Funktion:
void f(); // Globale Funktion
class x {
public:
void f(); // Mitgliedsfunktion
};

x x;

f(); // Rufe f

x.f(); // Rufe x::f

auf Dies ist weder überraschend noch verwirrend, da der Aufruf globaler Funktionen und Mitgliedsfunktionen immer eine unterschiedliche

-Syntax verwendet. Wenn Sie der Klasse jedoch eine Operator-Neufunktion mit mehreren Parametern hinzufügen, lautet das Ergebnis

, was überraschend sein kann.

class x {
public:
void f();

// Der Parameter des Operators new gibt einen
// New-Hander an (News Fehlerbehandlung) Funktion
static void * Operator new(size_t size, new_handler p);
};

void specialerrorhandler(); // an anderer Stelle definiert

x *px1 =
new (specialerrorhandler) x; // Call x::operator new

x *px2 = new x; // Error!

Nach der Definition einer Funktion namens „Operator New“ in einer Klasse wird der Zugriff auf Standard New versehentlich blockiert. In Punkt 50 wird erläutert, warum dies so ist. Uns geht es hier jedoch mehr darum, wie wir dieses Problem vermeiden können.

Eine Möglichkeit besteht darin, einen neuen Operator in der Klasse zu schreiben, der die Standard-New-Aufrufmethode unterstützt. Er macht das Gleiche wie die Standard-New-Aufrufmethode. Dies kann mithilfe einer effizienten Inline-Funktion implementiert werden.

class x {
public:
void f();

static void * Operator new(size_t size, new_handler p);

static void * Operator new(size_t size)
{ return ::operator new(size); }
};

x *px1 =
new (specialerrorhandler) x; // Call x:: Operator
// new(size_t, new_handler)

x* px2 = new x; // Call x::operator
// new(size_t)

Eine weitere Methode besteht darin, Standardwerte für jeden dem Operator new hinzugefügten Parameter bereitzustellen (siehe Punkt 24):

class x {
public:
void f();

static
void * Operator new(size_t size, // Der Standardwert von p ist 0
new_handler p = 0); //
};

x *px1 = new (specialerrorhandler) x ; // Korrigieren

x* px2 = new x; // Auch richtig

Egal welche Methode verwendet wird, wenn Sie neue Funktionen für die „Standard“-Form von new in anpassen möchten In Zukunft müssen Sie nur noch einmal schreiben. Schreiben Sie diese Funktion.

Der Aufrufer kann die neue Funktion nach Neukompilierung und Verknüpfung nutzen.

6. Wenn Sie „Operator New“ schreiben, müssen Sie auch „Operator Delete“ schreiben ?

Die Antwort lautet normalerweise: für Effizienz. Die Standardoperatoren new und delete sind sehr vielseitig und ihre Flexibilität ermöglicht auch eine weitere Verbesserung ihrer Leistung in bestimmten spezifischen Situationen. Dies gilt insbesondere für Anwendungen, die eine dynamische Zuweisung einer großen Anzahl kleiner Objekte erfordern.

Zum Beispiel gibt es eine Klasse, die ein Flugzeug darstellt: Die Klasse „Flugzeug“ enthält nur einen Zeiger, der auf die tatsächliche Beschreibung des Flugzeugobjekts zeigt (diese Technik wird in Punkt 34 erklärt):

class planerep { ... }; Stellt ein Flugzeugobjekt dar
//
class Airplane {
public:
...
private:
airplanerep *rep; Verweist auf die tatsächliche Beschreibung
};

Ein Flugzeugobjekt ist nicht groß, es enthält nur einen Zeiger (wie in Punkt 14 und m24 erläutert, wenn die Flugzeugklasse eine virtuelle Funktion deklariert, enthält sie implizit ein zweiter Zeiger). Wenn Sie jedoch „Operator New“ anrufen, um ein Flugzeugobjekt zuzuweisen, erhalten Sie möglicherweise mehr Speicher, als zum Speichern des Zeigers (oder Zeigerpaars) erforderlich ist. Der Grund, warum dieses scheinbar seltsame Verhalten auftritt, liegt darin, dass Operator new und Operator delete Informationen aneinander weitergeben müssen.

Da es sich bei der Standardversion von Operator New um einen Allzweck-Speicherzuweiser handelt, muss er in der Lage sein, Speicherblöcke beliebiger Größe zuzuweisen. Ebenso muss der Operator delete auch in der Lage sein, Speicherblöcke beliebiger Größe freizugeben. Wenn der Operator delete herausfinden möchte, wie viel Speicher er freigeben möchte, muss er wissen, wie viel Speicher ursprünglich vom Operator new zugewiesen wurde. Es gibt eine übliche Methode für den Operator new, dem Operator delete mitzuteilen, wie groß der ursprünglich zugewiesene Speicher ist, indem er vorab einige zusätzliche Informationen in den zurückgegebenen Speicher einfügt, um die Größe des zugewiesenen Speicherblocks anzugeben. Das heißt, wenn Sie die folgende Aussage schreiben:

Flugzeug *pa = neues Flugzeug;

erhalten Sie keinen Speicherblock, der so aussieht:

pa—— > Der Speicher des Flugzeugobjekts

Stattdessen erhalten Sie einen Speicherblock wie diesen:

pa——> Speicherblockgrößendaten + Der Speicher des Flugzeugobjekts

Bei sehr kleinen Objekten wie einem Flugzeug verdoppeln diese zusätzlichen Dateninformationen die erforderliche Speichergröße bei der dynamischen Zuweisung des Objekts (insbesondere, wenn die Klasse keine virtuellen Funktionen enthält).

Wenn die Software in einer Umgebung ausgeführt wird, in der der Speicher knapp ist, kann sie sich ein solch luxuriöses Speicherzuweisungsschema nicht leisten. Indem Sie einen Operator speziell für die Flugzeugklasse neu schreiben, können Sie die Tatsache nutzen, dass die Größe jedes Flugzeugs gleich ist, ohne jedem zugewiesenen Speicherblock zusätzliche Informationen hinzuzufügen.

Insbesondere gibt es eine Möglichkeit, Ihren benutzerdefinierten Operator new zu implementieren: Lassen Sie zunächst den Standardoperator new einige große Blöcke Rohspeicher zuweisen, wobei jeder Block groß genug ist, um viele Flugzeugobjekte aufzunehmen. Aus diesen großen Speicherblöcken wird der Speicherblock des Flugzeugobjekts entnommen. Speicherblöcke, die derzeit nicht verwendet werden, werden in verknüpften Listen – sogenannten freien verknüpften Listen – für die zukünftige Verwendung durch das Flugzeug organisiert. Es hört sich so an, als müsste jedes Objekt den Overhead eines Next-Felds tragen (um die verknüpfte Liste zu unterstützen), aber nicht: Der Platz im Rep-Feld wird auch zum Speichern des Next-Zeigers verwendet (da er nur für den verwendeten Speicherblock benötigt wird). als Flugzeugobjekt-Rep-Zeiger; in ähnlicher Weise benötigt nur der Speicherblock, der nicht als Flugzeugobjekt verwendet wird, den nächsten Zeiger, was durch Union erreicht werden kann.


Während der spezifischen Implementierung muss die Definition von Flugzeugen geändert werden, um eine angepasste Speicherverwaltung zu unterstützen. Sie können dies tun:

class Airplane { // Modifizierte Klasse – unterstützt benutzerdefinierte Speicherverwaltung
public: //

static void * Operator new(size_t size);

...

privat:
Gewerkschaft {
planerep *rep; // für verwendete Objekte
airplane *next; // für nicht verwendete Objekte (in freien verknüpften Listen)

// Klassenkonstanten, geben Sie an, wie viele
/ / plane-Objekte, um sie in einen großen Speicherblock einzufügen und sie später zu initialisieren
static const int block_size;

static Airplane *headoffreelist;

};

Das Obige Der Code fügt mehrere Deklarationen hinzu: eine Operator-Neufunktion, eine Union (wodurch die Felder „rep“ und „next“ denselben Platz belegen), eine Konstante (die die Größe des großen Speicherblocks angibt) und einen statischen Zeiger (der den Header einer freien verknüpften Liste verfolgt). ). Es ist wichtig, den Header-Zeiger als statisches Mitglied zu deklarieren, da es nur eine freie verknüpfte Liste für die gesamte Klasse und nicht für jedes Flugzeugobjekt gibt.

Es ist Zeit, die neue Operator-Funktion zu schreiben:

void * Airplane::operator new(size_t size)
{
// Leiten Sie die „falsche“ Größenanfrage an: :operator new() processing;
// Siehe Abschnitt 8 für Details
if (size != sizeof(airplane))
return ::operator new(size);

airplane * p = // p zeigt auf den Kopf der freien verknüpften Liste
headoffreelist; //

// Wenn p zulässig ist, verschieben Sie den Kopf auf das nächste Element
//
if ( p)
headoffreelist = p->next;

else {
// Wenn die freie verknüpfte Liste leer ist, weisen Sie einen großen Speicherblock zu,
// kann Flugzeuge in Blockgröße aufnehmen Objekte
airplane *newblock =
static_cast(::operator new(block_size *
sizeof(airplane)));

// Verknüpfen Sie jeden kleinen Speicherblock, um eine neue freie verknüpfte Liste zu bilden
//Überspringen Sie das 0. Element, da es an den Aufrufer des Operators new zurückgegeben wird
//
for (int i = 1; i < block_size-1; ++i)
newblock [i].next = &newblock[i+1];

// Beende die verknüpfte Liste mit einem Nullzeiger
newblock[block_size-1].next = 0;

/ / p wird auf den Kopf der Tabelle gesetzt und auf das
, auf das headoffreelist // zeigt, folgt der Speicherblock
p = newblock;
headoffreelist = &newblock[1];
}

return p;
}

Wenn Sie Punkt 8 lesen, werden Sie wissen, dass, wenn der Operator new die Speicherzuweisungsanforderung nicht erfüllen kann, eine Reihe von Routinen im Zusammenhang mit New-Handler-Funktionen und Ausnahmen auftreten wird ausgeführt. Der obige Code enthält diese Schritte nicht. Dies liegt daran, dass der von „Operator New“ verwaltete Speicher von::Operator New zugewiesen wird. Dies bedeutet, dass der Operator new nur dann fehlschlägt, wenn ::operator new fehlschlägt. Und wenn ::operator new fehlschlägt, führt es die Aktion des New-Handlers aus (kann mit dem Auslösen einer Ausnahme enden), sodass es nicht erforderlich ist, dass der Flugzeugbetreiber new auch damit umgeht. Mit anderen Worten, die Aktionen von new-handler sind tatsächlich immer noch vorhanden, man sieht sie nur nicht, sie sind in ::operator new versteckt.

Mit dem neuen Operator müssen als nächstes die statischen Datenelemente von Flugzeugen definiert werden:

airplane *airplane::headoffreelist;

const int Airplane:: block_size = 512;

Es besteht keine Notwendigkeit, headoffreelist explizit auf einen Nullzeiger zu setzen, da die Anfangswerte statischer Mitglieder standardmäßig auf 0 gesetzt sind. block_size bestimmt, wie groß ein Speicherblock sein soll, der von ::operator new erhalten werden soll.

Diese Version von Operator New wird sehr gut funktionieren. Es reserviert weniger Speicher für das Flugzeugobjekt als der Standardoperator new und läuft schneller, möglicherweise doppelt so schnell. Dies ist nicht überraschend. Der allgemeine Standardoperator new muss Speicheranforderungen unterschiedlicher Größe und interner und externer Fragmentierung verarbeiten, während Ihr Operator new nur mit einem Zeigerpaar in einer verknüpften Liste arbeitet. Es ist oft einfach, Flexibilität gegen Geschwindigkeit einzutauschen.

Im Folgenden besprechen wir das Löschen des Operators. Erinnern Sie sich an das Löschen durch den Operator? In diesem Artikel geht es um das Löschen durch Operatoren. Bisher hat die Flugzeugklasse jedoch nur „Operator New“ deklariert, nicht jedoch „Operator Delete“. Überlegen Sie, was passieren würde, wenn Sie den folgenden Code schreiben würden:

airplane *pa = new Airplane; // Call
// Airplane::operator new
...

delete pa; // Call::operator delete

Wenn Sie diesen Code lesen und die Ohren spitzen, hören Sie das Geräusch des abstürzenden und brennenden Flugzeugs und das Weinen des Programmierers. Das Problem besteht darin, dass der Operator new (der in Airplane definierte) einen Zeiger auf den Speicher ohne Header-Informationen zurückgibt, während der Operator delete (der Standardwert) davon ausgeht, dass der an ihn übergebene Speicher Header-Informationen enthält. Dies ist die Ursache der Tragödie.

Dieses Beispiel veranschaulicht ein allgemeines Prinzip: „Operator new“ und „Operator delete“ müssen gleichzeitig geschrieben werden, damit keine unterschiedlichen Annahmen auftreten. Wenn Sie Ihr eigenes Speicherzuweisungsprogramm schreiben, müssen Sie auch ein Freigabeprogramm schreiben. (Einen weiteren Grund, warum diese Regel befolgt werden sollte, finden Sie in der Seitenleiste zur Platzierung des Artikels zum Zählen von Objekten.)

Entwerfen Sie die Flugzeugklasse daher weiterhin wie folgt:

Klasse Flugzeug { // Dasselbe wie zuvor, aber mit dem Zusatz einer
public: // Deklaration des Operators delete
...

static void Operator delete(void *deadobject,
size_t size) ;

};

// Was dem Operator delete übergeben wird, ist ein Speicherblock. Wenn
/ seine Größe korrekt ist, wird er am Anfang des freien Speichers hinzugefügt Blockliste
/ /
void Airplane::operator delete(void *deadobject,

size_t size)
{
if (deadobject == 0) return; // Siehe Klausel 8

if (size != sizeof(airplane)) { // Siehe Punkt 8
::operator delete(deadobject);
return;
}

airplane *carcass =
static_cast (deadobject);

carcass->next = headoffreelist;
headoffreelist = carcass;
}

Weil die „falsche“ Größenanforderung früher an den Operator new übertragen wurde Wird der globale Operator new verwendet (siehe Punkt 8), so muss das „falsche“ Größenobjekt ebenfalls dem globalen Operator delete zur Verarbeitung übergeben werden. Wenn Sie dies nicht tun, reproduzieren Sie das Problem, das Sie zuvor so sehr versucht haben zu vermeiden – die syntaktische Diskrepanz zwischen „Neu“ und „Löschen“.

Interessanterweise ist der an den Operator delete übergebene size_t-Wert möglicherweise falsch, wenn das zu löschende Objekt von einer Klasse ohne virtuellen Destruktor geerbt wird. Aus diesem Grund müssen Basisklassen virtuelle Destruktoren haben, und Punkt 14 listet einen zweiten, zwingenderen Grund auf. Denken Sie hier einfach daran, dass der Operator delete möglicherweise nicht ordnungsgemäß funktioniert, wenn die Basisklasse einen virtuellen Konstruktor weglässt.

Alles ist schön und gut, aber ich kann an deinem Stirnrunzeln erkennen, dass du dir Sorgen über Speicherverluste machen musst. Wenn Sie über viel Entwicklungserfahrung verfügen, werden Sie nicht übersehen, dass der Flugzeugbetreiber new::operator new aufruft, um einen großen Teil des Speichers zu erhalten, der Operator delete des Flugzeugs diese jedoch nicht freigibt. Speicherleck! Speicherleck! Ich kann deutlich hören, wie in Deinem Kopf die Alarmglocken schrillen.

Aber bitte hören Sie sich meine Antwort genau an, hier liegt kein Speicherverlust vor!

Der Grund für Speicherlecks ist, dass der Zeiger auf den Speicher nach der Speicherzuweisung verloren geht. Ohne Müllbehandlung oder andere Mechanismen außerhalb der Sprache könnte dieser Speicher nicht zurückgewonnen werden. Im obigen Design gibt es jedoch keinen Speicherverlust, da der Speicherzeiger niemals verloren geht. Jeder große Speicherblock wird zunächst in Blöcke in Flugzeuggröße unterteilt, und diese Blöcke werden dann in einer freien verknüpften Liste platziert. Wenn der Client Airplane::operator new aufruft, wird der kleine Block aus der freien verknüpften Liste entfernt und der Client erhält einen Zeiger auf den kleinen Block. Wenn der Client den Operator delete aufruft, wird der kleine Block wieder auf die freie Liste gesetzt. Bei diesem Design werden alle Speicherblöcke entweder vom Flugzeugobjekt verwendet (in diesem Fall liegt es in der Verantwortung des Clients, Speicherlecks zu vermeiden) oder sie befinden sich auf einer freien verknüpften Liste (in diesem Fall verfügen die Speicherblöcke über Zeiger). Hier liegt also kein Speicherverlust vor.

Es stimmt jedoch, dass der von ::operator new zurückgegebene Speicherblock nie von Aircraft::operator delete freigegeben wurde. Dieser Speicherblock hat einen Namen, der als Speicherpool bezeichnet wird. Es gibt jedoch einen wichtigen Unterschied zwischen Speicherlecks und Speicherpools. Speicherlecks können unbegrenzt zunehmen, selbst wenn sich der Client gut verhält; die Größe des Speicherpools wird niemals die vom Client angeforderte maximale Speichermenge überschreiten.

Es ist nicht schwierig, das Speicherverwaltungsprogramm von Flugzeugen so zu ändern, dass der von ::operator new zurückgegebene Speicherblock automatisch freigegeben wird, wenn er nicht verwendet wird. Dies wird hier jedoch nicht durchgeführt. Es gibt zwei Gründe : der erste Grund und hängt mit Ihrer ursprünglichen Absicht zusammen, die Speicherverwaltung anzupassen. Es gibt viele Gründe, die Speicherverwaltung anzupassen. Der grundlegendste davon ist, dass Sie sicher sind, dass die Standardeinstellungen „Operator new“ und „Operator delete“ zu viel Speicher verbrauchen oder/und langsam ausgeführt werden. Jedes zusätzlich geschriebene Byte und jede zusätzliche geschriebene Anweisung, um diese großen Speicherblöcke zu verfolgen und freizugeben, führt dazu, dass die Software langsamer läuft und mehr Speicher verbraucht als bei Verwendung einer Speicherpoolstrategie. Wenn Sie beim Entwerfen einer Bibliothek oder eines Programms mit hohen Leistungsanforderungen erwarten, dass die Größe des Speicherpools in einem angemessenen Bereich liegt, verwenden Sie am besten die Speicherpoolmethode.

Der zweite Grund hängt mit dem Umgang mit einigen unangemessenen Programmverhaltensweisen zusammen. Unter der Annahme, dass das Speicherverwaltungsprogramm des Flugzeugs geändert wird, kann die Löschung durch den Bediener des Flugzeugs alle großen Speicherblöcke freigeben, in denen keine Objekte vorhanden sind. Schauen Sie sich dann das folgende Programm an: int main()
{
airplane *pa = new Airplane; // Erste Zuweisung: Holen Sie sich einen großen Speicherblock,
// generieren Sie eine kostenlose verknüpfte Liste usw.

pa löschen; //Der Speicherblock ist leer;
//Freigeben

pa = neues Flugzeug; //Erneut einen großen Speicherblock erhalten,
// Erstellen Sie eine kostenlose verknüpfte Liste usw.

pa löschen; // Der Speicherblock ist wieder leer,
// Freigeben

... // Sie haben eine Idee...

return 0;
}

Dieses schreckliche kleine Programm läuft langsamer und beansprucht mehr Speicher als ein Programm, das mit den Standardoperatoren new und delete geschrieben wurde, ganz zu schweigen von einem Programm, das mit geschrieben wurde ein Speicherpool.

Natürlich gibt es Möglichkeiten, mit dieser unvernünftigen Situation umzugehen, aber je mehr außergewöhnliche Situationen Sie in Betracht ziehen, desto wahrscheinlicher ist es, dass Sie die Speicherverwaltungsfunktion erneut implementieren, und was erhalten Sie am Ende? Speicherpools können nicht alle Speicherverwaltungsprobleme lösen und sind in vielen Situationen geeignet.

In der tatsächlichen Entwicklung müssen Sie häufig speicherpoolbasierte Funktionen für viele verschiedene Klassen implementieren. Sie werden denken: „Es muss eine Möglichkeit geben, diesen Speicherzuweiser mit fester Größe zu kapseln, damit er bequem verwendet werden kann.“ Ja, es gibt einen Weg. Auch wenn ich mich schon so lange mit dieser Klausel beschäftigt habe, möchte ich sie dennoch kurz vorstellen und die konkrete Umsetzung dem Leser als Übung überlassen.

Im Folgenden wird lediglich eine minimale Schnittstelle der Pool-Klasse angegeben (siehe Punkt 18). Jedes Objekt der Pool-Klasse ist ein Speicherzuweiser für einen bestimmten Objekttyp (dessen Größe im Konstruktor von angegeben wird). der Pool).

class pool {
public:
pool(size_t n); //
für ein Objekt der Größe n erstellen// Ein Allokator


void * alloc(size_t n); // Genügend Speicher für ein Objekt zuweisen
// Befolgen Sie die Operator-Neukonvention von Punkt 8

void free (void *p, size_t n); // Den Speicher, auf den p zeigt, in den Speicherpool zurückgeben
// Befolgen Sie die Operator-Löschkonvention in Klausel 8

~pool(); der Speicherpool Der gesamte Speicher in

};

Diese Klasse unterstützt die Erstellung von Poolobjekten, führt Zuweisungs- und Freigabevorgänge durch und wird zerstört. Wenn ein Poolobjekt zerstört wird, wird der gesamte von ihm zugewiesene Speicher freigegeben. Dies bedeutet, dass es nun eine Möglichkeit gibt, das speicherverlustartige Verhalten von Flugzeugfunktionen zu vermeiden. Dies bedeutet jedoch auch, dass bei einem zu schnellen Aufruf des Destruktors des Pools (nicht alle Objekte, die den Speicherpool verwenden, werden zerstört) einige Objekte feststellen, dass der von ihnen verwendete Speicher plötzlich nicht mehr vorhanden ist. Die Ergebnisse sind oft unvorhersehbar.

Mit dieser Pool-Klasse können sogar Java-Programmierer ganz einfach ihre eigenen Speicherverwaltungsfunktionen zur Flugzeugklasse hinzufügen:

Klasse Flugzeug {
öffentlich:

... // Gewöhnliche Flugzeugfunktionen

static void * Operator new(size_t size);
static void Operator delete(void *p, size_t size);


private:
airplanerep *rep; // Zeiger auf die eigentliche Beschreibung
static pool mempool; //Speicherpool für Flugzeuge

};

inline void * Airplane: :operator new(size_t size )
{ return mempool.alloc(size); }

inline void Airplane::operator delete(void *p,
size_t size)
{ mempool free(p, size) ; }

// Erstellen Sie einen Speicherpool für das Flugzeugobjekt,
// Implementieren Sie
pool Airplane::mempool(sizeof(airplane)) in der Klassenimplementierungsdatei ;

Dieses Design ist viel klarer und sauberer als das vorherige, da die Flugzeugklasse nicht mehr mit Nicht-Flugzeug-Code vermischt ist. Die Union, der Kopfzeiger der freien verknüpften Liste und die Konstanten, die die Größe des ursprünglichen Speicherblocks definieren, sind alle dort versteckt, wo sie sein sollten – in der Poolklasse. Lassen Sie die Programmierer, die den Pool schreiben, sich um die Details der Speicherverwaltung kümmern. Ihre Aufgabe besteht lediglich darin, dafür zu sorgen, dass die Flugzeugklasse ordnungsgemäß funktioniert.

Sie sollten jetzt verstehen, dass benutzerdefinierte Speicherverwaltungsprogramme die Leistung des Programms erheblich verbessern können und in Klassen wie Pool gekapselt werden können. Aber vergessen Sie bitte nicht den Hauptpunkt: „Operator New“ und „Operator Delete“ müssen gleichzeitig funktionieren. Wenn Sie also „Operator New“ schreiben, müssen Sie auch „Operator Delete“ schreiben.

Das Obige ist die detaillierte Erklärung der C++-Speicherverwaltung. Weitere verwandte Artikel finden Sie auf der chinesischen PHP-Website (m.sbmmt.com).


Verwandte Etiketten:
Quelle:php.cn
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage