Ist es sicher, einen ungültigen Zeiger zu löschen?


96

Angenommen, ich habe den folgenden Code:

void* my_alloc (size_t size)
{
   return new char [size];
}

void my_free (void* ptr)
{
   delete [] ptr;
}

Ist das sicher? Oder muss vor dem Löschen ptrgegossen werden char*?


Warum führen Sie die Speicherverwaltung selbst durch? Welche Datenstruktur erstellen Sie? Die Notwendigkeit einer expliziten Speicherverwaltung ist in C ++ ziemlich selten. Normalerweise sollten Sie Klassen verwenden, die dies für Sie aus der STL (oder zur Not aus Boost) handhaben.
Brian

6
Nur zum Lesen verwende ich void * -Variablen als Parameter für meine Threads in Win C ++ (siehe _beginthreadex). Normalerweise zeigen sie akut auf Klassen.
Ryansstack

3
In diesem Fall handelt es sich um einen Allzweck-Wrapper für Neu / Löschen, der Zuordnungsverfolgungsstatistiken oder einen optimierten Speicherpool enthalten kann. In anderen Fällen wurden Objektzeiger falsch als void * -Mitgliedvariablen gespeichert und im Destruktor falsch gelöscht, ohne auf den entsprechenden Objekttyp zurückzugreifen. Also war ich neugierig auf die Sicherheit / Fallstricke.
Andrew

1
Für einen Allzweck-Wrapper für new / delete können Sie die new / delete-Operatoren überladen. Je nachdem, welche Umgebung Sie verwenden, erhalten Sie wahrscheinlich Hooks in die Speicherverwaltung, um Zuordnungen zu verfolgen. Wenn Sie in eine Situation geraten, in der Sie nicht wissen, was Sie löschen, nehmen Sie dies als starken Hinweis darauf, dass Ihr Design nicht optimal ist und überarbeitet werden muss.
Hans

38
Ich denke, es gibt zu viele Fragen, anstatt sie zu beantworten. (Nicht nur hier, sondern in allen SO)
Petruza

Antworten:


24

Es kommt auf "sicher" an. Dies funktioniert normalerweise, da Informationen zusammen mit dem Zeiger auf die Zuordnung selbst gespeichert werden, sodass der Deallocator sie an die richtige Stelle zurückgeben kann. In diesem Sinne ist es "sicher", solange Ihr Allokator interne Grenz-Tags verwendet. (Viele tun es.)

Wie in anderen Antworten erwähnt, werden beim Löschen eines ungültigen Zeigers keine Destruktoren aufgerufen, was ein Problem sein kann. In diesem Sinne ist es nicht "sicher".

Es gibt keinen guten Grund, das, was Sie tun, so zu tun, wie Sie es tun. Wenn Sie Ihre eigenen Freigabefunktionen schreiben möchten, können Sie Funktionsvorlagen verwenden, um Funktionen mit dem richtigen Typ zu generieren. Ein guter Grund dafür ist die Generierung von Pool-Allokatoren, die für bestimmte Typen äußerst effizient sein können.

Wie in anderen Antworten erwähnt, ist dies ein undefiniertes Verhalten in C ++. Im Allgemeinen ist es gut, undefiniertes Verhalten zu vermeiden, obwohl das Thema selbst komplex und voller widersprüchlicher Meinungen ist.


9
Wie ist das eine akzeptierte Antwort? Es macht keinen Sinn, "einen leeren Zeiger zu löschen" - Sicherheit ist ein strittiger Punkt.
Kerrek SB

6
"Es gibt keinen guten Grund, das zu tun, was Sie tun, wie Sie es tun." Das ist deine Meinung, keine Tatsache.
Rxantos

8
@rxantos Geben Sie ein Gegenbeispiel an, bei dem es in C ++ eine gute Idee ist, das zu tun, was der Autor der Frage tun möchte.
Christopher

Ich glaube , diese Antwort tatsächlich meist sinnvoll ist, aber ich denke auch , dass jede Antwort auf diese Frage Bedürfnisse zumindest zu erwähnen , dass dies nicht definiertes Verhalten ist.
Kyle Strand

