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

136

Okay, ich denke, wir sind uns alle einig, dass das, was mit dem folgenden Code passiert, undefiniert ist, je nachdem, was übergeben wird.

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

Der Zeiger kann viele verschiedene Dinge sein, und daher delete[]ist es undefiniert , eine bedingungslose Ausführung durchzufü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 , in dem der Zeiger ist ein Array, wer ist es, das weiß? Ich meine, aus Sicht der Sprache / des Compilers hat es keine Ahnung, ob es sich arrum einen Array-Zeiger oder einen Zeiger auf ein einzelnes Int handelt. Heck, es weiß nicht einmal, ob arrdynamisch 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 einen Int zu löschen und keine Art von "Amoklauf" durchzuführen, indem der Rest des Speichers über diesen Punkt hinaus gelöscht wird (im Gegensatz zu strlenund einer nicht \0terminierten Zeichenfolge - es wird so lange fortgesetzt, bis es passiert Treffer 0).

Wessen Aufgabe ist es also, sich an diese Dinge zu erinnern? Hält das Betriebssystem eine Art Aufzeichnung im Hintergrund? (Ich meine, mir ist klar, dass ich diesen Beitrag damit begonnen habe, dass das, was passiert, undefiniert ist, aber Tatsache ist, dass das Szenario „Amoklauf“ nicht vorkommt, also erinnert sich in der praktischen Welt jemand daran.)

GRB
quelle
6
es weiß aus den eckigen Klammern nach dem Löschen
JoelFan
"Der Zeiger ist ein Array". Nein, Zeiger sind niemals Arrays. Sie zeigen oft auf das erste Element des Arrays, aber das ist etwas anderes.
Aaron McDaid

Antworten:

99

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

Der Compiler muss nachverfolgen, wie viele Objekte irgendwie gelöscht werden müssen. Dies kann geschehen, indem genug zu viel zugewiesen wird, um die Arraygröße zu speichern. Weitere Informationen finden Sie in den C ++ Super-FAQ .

Fred Larson
quelle
14
Tatsächlich ist die Verwendung von delete [] zum Löschen von mit new erstellten Objekten ausnutzbar. taossa.com/index.php/2007/01/03/…
Rodrigo
23
@ Rodrigo Der Link in Ihrem Kommentar ist defekt, aber zum Glück hat die Wayback-Maschine eine Kopie davon unter replay.web.archive.org/20080703153358/http://taossa.com/…
David Gardner
103

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

Die Antwort darauf geht auf die Wurzeln von C ++ als C-kompatible Sprache zurück (nach der es nicht mehr wirklich strebt). Stroustrups Philosophie war, dass der Programmierer nicht für Funktionen bezahlen muss, die er nicht verwendet. Wenn sie keine Arrays verwenden, sollten sie nicht die Kosten für Objekt-Arrays für jeden zugewiesenen Speicherblock tragen müssen.

Das heißt, wenn Ihr Code dies einfach tut

Foo* foo = new Foo;

Dann sollte der zugewiesene Speicherplatz fookeinen zusätzlichen Overhead enthalten, der zur Unterstützung von Arrays von erforderlich wäre Foo.

Da nur Array-Zuordnungen so eingerichtet sind, dass sie die zusätzlichen 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;

Wenn bar ein Zeiger auf ein Array ist.

Für die meisten von uns (ich selbst eingeschlossen) scheint diese Aufregung um ein paar zusätzliche Bytes Speicher heutzutage kurios zu sein. 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.

Dan Breslau
quelle
20
"Die Aufregung um ein paar zusätzliche Bytes Speicher scheint heutzutage kurios zu sein". Glücklicherweise sehen nackte Arrays für solche Leute auch merkwürdig aus, sodass sie einfach einen Vektor oder ein boost :: array verwenden und delete [] für immer vergessen können :-)
Steve Jessop
28

Ja, das Betriebssystem hält einige Dinge im Hintergrund. Zum Beispiel, wenn Sie ausführen

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 Speicherplätze 1000 bis 1024 zu, aber der Zeiger gibt Punkte 1004 mit Positionen 1000 zurück. 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.

bsdfish
quelle
26
+1 - Gültiger Punkt im Allgemeinen, außer dass normalerweise die Sprachlaufzeit für das Speichern dieser Metadaten verantwortlich ist, nicht das Betriebssystem.
Scharfzahn
Was passiert mit der Größe des Arrays oder der Größe eines Objekts, für das das Array definiert ist? Zeigt es die zusätzlichen 4 Bytes an, wenn Sie eine Größe von für dieses Objekt ausführen?
Shree
3
Nein, sizeof zeigt nur die Größe des Arrays an. Wenn sich die Laufzeit für die Implementierung mit der von mir beschriebenen Methode entscheidet, handelt es sich ausschließlich um ein Implementierungsdetail, das aus Anwendersicht maskiert werden sollte. Der Speicher vor dem Zeiger gehört nicht dem Benutzer und wird nicht gezählt.
bsdfish
2
Noch wichtiger ist, dass sizeof in keinem Fall die wahre Größe eines dynamisch zugewiesenen Arrays zurückgibt. Es können nur Größen zurückgegeben werden, die zur Kompilierungszeit bekannt sind.
Bdonlan
Ist es möglich, diese Metadaten in einer for-Schleife zu verwenden, um das Array genau zu durchlaufen? zB for(int i = 0; i < *(arrayPointer - 1); i++){ }
Sam
13

