wake-up-neo.com

Woher weiß delete [], dass es sich um ein Array handelt?

Okay, ich denke, wir sind uns alle einig, dass das, was mit dem folgenden Code passiert, undefiniert ist, abhängig davon, was übergeben wird.

void deleteForMe(int* pointer)
{
     delete[] pointer;
}

Der Zeiger kann eine Vielzahl von verschiedenen Dingen sein, und daher ist es undefiniert, eine unbedingte delete[] - Anweisung darauf auszuführen. Nehmen wir jedoch an, dass wir tatsächlich einen Array-Zeiger übergeben,

int main()
{
     int* arr = new int[5];
     deleteForMe(arr);
     return 0;
}

Meine Frage ist, in diesem Fall, wo der Zeiger ist ein Array, wer ist es, der das weiß? Ich meine, aus der Sicht der Sprache/des Compilers hat es keine Ahnung, ob arr ein Array-Zeiger ist oder nicht, im Gegensatz zu einem Zeiger auf ein einzelnes int. Zum Teufel, es weiß nicht einmal, ob arr dynamisch erstellt wurde. Wenn ich stattdessen Folgendes tue,

int main()
{
     int* num = new int(1);
     deleteForMe(num);
     return 0;
}

Das Betriebssystem ist intelligent genug, um nur ein int zu löschen und keine Art von "Killing Spree" zu starten, indem der Rest des Speichers über diesen Punkt hinaus gelöscht wird (im Gegensatz zu strlen und einem non - \0 -terminierte Zeichenfolge - es wird so lange fortgesetzt, bis 0) erreicht wird.

Wessen Aufgabe ist es also, sich an diese Dinge zu erinnern? Bewahrt das Betriebssystem eine Art Aufzeichnung im Hintergrund auf? (Ich meine, ich erkenne, dass ich diesen Beitrag damit begonnen habe, dass das, was passiert, undefiniert ist, aber die Tatsache ist, dass das Szenario des "Amoklaufs" nicht passiert, also in der praktischen Welt . jemand erinnert sich.)

133
GRB

Der Compiler weiß nicht, dass es sich um ein Array handelt, er vertraut dem Programmierer. Das Löschen eines Zeigers auf ein einzelnes int mit delete [] Würde zu undefiniertem Verhalten führen. Ihr zweites Beispiel für main() ist unsicher, auch wenn es nicht sofort abstürzt.

Der Compiler muss jedoch nachverfolgen, wie viele Objekte irgendwie gelöscht werden müssen. Dies kann durch eine Überbelegung erfolgen, die ausreicht, um die Arraygröße zu speichern. Weitere Informationen finden Sie in den C++ Super FAQ .

98
Fred Larson

Eine Frage, die die bisher gegebenen Antworten nicht zu beantworten scheinen: Wenn die Laufzeitbibliotheken (nicht das Betriebssystem) die Anzahl der Dinge im Array verfolgen können, warum brauchen wir dann das delete[] Syntax überhaupt? Warum kann ein einzelnes delete Formular nicht für alle Löschvorgänge verwendet werden?

Die Antwort darauf geht zurück auf C++ 's Wurzeln als C-kompatible Sprache (die es eigentlich nicht mehr sein möchte). Die Philosophie von Stroustrup war, dass der Programmierer nicht für Features bezahlen muss, die er nicht verwendet. Wenn sie keine Arrays verwenden, sollten sie nicht die Kosten für Objektarrays für jeden zugewiesenen Speicherblock tragen müssen.

Das heißt, wenn Ihr Code es einfach tut

Foo* foo = new Foo;

dann sollte der für foo zugewiesene Speicherplatz keinen zusätzlichen Overhead enthalten, der zur Unterstützung von Arrays von Foo erforderlich wäre.

Da nur Array-Zuordnungen eingerichtet sind, die zusätzliche Informationen zur Array-Größe enthalten, müssen Sie die Laufzeitbibliotheken anweisen, beim Löschen der Objekte nach diesen Informationen zu suchen. Deshalb müssen wir verwenden

delete[] bar;

statt nur

delete bar;

if bar ist ein Zeiger auf ein Array.

Für die meisten von uns (auch für mich) scheint diese Aufregung um ein paar zusätzliche Bytes Speicher heutzutage kurios. Es gibt jedoch immer noch Situationen, in denen das Speichern einiger Bytes (von einer möglicherweise sehr hohen Anzahl von Speicherblöcken) wichtig sein kann.

