Warum in C++ Arrays per delete gelöscht werden müssen[] / Sudo Null IT-Nachrichten

Der Hinweis ist für C++-Anfänger-Programmierer gedacht, die sich fragen, warum überall gesagt wird, dass Sie delete verwenden müssen[] für Arrays, aber statt einer klaren Erklärung verstecken sie sich einfach hinter dem magischen “undefinierten Verhalten”. Ein bisschen Code, ein paar Bilder und ein Blick unter die Haube von Compilern – ich bitte alle Interessierten unter cat.

delete_or_delete_for_array_en/image1.png

Einführung

Vielleicht haben Sie es nicht bemerkt oder einfach nicht darauf geachtet, aber wenn Sie den Code schreiben, um den von Arrays belegten Speicher freizugeben, müssen Sie nicht die Anzahl der zu entfernenden Elemente schreiben. Alles in allem funktioniert es super.

int *p = neue SomeClass[42]; // Geben Sie die Anzahl der Löschvorgänge an[] p; // Zahl nicht angeben

Was ist das, Magie? Teilweise ja. Darüber hinaus sehen und implementieren es Entwickler verschiedener Compiler unterschiedlich.

delete_or_delete_for_array_en/image2.png

Es gibt zwei Hauptansätze, wie sich Compiler die Anzahl der Elemente in einem Array merken:

Überverteilung

Die erste Methode wird, wie der Name schon sagt, durch einfaches Aufzeichnen der Anzahl der Elemente implementiert Vor Reihe. Beachten Sie, dass Sie in diesem Fall den Zeiger erhalten, nachdem Sie die Anweisung ausgeführt haben Neuzeigt auf das erste Element des Arrays, nicht auf seinen tatsächlichen Anfang.

delete_or_delete_for_array_en/image3.png

Unter keinen Umständen sollte ein solcher Zeiger an einen regulären Operator übergeben werden. löschen. Höchstwahrscheinlich wird es einfach das erste Element des Arrays entfernen und den Rest unberührt lassen. Beachten Sie, dass ich aus einem bestimmten Grund “höchstwahrscheinlich” geschrieben habe – schließlich kann niemand garantieren, was tatsächlich passieren wird und wie sich Ihr Programm weiter verhalten wird. Es hängt alles davon ab, welche Objekte sich im Array befanden und ob sie etwas Wichtiges in ihren Destruktoren getan haben. Das heißt, wir bekommen den Klassiker undefiniertes Verhalten. Stimmen Sie zu, das ist nicht das, was Sie erwarten, wenn Sie versuchen, ein Array zu löschen.

Eine interessante Tatsache: In den meisten Implementierungen der Standardbibliothek ist der Operator löschen darin ruft es nur die Funktion auf frei. Wenn wir einen Zeiger auf ein Array übergeben, erhalten wir ein weiteres undefiniertes Verhalten. Dies liegt daran, dass diese Funktion am Eingang einen Zeiger erwartet, der als Ergebnis der Arbeit der Funktionen erhalten wird calloc, malloc oder Reallok. Und wie wir oben herausgefunden haben, geschieht dies nicht, weil die Variable am Anfang des Arrays versteckt und der Zeiger auf den Anfang des Arrays verschoben wird.

Was ist der Unterschied zwischen operator löschen[]? Und er liest einfach die Anzahl der Elemente im Array, ruft den Destruktor für jedes Objekt auf und löscht danach den Speicher (zusammen mit der versteckten Variablen).

Wenn es jemanden interessiert, dann verwandelt sich die Konstruktion in so etwas wie diesen Pseudocode löschen[] p; bei Anwendung dieser Strategie:

// Holen Sie sich die Anzahl der Elemente im Array size_t n = * (size_t*) ((char*)p – sizeof(size_t)); // Den Destruktor für jeden aufrufen while (n– != 0) { p[n].~IrgendeineKlasse(); } // Und zum Schluss den Speicheroperator delete aufräumen[] ((char*)p – sizeof(size_t));

Die Compiler MSVC, GCC und Clang verwenden diese Methode. Sie können dies überprüfen, indem Sie sich den Code zur Speicherbehandlung in den jeweiligen Repositories ansehen (GCC und Klirren) oder den Dienst nutzen Compiler-Explorer.