@Christopher Versuchen Sie, ein einzelnes Garbage Collector-System zu schreiben, das nicht typspezifisch ist, sondern einfach funktioniert. Die Tatsache, dass sizeof(T*) == sizeof(U*)für alle T,Udarauf hindeutet, dass es möglich sein sollte, eine nicht vorlagenbasierte, void *auf Garbage Collector basierende Implementierung zu haben. Aber dann, wenn der GC tatsächlich einen Zeiger löschen / freigeben muss, stellt sich genau diese Frage. Damit es funktioniert, benötigen Sie entweder Lambda-Funktions-Destruktor-Wrapper (urgh) oder eine Art dynamisches "Typ als Daten" -Ding, das ein Hin- und Herwechseln zwischen einem Typ und etwas Speicherbarem ermöglicht.
BitTickler

147

Das Löschen über einen ungültigen Zeiger ist im C ++ - Standard nicht definiert - siehe Abschnitt 5.3.5 / 3:

Wenn sich der statische Typ des Operanden von seinem dynamischen Typ unterscheidet, muss der statische Typ in der ersten Alternative (Objekt löschen) eine Basisklasse des dynamischen Typs des Operanden sein und der statische Typ muss einen virtuellen Destruktor haben oder das Verhalten ist undefiniert . Bei der zweiten Alternative (Array löschen) ist das Verhalten undefiniert, wenn sich der dynamische Typ des zu löschenden Objekts von seinem statischen Typ unterscheidet.

Und seine Fußnote:

Dies bedeutet, dass ein Objekt nicht mit einem Zeiger vom Typ void * gelöscht werden kann, da keine Objekte vom Typ void vorhanden sind

.


6
Sind Sie sicher, dass Sie das richtige Zitat gefunden haben? Ich denke, die Fußnote bezieht sich auf diesen Text: "Wenn sich der statische Typ des Operanden von seinem dynamischen Typ unterscheidet, soll der statische Typ in der ersten Alternative (Objekt löschen) eine Basisklasse des dynamischen Typs des Operanden und des statischen sein Der Typ muss einen virtuellen Destruktor haben oder das Verhalten ist undefiniert. Bei der zweiten Alternative (Array löschen) ist das Verhalten undefiniert, wenn der dynamische Typ des zu löschenden Objekts von seinem statischen Typ abweicht. " :)
Johannes Schaub - litb

2
Sie haben Recht - ich habe die Antwort aktualisiert. Ich denke nicht, dass es den grundlegenden Punkt negiert?

Nein natürlich nicht. Es heißt immer noch, es ist UB. Noch mehr, jetzt heißt es normativ, dass das Löschen von void * UB ist :)
Johannes Schaub - litb

Füllen Sie den spitzen Adressspeicher eines ungültigen Zeigers mit NULLeinem Unterschied für die Verwaltung des Anwendungsspeichers.
artu-hnrq

25

Es ist keine gute Idee und nichts, was Sie in C ++ tun würden. Sie verlieren Ihre Typinformationen ohne Grund.

Ihr Destruktor wird nicht für die Objekte in Ihrem Array aufgerufen, die Sie löschen, wenn Sie ihn für nicht primitive Typen aufrufen.

Sie sollten stattdessen new / delete überschreiben.

Durch das Löschen der Leere * wird Ihr Speicher wahrscheinlich zufällig korrekt freigegeben, aber es ist falsch, da die Ergebnisse undefiniert sind.

Wenn Sie aus einem mir unbekannten Grund Ihren Zeiger in einer Leere * speichern müssen, um ihn freizugeben, sollten Sie malloc und free verwenden.


5
Sie haben Recht damit, dass der Destruktor nicht aufgerufen wird, aber falsch, wenn die Größe unbekannt ist. Wenn Sie delete einen Zeiger geben, den Sie von new erhalten haben, kennt er tatsächlich die Größe des zu löschenden Objekts, ganz abgesehen vom Typ. Wie es funktioniert, ist nicht im C ++ - Standard festgelegt, aber ich habe Implementierungen gesehen, bei denen die Größe unmittelbar vor den Daten gespeichert wird, auf die der von 'new' zurückgegebene Zeiger zeigt.
KeyserSoze

Der Teil über die Größe wurde entfernt, obwohl der C ++ - Standard angibt, dass er undefiniert ist. Ich weiß, dass malloc / free für void * -Pointer funktionieren würde.
Brian R. Bondy