102
Dan Breslau

Ja, das Betriebssystem behält einige Dinge im Hintergrund. Zum Beispiel, wenn Sie laufen

int* num = new int[5];

das Betriebssystem kann 4 zusätzliche Bytes zuweisen, die Größe der Zuweisung in den ersten 4 Bytes des zugewiesenen Speichers speichern und einen Versatzzeiger zurückgeben (dh, es weist die Speicherplätze 1000 bis 1024 zu, der zurückgegebene Zeiger zeigt jedoch auf 1004 mit Speicherplätzen 1000- 1003 Speichern der Größe der Zuordnung). Wenn delete aufgerufen wird, kann es 4 Bytes anzeigen, bevor der Zeiger an ihn übergeben wird, um die Größe der Zuordnung zu ermitteln.

Ich bin sicher, dass es andere Möglichkeiten gibt, die Größe einer Zuordnung zu verfolgen, aber das ist eine Option.

27
bsdfish

Dies ist sehr ähnlich zu this question und enthält viele der Details, nach denen Sie suchen.

Aber es reicht zu sagen, dass es nicht die Aufgabe des Betriebssystems ist, irgendetwas davon zu verfolgen. Es sind tatsächlich die Laufzeitbibliotheken oder der zugrunde liegende Speichermanager, die die Größe des Arrays verfolgen. Dies geschieht normalerweise durch Zuweisen von zusätzlichem Speicherplatz im Voraus und Speichern der Größe des Arrays an diesem Speicherort (die meisten verwenden einen Kopfknoten).

Dies kann bei einigen Implementierungen durch Ausführen des folgenden Codes angezeigt werden

int* pArray = new int[5];
int size = *(pArray-1);
13
JaredPar

delete oder delete[] würde wahrscheinlich beide den zugewiesenen Speicher freigeben (Speicher darauf), aber der große Unterschied ist, dass delete in einem Array nicht den Destruktor jedes Elements des Arrays aufruft.

Wie auch immer, mischen new/new[] und delete/delete[] ist wahrscheinlich UB.

9
Benoît

Es weiß nicht, dass es sich um ein Array handelt. Deshalb müssen Sie delete[] anstelle von normalem altem delete.

6
eduffy

Ich hatte eine ähnliche Frage dazu. In C ordnen Sie Speicher mit malloc () (oder einer ähnlichen Funktion) zu und löschen ihn mit free (). Es gibt nur ein malloc (), das einfach eine bestimmte Anzahl von Bytes zuweist. Es gibt nur ein freies (), das einfach einen Zeiger als Parameter verwendet.

Warum kann man in C den Zeiger nur an free übergeben, aber in C++ muss man angeben, ob es sich um ein Array oder eine einzelne Variable handelt?

Die Antwort, die ich gelernt habe, hat mit Klassendestruktoren zu tun.

Wenn Sie eine Instanz einer Klasse MyClass zuweisen ...

classes = new MyClass[3];

Und löschen Sie es mit delete. Möglicherweise erhalten Sie den Destruktor nur für die erste aufgerufene Instanz von MyClass. Wenn Sie delete [] verwenden, können Sie sicher sein, dass der Destruktor für alle Instanzen im Array aufgerufen wird.

DAS ist der wichtige Unterschied. Wenn Sie einfach mit Standardtypen (z. B. int) arbeiten, wird dieses Problem nicht wirklich angezeigt. Außerdem sollten Sie bedenken, dass das Verhalten für die Verwendung von delete für new [] und delete [] für new undefiniert ist - es funktioniert möglicherweise nicht auf allen Compilern/Systemen gleich.

5
ProdigySim

Einer der Ansätze für Compiler besteht darin, etwas mehr Speicher zuzuweisen und die Anzahl der Elemente im Element head zu speichern.

Beispiel wie es gemacht werden könnte: Hier

int* i = new int[4];

der Compiler weist sizeof (int) * 5 Bytes zu.

int *temp = malloc(sizeof(int)*5)

Speichert 4 In den ersten sizeof(int) Bytes

*temp = 4;

und setze i

i = temp + 1;

Also zeigt i auf ein Array von 4 Elementen, nicht auf 5.

Und

delete[] i;

wird folgendermaßen verarbeitet

int *temp = i - 1;
int numbers_of_element = *temp; // = 4
... call destructor for numbers_of_element elements if needed
... that are stored in temp + 1, temp + 2, ... temp + 4
free (temp)
3
Avt