Dies ist dieser Frage sehr ähnlich und enthält viele der Details, nach denen Sie suchen.

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

Dies kann bei einigen Implementierungen angezeigt werden, indem der folgende Code ausgeführt wird

int* pArray = new int[5];
int size = *(pArray-1);
JaredPar
quelle
Ob das funktioniert? In Windows & Linux haben wir das nicht zum Laufen gebracht.
Kumpel
1
versuchen Sie es size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)stattdessen
9

deleteoder delete[]würde wahrscheinlich beide den zugewiesenen Speicher freigeben (Speicher zeigt), aber der große Unterschied besteht darin, dass deleteauf einem Array nicht der Destruktor jedes Elements des Arrays aufgerufen wird.

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

Benoît
quelle
1
Klar, kurz und die nützlichste Antwort!
GntS
6

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

nervös
quelle
5

Ich hatte eine ähnliche Frage dazu. In C weisen 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 eine freie (), die einfach einen Zeiger als Parameter verwendet.

Warum können Sie in C den Zeiger einfach an free übergeben, aber in C ++ müssen Sie angeben, ob es sich um ein Array oder eine einzelne Variable handelt?

Ich habe gelernt, dass die Antwort mit Klassendestruktoren zu tun hat.

Wenn Sie eine Instanz einer Klasse MyClass zuweisen ...

classes = new MyClass[3];

Wenn Sie es mit delete löschen, erhalten Sie möglicherweise nur den Destruktor für die erste aufgerufene MyClass-Instanz. 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 auftreten. Außerdem sollten Sie sich daran erinnern, dass das Verhalten für die Verwendung von delete bei new [] und delete [] bei new undefiniert ist - es funktioniert möglicherweise nicht auf jedem Compiler / System auf die gleiche Weise.

ProdigySim
quelle
3

Es liegt an der Laufzeit, die für die Speicherzuweisung 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 übliche Methode besteht darin, eine zusätzliche Zelle für die Arraygröße zuzuweisen.

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. Wenn Sie sich irren, löschen Sie entweder nicht richtig (z. B. ptr anstelle von Array) oder Am Ende nehmen Sie einen nicht verwandten Wert für die Größe und verursachen erheblichen Schaden.

Uri
quelle
3

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

Beispiel, wie es gemacht werden könnte: Hier

int* i = new int[4];

Der Compiler weist eine Größe von (int) * 5 Bytes zu.

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

Wird 4in ersten sizeof(int)Bytes gespeichert

*temp = 4;

und setzen i

i = temp + 1;

Zeigt also iauf 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)
Avt
quelle
1

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

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

Wenn der Compiler automatisch feststellen könnte, ob ein an den Löschoperator übergebener Zeiger ein Zeigerarray ist, gibt es in C ++ nur einen Löschoperator, was in beiden Fällen ausreichen würde.

Mloskot
quelle
1

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

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

Eine vollständige Spezifikation für die Zuweisung von zusätzlichem Speicher finden Sie in C ++ ABI (wie Compiler implementiert werden): Itanium C ++ ABI: Neue Cookies für den Array-Operator

Shibo
quelle
Ich wünschte nur, jeder Compiler hätte ein dokumentiertes ABI für C ++ beobachtet. +1 für den Link, den ich zuvor besucht habe. Vielen Dank.
Don Wakefield
0

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

Don Wakefield
quelle
8
Ich denke, Sie meinen, sollte nicht, da Ihr durchschnittlicher Compiler den Missbrauch nicht erkennen wird.
Don Wakefield
0

"undefiniertes Verhalten" bedeutet einfach, dass die Sprachspezifikation keine Garantie dafür gibt, was passieren wird. Es bedeutet nicht unbedingt, dass etwas Schlimmes passieren wird.

Wessen Aufgabe ist es also, sich an diese Dinge zu erinnern? Hält das Betriebssystem eine Art Aufzeichnung im Hintergrund? (Ich meine, mir ist klar, dass ich diesen Beitrag damit begonnen habe, dass das, was passiert, undefiniert ist, aber Tatsache ist, dass das Szenario „Amoklauf“ nicht vorkommt, also erinnert sich in der praktischen Welt jemand daran.)

Hier gibt es normalerweise 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, den die C ++ - Implementierung angefordert hat. Normalerweise speichert der Speichermanager seine Metadaten vor dem zugewiesenen Speicherblock.

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