Nehmen Sie nicht an, Sie haben einen Weblink zum entsprechenden Abschnitt des Standards? Ich weiß, dass die wenigen Implementierungen von new / delete, die ich mir angesehen habe, ohne Typkenntnisse definitiv korrekt funktionieren, aber ich gebe zu, dass ich mir nicht angesehen habe, was der Standard spezifiziert. IIRC C ++ erforderte ursprünglich eine Array-Elementanzahl beim Löschen von Arrays, funktioniert jedoch nicht mehr mit den neuesten Versionen.
KeyserSoze

Bitte siehe @Neil Butterworth Antwort. Seine Antwort sollte meiner Meinung nach die akzeptierte sein.
Brian R. Bondy

2
@keysersoze: Im Allgemeinen stimme ich Ihrer Aussage nicht zu. Nur weil eine Implementierung die Größe vor dem zugewiesenen Speicher gespeichert hat, bedeutet dies nicht, dass dies eine Regel ist.

13

Das Löschen eines ungültigen Zeigers ist gefährlich, da Destruktoren nicht für den Wert aufgerufen werden, auf den er tatsächlich zeigt. Dies kann zu Speicher- / Ressourcenlecks in Ihrer Anwendung führen.


1
char hat keinen Konstruktor / Destruktor.
Rxantos

9

Die Frage macht keinen Sinn. Ihre Verwirrung kann teilweise auf die schlampige Sprache zurückzuführen sein, mit der Menschen häufig sprechendelete :

Sie verwenden delete, um ein Objekt zu zerstören , das dynamisch zugewiesen wurde. Dazu bilden Sie einen Löschausdruck mit einem Zeiger auf dieses Objekt . Sie "löschen niemals einen Zeiger". Was Sie wirklich tun, ist "ein Objekt löschen, das durch seine Adresse identifiziert wird".

Jetzt sehen wir, warum die Frage keinen Sinn ergibt: Ein ungültiger Zeiger ist nicht die "Adresse eines Objekts". Es ist nur eine Adresse ohne Semantik. Es kann von der Adresse eines tatsächlichen Objekts stammen, aber diese Informationen gehen verloren, weil sie im Typ des ursprünglichen Zeigers codiert wurden . Die einzige Möglichkeit, einen Objektzeiger wiederherzustellen, besteht darin, den leeren Zeiger auf einen Objektzeiger zurückzusetzen (für den der Autor wissen muss, was der Zeiger bedeutet). voidselbst ist ein unvollständiger Typ und daher niemals der Typ eines Objekts, und ein ungültiger Zeiger kann niemals zur Identifizierung eines Objekts verwendet werden. (Objekte werden gemeinsam anhand ihres Typs und ihrer Adresse identifiziert.)


Zugegeben, die Frage macht ohne umgebenden Kontext wenig Sinn. Einige C ++ - Compiler kompilieren diesen unsinnigen Code immer noch gerne (wenn sie sich hilfreich fühlen, können sie eine Warnung darüber auslösen). Daher wurde die Frage gestellt, um die bekannten Risiken der Ausführung von Legacy-Code zu bewerten, der diese schlecht beratene Operation enthält: Wird sie abstürzen? einen Teil oder den gesamten Speicher des Zeichenarrays verlieren? etwas anderes, das plattformspezifisch ist?
Andrew

Danke für die nachdenkliche Antwort. Upvoting!
Andrew

1
@ Andrew: Ich fürchte, der Standard ist diesbezüglich ziemlich klar: "Der Wert des Operanden von deletekann ein Nullzeigerwert, ein Zeiger auf ein Nicht-Array-Objekt sein, das durch einen vorherigen neuen Ausdruck erstellt wurde , oder ein Zeiger auf a Unterobjekt, das eine Basisklasse eines solchen Objekts darstellt. Wenn nicht, ist das Verhalten undefiniert. " Wenn also ein Compiler Ihren Code ohne Diagnose akzeptiert, ist dies nichts anderes als ein Fehler im Compiler ...
Kerrek SB