Es liegt an der Laufzeit, die für die Speicherzuordnung verantwortlich ist, genauso wie Sie ein mit malloc in Standard C erstelltes Array mit free löschen können. Ich denke, jeder Compiler implementiert es anders. Eine gebräuchliche Methode ist die Zuweisung einer zusätzlichen Zelle für die Arraygröße.

Die Laufzeit ist jedoch nicht intelligent genug, um zu erkennen, ob es sich um ein Array oder einen Zeiger handelt. Sie müssen dies mitteilen, und wenn Sie sich irren, löschen Sie entweder nicht richtig (z. B. ptr anstelle von array) oder Sie erhalten einen unabhängigen Wert für die Größe und verursachen erheblichen Schaden.

3
Uri

Semantisch können beide Versionen des Löschoperators in C++ jeden Zeiger "essen"; Wenn jedoch ein Zeiger auf ein einzelnes Objekt auf delete[] gegeben wird, führt dies zu UB, was bedeutet, dass alles passieren kann, einschließlich eines Systemabsturzes oder gar nichts.

In C++ muss der Programmierer abhängig vom Thema der Freigabe die richtige Version des Löschoperators auswählen: Array oder einzelnes Objekt.

Wenn der Compiler automatisch feststellen könnte, ob ein an den Löschoperator übergebener Zeiger ein Zeigerarray ist, gäbe es in C++ nur einen Löschoperator, was für beide Fälle ausreichen würde.

1
mloskot

Stimmen Sie zu, dass der Compiler nicht weiß, ob es sich um ein Array handelt oder nicht. Es liegt an dem Programmierer.

Der Compiler verfolgt manchmal, wie viele Objekte gelöscht werden müssen, indem er genug zuordnet, um die Array-Größe zu speichern, dies ist jedoch nicht immer erforderlich.

Eine vollständige Spezifikation für die Zuordnung von zusätzlichem Speicher finden Sie in C++ ABI (Implementierung von Compilern): Neue Cookies für Itanium C++ ABI: Array Operator

1
shibo

"undefiniertes Verhalten" bedeutet einfach, dass die Sprachspezifikation keine Gewähr dafür leistet, was passieren wird. Es bedeutet nicht unbedingt, dass etwas Schlimmes passieren wird.

Wessen Aufgabe ist es also, sich an diese Dinge zu erinnern? Bewahrt das Betriebssystem eine Art Aufzeichnung im Hintergrund auf? (Ich meine, ich merke, dass ich diesen Beitrag mit der Aussage begonnen habe, dass das, was passiert, undefiniert ist, aber die Tatsache ist, dass das Szenario „Amoklauf“ nicht passiert. In der praktischen Welt erinnert sich also jemand.)

Hier gibt es typischerweise zwei Schichten. Der zugrunde liegende Speichermanager und die C++ - Implementierung.

Im Allgemeinen merkt sich der Speichermanager (unter anderem) die Größe des zugewiesenen Speicherblocks. Dies kann größer sein als der Block, nach dem die C++ - Implementierung gefragt hat. Normalerweise speichert der Speichermanager seine Metadaten vor dem zugewiesenen Speicherblock.

Die C++ - Implementierung merkt sich im Allgemeinen nur die Größe des Arrays, wenn dies für eigene Zwecke erforderlich ist, in der Regel, weil der Typ einen nicht-trivalenten Destruktor hat.

Daher ist die Implementierung von "delete" und "delete []" für Typen mit einem trivialen Destruktor in der Regel dieselbe. Die C++ - Implementierung übergibt den Zeiger einfach an den zugrunde liegenden Speichermanager. So etwas wie

free(p)

Auf der anderen Seite unterscheiden sich "delete" und "delete []" wahrscheinlich bei Typen mit einem nicht trivialen Destruktor. "delete" würde ungefähr so ​​lauten (wobei T der Typ ist, auf den der Zeiger zeigt)

p->~T();
free(p);

Während "Delete []" wäre so etwas wie.

size_t * pcount = ((size_t *)p)-1;
size_t count = *count;
for (size_t i=0;i<count;i++) {
  p[i].~T();
}
char * pmemblock = ((char *)p) - max(sizeof(size_t),alignof(T));
free(pmemblock);
0
plugwash

Sie können delete nicht für ein Array und delete [] nicht für ein Nicht-Array verwenden.

0
ThundrbltMN