delete_or_delete_for_array_en/image4.png

Wie Sie im obigen Bild sehen können (der obere Teil ist der Code, der untere Teil ist die Assembler-Ausgabe des Compilers), habe ich einen einfachen Code skizziert, der eine Struktur und eine Funktion deklariert, um ein Array derselben Strukturen zu erstellen.

Hinweis: Ein leerer Destruktor für eine Struktur ist keineswegs redundanter Code. Tatsache ist, dass laut Itanium CXX ABI für Arrays, die aus Typen bestehen, mit trivialer Destruktor, muss der Compiler einen anderen Ansatz für die Speicherverwaltung verwenden. Tatsächlich gibt es noch ein paar weitere Anforderungen, die Sie alle in Abschnitt 2.7 „Neue Cookies des Array-Operators“ sehen können. Itanium CXX ABI. Es listet auch die Anforderungen auf, wo und wie Informationen über die Anzahl der Elemente in einem Array zu finden sind.

Was passiert aus Sicht des Assemblers in einfacher Sprache:

  • Zeile N3: Schreiben der erforderlichen Speichermenge (20 Bytes für 5 Objekte + 8 Bytes für die Array-Größe) in das Register;
  • Leitung N4: Vermittlungsruf Neu Speicher zuweisen;
  • Zeile N5: Schreiben der Anzahl der Elemente an den Anfang des zugewiesenen Speichers;
  • Zeile N6: Offset des Zeigers auf den Anfang des Arrays um sizeof(size_t)das Ergebnis ist der Rückgabewert.

Die Vorteile dieser Methode liegen in der einfachen Implementierung und Arbeitsgeschwindigkeit, die Nachteile sind jedoch, dass sie Fehler bei einer falschen Wahl des Bedieners nicht verzeiht löschen. Im besten Fall bekommen Sie sofort einen Programmabsturz mit dem Fehler „Heap Corrupt“ und im schlimmsten Fall suchen Sie lange und mühsam nach den Gründen für das seltsame Verhalten des Programms.

Assoziatives Array

Der zweite Weg impliziert die Existenz eines versteckten globalen Containers, der Zeiger auf Arrays speichert und wie viele Elemente sie enthalten. In diesem Fall gibt es keine versteckten Daten vor den Arrays und dem Aufruf löschen[] p; so umgesetzt:

// Holen Sie sich die Größe des Arrays aus dem versteckten globalen Speicher size_t n = arrayLengthAssociation.lookup(p); // Destruktoren für jedes Element aufrufen while (n– != 0) { p[n].~IrgendeineKlasse(); } // Speicher löschen Operator delete[] (p);

Nun, es sieht nicht so “magisch” aus wie die vorherige Version. Gibt es weitere Unterschiede? Ja.

Zusätzlich zu dem bereits erwähnten Mangel an versteckten Daten vor dem Array kommt es zu einer leichten Verlangsamung durch die Notwendigkeit, Daten im globalen Speicher nachzuschlagen. Wir kompensieren dies aber dadurch, dass das Programm bei der falschen Wahl des Operators nachsichtiger sein kann löschen.

Dieser Ansatz wurde im Compiler verwendet vorne. Wir werden nicht auf seine Implementierung eingehen, aber wenn jemand daran interessiert ist, sich mit den Innereien eines der ersten C ++ – Compiler zu befassen, kann dies getan werden GitHub.

Mini-Nachwort

All dies ist die interne Küche von Compilern, und Sie sollten sich nicht auf das eine oder andere Verhalten verlassen. Dies gilt insbesondere dann, wenn geplant ist, das Programm auf andere Plattformen zu portieren. Glücklicherweise gibt es mehrere Möglichkeiten, diese Fehlerklasse zu vermeiden:

  • Verwenden Sie Funktionsfamilien std::make_*. Zum Beispiel: std::make_unique, std::make_shared,…
  • Nutzen Sie beispielsweise statische Analysetools, um Fehler frühzeitig zu erkennen PVS-Studio. 😊

Wenn Sie sich für das Thema undefiniertes Verhalten und Funktionen von Compilern interessieren, kann ich Ihnen noch ein paar zusätzliche Materialien empfehlen:

Similar Posts

Leave a Reply

Your email address will not be published.