1
@ KerrekSB - Re es ist nichts als ein Fehler im Compiler - Ich bin anderer Meinung. Der Standard sagt, dass das Verhalten undefiniert ist. Dies bedeutet, dass der Compiler / die Implementierung alles tun kann und dennoch dem Standard entspricht. Wenn der Compiler sagt, dass Sie einen void * -Zeiger nicht löschen können, ist das in Ordnung. Wenn der Compiler darauf reagiert, die Festplatte zu löschen, ist dies ebenfalls in Ordnung. OTOH: Wenn der Compiler keine Diagnose generiert, sondern Code generiert, der den mit diesem Zeiger verknüpften Speicher freigibt, ist dies ebenfalls in Ordnung. Dies ist eine einfache Möglichkeit, mit dieser Form von UB umzugehen.
David Hammen

Nur um hinzuzufügen: Ich dulde die Verwendung von nicht delete void_pointer. Es ist undefiniertes Verhalten. Programmierer sollten niemals undefiniertes Verhalten aufrufen, selbst wenn die Antwort das zu tun scheint, was der Programmierer wollte.
David Hammen

6

Wenn Sie dies wirklich tun müssen, warum nicht den Mittelsmann (die newund deleteOperatoren) ausschneiden und den globalen operator newund operator deletedirekten Anruf tätigen ? (Natürlich, wenn Sie zum Instrumente sind zu versuchen , die newund deleteBetreiber, sollten Sie tatsächlich neu zu implementieren operator newund operator delete.)

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

void my_free (void* ptr)
{
   ::operator delete(ptr);
}

Beachten Sie, dass im Gegensatz zu malloc(), operator newwirft std::bad_allocauf Fehler (oder ruft die, new_handlerwenn eine registriert ist).


Dies ist korrekt, da char keinen Konstruktor / Destruktor hat.
Rxantos

5

Weil char keine spezielle Destruktorlogik hat. DAS wird nicht funktionieren.

class foo
{
   ~foo() { printf("huzza"); }
}

main()
{
   foo * myFoo = new foo();
   delete ((void*)foo);
}

Der d'ctor wird nicht gerufen.


5

Wenn Sie void * verwenden möchten, warum verwenden Sie nicht einfach malloc / free? Neu / Löschen ist mehr als nur Speicherverwaltung. Grundsätzlich ruft new / delete einen Konstruktor / Destruktor auf und es sind noch weitere Dinge im Gange. Wenn Sie nur integrierte Typen (wie char *) verwenden und diese durch void * löschen, funktioniert dies, wird jedoch nicht empfohlen. Unter dem Strich verwenden Sie malloc / free, wenn Sie void * verwenden möchten. Andernfalls können Sie Vorlagenfunktionen verwenden.

template<typename T>
T* my_alloc (size_t size)
{
   return new T [size];
}

template<typename T>
void my_free (T* ptr)
{
   delete [] ptr;
}

int main(void)
{
    char* pChar = my_alloc<char>(10);
    my_free(pChar);
}

Ich habe den Code im Beispiel nicht geschrieben - bin auf ein Muster gestoßen, das an einigen Stellen verwendet wurde, das die C / C ++ - Speicherverwaltung neugierig mischte, und habe mich gefragt, was die spezifischen Gefahren sind.
Andrew

Das Schreiben von C / C ++ ist ein Rezept für einen Fehler. Wer das geschrieben hat, sollte das eine oder andere geschrieben haben.
David Thornley

2
@ David Das ist C ++, nicht C / C ++. C verfügt weder über Vorlagen noch verwendet es new und delete.
Rxantos

5

Viele Leute haben bereits kommentiert, dass es nicht sicher ist, einen ungültigen Zeiger zu löschen. Ich stimme dem zu, aber ich wollte auch hinzufügen, dass Sie, wenn Sie mit leeren Zeigern arbeiten, um zusammenhängende Arrays oder ähnliches zuzuweisen, dies tun können, newdamit Sie deletesicher (mit, ahem) verwenden können ein wenig zusätzliche Arbeit). Dies erfolgt durch Zuweisen eines leeren Zeigers zum Speicherbereich (als "Arena" bezeichnet) und anschließendes Bereitstellen des Zeigers zur Arena für einen neuen. Siehe diesen Abschnitt in den C ++ - FAQ . Dies ist ein gängiger Ansatz zur Implementierung von Speicherpools in C ++.


0

Es gibt kaum einen Grund dafür.