Für Typen mit einem trivialen Destruktor ist die Implementierung von "delete" und "delete []" normalerweise dieselbe. Die C ++ - Implementierung übergibt den Zeiger einfach an den zugrunde liegenden Speichermanager. Etwas wie

free(p)

Andererseits sind bei Typen mit einem nicht trivialen Destruktor "delete" und "delete []" wahrscheinlich unterschiedlich. "Löschen" wäre so etwas wie (wobei T der Typ ist, auf den der Zeiger zeigt)

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

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

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);
Plugwash
quelle
-1

Durchlaufen Sie ein Array von Objekten und rufen Sie den Destruktor für jedes Objekt auf. Ich habe diesen einfachen Code erstellt, der neue [] und delete [] Ausdrücke überlädt und eine Vorlagenfunktion bereitstellt, mit der Speicher freigegeben und bei Bedarf für jedes Objekt Destruktor aufgerufen werden kann:

// overloaded new expression 
void* operator new[]( size_t size )
{
    // allocate 4 bytes more see comment below 
    int* ptr = (int*)malloc( size + 4 );

    // set value stored at address to 0 
    // and shift pointer by 4 bytes to avoid situation that
    // might arise where two memory blocks 
    // are adjacent and non-zero
    *ptr = 0;
    ++ptr; 

    return ptr;
}
//////////////////////////////////////////

// overloaded delete expression 
void static operator delete[]( void* ptr )
{
    // decrement value of pointer to get the
    // "Real Pointer Value"
    int* realPtr = (int*)ptr;
    --realPtr;

    free( realPtr );
}
//////////////////////////////////////////

// Template used to call destructor if needed 
// and call appropriate delete 
template<class T>
void Deallocate( T* ptr )
{
    int* instanceCount = (int*)ptr;
    --instanceCount;

    if(*instanceCount > 0) // if larger than 0 array is being deleted
    {
        // call destructor for each object
        for(int i = 0; i < *instanceCount; i++)
        {
            ptr[i].~T();
        }
        // call delete passing instance count witch points
        // to begin of array memory 
        ::operator delete[]( instanceCount );
    }
    else
    {
        // single instance deleted call destructor
        // and delete passing ptr
        ptr->~T();
        ::operator delete[]( ptr );
    }
}

// Replace calls to new and delete
#define MyNew ::new
#define MyDelete(ptr) Deallocate(ptr)

// structure with constructor/ destructor
struct StructureOne
{
    StructureOne():
    someInt(0)
    {}
    ~StructureOne() 
    {
        someInt = 0;
    }

    int someInt;
};
//////////////////////////////

// structure without constructor/ destructor
struct StructureTwo
{
    int someInt;
};
//////////////////////////////


void main(void)
{
    const unsigned int numElements = 30;

    StructureOne* structOne = nullptr;
    StructureTwo* structTwo = nullptr;
    int* basicType = nullptr;
    size_t ArraySize = 0;

/**********************************************************************/
    // basic type array 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( int ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor. value assigned to basicType pointer
    // will be the same as value of "++ptr" in new expression
    basicType = MyNew int[numElements];

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( int ) * numElements"
    MyDelete( basicType );

/**********************************************************************/
    // structure without constructor and destructor array 

    // behavior will be the same as with basic type 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( StructureTwo ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor value assigned to structTwo pointer
    // will be the same as value of "++ptr" in new expression
    structTwo = MyNew StructureTwo[numElements]; 

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( StructureTwo ) * numElements"
    MyDelete( structTwo );

/**********************************************************************/
    // structure with constructor and destructor array 

    // place break point check size and compare it with size passed in
    // new expression size in expression will be larger by 4 bytes
    ArraySize = sizeof( StructureOne ) * numElements;

    // value assigned to "structOne pointer" will be different 
    // of "++ptr" in new expression  "shifted by another 4 bytes"
    structOne = MyNew StructureOne[numElements];

    // Place break point in template function to see the behavior
    // destructors will be called for each array object 
    MyDelete( structOne );
}
///////////////////////////////////////////
Rafal Rebisz
quelle
-2

Definieren Sie einfach einen Destruktor innerhalb einer Klasse und führen Sie Ihren Code mit beiden Syntaxen aus

delete pointer

delete [] pointer

Je nach Ausgabe können Sie die Lösungen finden

bubu
quelle
Verwenden Sie delete [], wenn Sie einen neuen Array-Typ erstellen. zum Beispiel int * a = new int; int * b = new int [5]; lösche a; löschen [] b;
Lineesh K Mohan
-3

Die Antwort:

int * pArray = new int [5];

int size = * (pArray-1);

Der oben angegebene Wert ist nicht korrekt und führt zu einem ungültigen Wert. Die "-1" zählt Elemente Unter 64-Bit-Windows-Betriebssystemen befindet sich die richtige Puffergröße in der Ptr-4-Byte-Adresse

Evgeni Raikhel
quelle