Wenn Sie den Typ der Daten nicht kennen und nur wissen, dass dies der void*Fall ist, sollten Sie diese Daten zunächst nur als typenlosen Blob aus Binärdaten ( unsigned char*) behandeln und malloc/ verwenden, freeum damit umzugehen . Dies ist manchmal für Dinge wie Wellenformdaten und dergleichen erforderlich, bei denen Sie void*Zeiger auf C apis weitergeben müssen. Das ist gut.

Wenn Sie zu tun , die Art der Daten wissen (dh es hat eine Ctor / dtor), aber aus irgendeinem Grund beendet man mit einem up - void*Zeiger (aus welchem Grund Sie haben) , dann sollten Sie es wirklich zurückgeworfen auf die Art wissen Sie es sei und rufe deletees an.


0

Ich habe void * (auch bekannt als unbekannte Typen) in meinem Framework für die Zeit der Code-Reflektion und anderer mehrdeutiger Heldentaten verwendet. Bisher hatte ich keine Probleme (Speicherverlust, Zugriffsverletzungen usw.) von Compilern. Nur Warnungen, da der Vorgang nicht dem Standard entspricht.

Es ist durchaus sinnvoll, ein Unbekanntes zu löschen (void *). Stellen Sie einfach sicher, dass der Zeiger diesen Richtlinien entspricht, da er sonst möglicherweise keinen Sinn mehr ergibt:

1) Der unbekannte Zeiger darf nicht auf einen Typ zeigen, der einen trivialen Dekonstruktor hat. Wenn er als unbekannter Zeiger umgewandelt wird, sollte er NIEMALS GELÖSCHT werden. Löschen Sie den unbekannten Zeiger erst, nachdem Sie ihn wieder in den ORIGINAL-Typ umgewandelt haben.

2) Wird die Instanz als unbekannter Zeiger im Stapel- oder Heap-gebundenen Speicher referenziert? Wenn der unbekannte Zeiger auf eine Instanz auf dem Stapel verweist, sollte er NIEMALS GELÖSCHT werden!

3) Sind Sie zu 100% sicher, dass der unbekannte Zeiger ein gültiger Speicherbereich ist? Nein, dann sollte es NIEMALS GELÖSCHT werden!

Insgesamt kann mit einem unbekannten (void *) Zeigertyp nur sehr wenig direkte Arbeit geleistet werden. Indirekt ist die Leere * jedoch ein großer Vorteil für C ++ - Entwickler, auf den sie sich verlassen können, wenn Datenmehrdeutigkeiten erforderlich sind.


0

Wenn Sie nur einen Puffer möchten, verwenden Sie malloc / free. Wenn Sie new / delete verwenden müssen, ziehen Sie eine triviale Wrapper-Klasse in Betracht:

template<int size_ > struct size_buffer { 
  char data_[ size_]; 
  operator void*() { return (void*)&data_; }
};

typedef sized_buffer<100> OpaqueBuffer; // logical description of your sized buffer

OpaqueBuffer* ptr = new OpaqueBuffer();

delete ptr;

0

Für den speziellen Fall von char.

char ist ein intrinsischer Typ, der keinen speziellen Destruktor hat. Die undichten Argumente sind also umstritten.

sizeof (char) ist normalerweise eins, daher gibt es auch kein Ausrichtungsargument. Bei seltenen Plattformen, bei denen die Größe von (char) nicht eins ist, weisen sie ausreichend Speicher für ihr char zu. Das Ausrichtungsargument ist also auch umstritten.

malloc / free wäre in diesem Fall schneller. Aber Sie verlieren std :: bad_alloc und müssen das Ergebnis von malloc überprüfen. Das Aufrufen der globalen Operatoren new und delete ist möglicherweise besser, da es den Middle Man umgeht.


1
" sizeof (char) ist normalerweise eins " sizeof (char) ist IMMER eins
neugieriger Kerl

Erst vor kurzem (2019) glauben die Leute, dass newdas Werfen tatsächlich definiert ist. Das ist nicht wahr. Es ist abhängig von Compiler und Compiler-Switch. Siehe zum Beispiel MSVC2019- /GX[-] enable C++ EH (same as /EHsc)Switches. Auch auf eingebetteten Systemen zahlen viele die Leistungssteuern für C ++ - Ausnahmen nicht. Der Satz, der mit "Aber Sie verlieren std :: bad_alloc ..." beginnt, ist fraglich.
BitTickler
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.