Operatorüberladung new, delete - falsche Adressen?
-
So, ich habe jetzt die ultimative Version geschrieben, die für alle Fälle funktioniert.
Wenn du delete[] auf ein durch ein nicht-[] new erstellten (POD oder nicht) Zeiger aufrufst, dann wird das einzige Objekt korrekt zerstört (falls halt nicht POD) und anschließend auch noch ein Fehler ausgegeben! Wenn du delete auf einen mit new[] erstellten POD aufrufst, dann funzt es, wenn es ein nicht-POD war, dann wird halt nur ein Objekt zerstört (das erste im Array), aber immerhin noch eine Fehlermeldung ausgegeben.Grüße,
Michaelconst int noarray = 5; const int isarray = 11; void* operator new(size_t Size) { unsigned int* p = (unsigned int*)malloc(Size+12); p[0] = noarray; p[1] = noarray | 0xF00; p[2] = 1; std::cerr << "new " << p << " size: " << Size << " returned: " << p+3 << std::endl; return reinterpret_cast<void*>(p+3); } void* operator new[](size_t Size) { unsigned int* p = (unsigned int*)malloc(Size+12); p[0] = isarray; p[1] = isarray | 0xF00; p[2] = 1; std::cerr << "new[] " << p << " size: " << Size << " returned: " << p+3 << std::endl; return reinterpret_cast<void*>(p+3); } void operator delete(void* p) { unsigned int* p_ = reinterpret_cast<unsigned int*>(p); p_-=3; if( (*p_&0xFF) == isarray ) { std::cerr << "ERRROORRRR!!!" << std::endl; } if( *p_&0xF00 ) p_-=1; std::cerr << "delete called with " << p << " freeing: " << p_ << std::endl; free(reinterpret_cast<void*>(p_)); } void operator delete[](void* p) { unsigned int* p_ = reinterpret_cast<unsigned int*>(p); p_-=2; if( (*p_&0xFF) == noarray ) { std::cerr << "ERRROORRRR!!!" << std::endl; } if( *p_ & 0xF00 ) p_ -= 1; std::cerr << "delete[] called with " << p << " freeing: " << p_ << std::endl; free(reinterpret_cast<void*>(p_)); }
-
Danke für deine Bemühungen.
Ich hätte aber noch einige Fragen zu deinem Code: Was machst du genau mit der binären Logik (
operator&undoperator|)? Und die 5 und 11 sind einfach willkürliche Zahlen? Und was ist mit den Hexadezimalwerten? Auch sonst versteh ich noch nicht ganz alles, ich versuchs mal zu interpretieren.Allerdings scheint mir das sehr viel Aufwand... Ist das
reinterpret_casten hier eigentlich überall einem definierten Verhalten? Man müsste einfach statt4sizeof(int)einsetzen, dann sollte es auch portabel sein... Oder man machts gleich mitchar.
-
Die 5 und 11 sind völlig willkürlich gewählt (aber der folgende Code geht davon aus, dass sie unterschiedlich und kleiner als 256 sind).
Die binäre Logik dient nur dazu, dass ich mir sparen konnte noch zusätzlich zwei konstanten zu definieren. Sie dient hier dazu, zu unterscheiden, ob man beim ersten oder zweiten Typfeld ist. Ja, in den mallocs sollte man 3*sizeof(unsigned int) benutzen anstatt 12. Das reinterpret_cast ist hier im Prinzip das gleiche, wie wenn du das Ergebnis von malloc (void*) in einen bestimmten Zeigertyp umwandelst. Der Code an sich ist definiert. Aber er funktioniert natürlich nur unter der Annahme, das der Compiler seine Verwaltung von dynamischen Feldern eines nicht-POD's so handhabt, wie du es beobachtet hast.
-
Decimad schrieb:
Die binäre Logik dient nur dazu, dass ich mir sparen konnte noch zusätzlich zwei konstanten zu definieren. Sie dient hier dazu, zu unterscheiden, ob man beim ersten oder zweiten Typfeld ist.
Ich hab mir eben auch überlegt, das mit 3
chars zu machen - der Wertebereich würde ja längstens reichen. Aber wieso unterteilst du die einzelnenints noch, wenn du sowieso drei belegst?Decimad schrieb:
Aber er funktioniert natürlich nur unter der Annahme, das der Compiler seine Verwaltung von dynamischen Feldern eines nicht-POD's so handhabt, wie du es beobachtet hast.
Hm, ein guter Einwand. Weiss diesbezüglich jemand mehr?
Schlussendlich scheint es mir ein bisschen viel Aufwand zu sein (wovon Teile möglicherweise gar nicht immer definiertes Verhalten sind) - ansonsten lass ich das einfach meine andere Abfrage erledigen (ob der Zeiger, der freigegeben wurde, gültig ist). Dann erhält man zwar eine weniger spezifische Fehlermeldung, aber da die Verwechslung von
deleteunddelete[]wohl nicht sehr häufig vorkommt...
Naja, vorläufig werde ich es trotzdem mit deinem Ansatz versuchen und schauen, ob er sich bewährt. Vielen Dank nochmals.

-
Wie meinst du das mit dem Unterteilen?
Bezüglich des gewählten Typs. Die 1 muss meiner Meinung nach sowieso in einem int stehen (halt weil delete[] darauf baut), drum machts doch nur mehr Aufwand, den Rest dann noch in char's zu packen.
-
Decimad schrieb:
Wie meinst du das mit dem Unterteilen?
Ich verstehe deinen Ansatz grundsätzlich nicht ganz.
Wenn Speicher allokiert wird, forderst du doch zusätzlich noch Speicher für 3
ints vor dem "normalen" Speicherbereich an. Dann überprüfst du beim Freigeben, ob an den Stellen der vorher allokiertenint-Speicherbereiche das Richtige steht (5 beinew, 11 beinew[]).Jetzt sagst du, du brauchst die binäre Logik, um herauszufinden, in welchem Typfeld du bist. Das verstehe ich nicht ganz...
Decimad schrieb:
Bezüglich des gewählten Typs. Die 1 muss meiner Meinung nach sowieso in einem int stehen (halt weil delete[] darauf baut), drum machts doch nur mehr Aufwand, den Rest dann noch in char's zu packen.
Sorry, welche Funktion hat die 1 in der dritten 4-Byte-Speicherzelle? Einfach als Puffer, da
delete[]bei Klassentypen beginnt, vor dem tatsächlichen Speicherbereich zu löschen?
-
Die 1 dient dazu, dass delete[] nicht wild rumzerstört, wenn du ihm ein durch new erstellten Zeiger übergibst (Ansonsten kämst du oftmals gar nicht mehr in deinen selbstdefinierten operator delete[], weil das zerstören schon Speicherzugriffsverletzungen erzeugt hat).
Ich brauche 2 Typfelder, weil ich ja nicht weiß, welchen Zeiger ich in operator delete[] oder delete bekomme (Der Zeiger liegt ja je nach auftretendem Fall 4 byte davor oder nicht). In beiden steht im unteren byte der Typ des Blocks und im zweiten der beiden Typfelder steht zudem im 2. Byte noch, dass es das 2. Byte ist, damit man sicher einen Zeiger für free() basteln kann.
-
Okay, ich habe nun einmal einen eigenen Ansatz versucht, bei dem man mit weniger auskommt. Habe ich etwas Wichtiges vergessen? Denn die Fehlererkennung funktioniert so.
Ich allokiere jetzt nur zwei
int-Felder vor dem eigentlichen Speicherbereich, auf die ich in den Freigabefunktionen mitptr[-2]undptr[-1]bei den Freigabefunktionen zugreife. Da immer zuerst aufptr[-1]geprüft wird (und dieser Speicherbereich in jedem Falle reserviert wurde), sollte der Zugriff auf Speicher, der einem nicht gehört, vermieden werden.const int NoArray = 42; const int IsArray = 13; const int Buffer = 1; void* operator new(size_t Size) { int* ptr = reinterpret_cast<int*>(malloc(Size + 2*sizeof(int))); ptr[0] = NoArray; ptr[1] = Buffer; return reinterpret_cast<void*>(ptr + 2); } void* operator new[](size_t Size) { int* ptr = reinterpret_cast<int*>(malloc(Size + 2*sizeof(int))); ptr[0] = IsArray; ptr[1] = Buffer; return reinterpret_cast<void*>(ptr + 2); } void operator delete(void* Pointer) { int* ptr = reinterpret_cast<int*>(Pointer); if (ptr[-1] != Buffer || ptr[-2] != NoArray) std::cerr << "/!\\ Fehler!" << std::endl; } void operator delete[](void* Pointer) { int* ptr = reinterpret_cast<int*>(Pointer); if (ptr[-1] != Buffer || ptr[-2] != IsArray) std::cerr << "/!\\ Fehler!" << std::endl; }Einige Dinge verstehe ich trotzdem noch nicht:
- Wieso muss
Buffergenau 1 sein, damit es zu keiner Zugriffsverletzung kommt? Du hast ja auch gesagt, dass dort sowieso eine 1 stehen muss, weil sie fürdelete[]benötigt wird... - Warum funktioniert das nur mit
int(beicharodershortgibt es Zugriffsverletzungen)? Ist das so, weil bei mirintgleich gross wie ein Zeiger ist? Müsste man demnach auch das allgemein halten, wenn man Portabilität gewährleisten will? - Gibt es noch sonstige Stellen, die gefährlich sein könnten (undefiniertes Verhalten etc.)?
- Wieso muss
-
Jau, die 1 muss da stehen und ein int sein, weil ja bevor operator delete[] aufgerufen wird, der "compiler" ein int zurückspringt, sich die Anzahl der Elemente rausliest und dann in der Schleife die Destruktoren der Objekte aufruft und anschließend deinen operator delete[] aufruft. Wenn du da 2 in operator new nur 2 chars vorpacken würdest, würde er schonmal 2 bytest aus unalloziertem Speicher lesen. Wenn es 2 shorts wären, würde er beide zu einem int zusammenfassen und diesen integer (der dann ja schon einen ziemlich großen Wert darstellen könnte, weil du ja dann gedacht buffer[0]<<16+buffer[1] da stehen hättest) als Feldanzahl interpretieren (Und dann x Millionen mal einen Destruktor aufrufen, bzw. schon ziemlich am Anfang abschmieren). Die 1 simuliert ja sozusagen ein Feld mit einem Element.
-
Ah, jetzt! In dem Feld vorher steht die Anzahl der Elemente...

Aber das könnte wahrscheinlich auch von Compiler zu Compiler variieren... Hm, ich glaube, dafür gibts wohl keine wirklich standardkonforme Lösung.
Und es wird doch einfachsizeof(void*)zurückgegangen, und nichtsizeof(int), oder? Dass das oft das Gleiche ist, ist ja eher Zufall...Ansonsten, ist mein Code ungefähr brauchbar? Oder wieso ist deiner um einiges komplizierter?

-
Na klar, das ganze Konzept, dass der Compiler den Arraycount in einem int vor dem Array speichert ist natürlich compilerabhängig. Wobei es wohl kaum einen gibt, der das nicht so implementiert. Fraglich wäre nur, ob das in x64-Code dann 64-bit Integer sind... Wobei das ja wiederum eigentlich mit sizeof(int) schon geregelt ist.
Dein Code ist hauptsächlich deshalb schöner, weil du meinen genommen hast und ihn schöner gemacht hast
Ich hatte zuerst die 2 Felder-Idee wegen der 4-byte-Geschichte und dann nachträglich die Idee mit der 1 noch davor und das dann da reingefrickelt.
Woher weißt du, das der Compiler um sizeof(void*) vorspringt und nicht um sizeof(int)? Ich dachte mir halt, da steht also eine Zahl vor, die 4 byte groß ist, also ist es ein int
Alles Annahmen... 
-
Decimad schrieb:
Dein Code ist hauptsächlich deshalb schöner, weil du meinen genommen hast und ihn schöner gemacht hast

Sollte kein Vorwurf sein, nur dank dir hab ich diesen Code ja so hingekriegt.

Nur die Geschichten mit der binären Logik und dem dritten Feld haben mich von Anfang an verwirrt, deshalb habe ich jetzt versucht, ohne sie auszukommen.
Decimad schrieb:
Woher weißt du, das der Compiler um sizeof(void*) vorspringt und nicht um sizeof(int)? Ich dachte mir halt, da steht also eine Zahl vor, die 4 byte groß ist, also ist es ein int
Alles Annahmen... 
Nun ja, ich habe mir vorgestellt, dass er halt gerade eine Zelle im Arbeitsspeicher zurückgeht, und eine Adresse benötigt halt
sizeof(void*)Bytes (Grösse eines Zeigers). Aber meistens sollte das sowieso gleich der Grösse vonintsein... Aber ja, ist halt auch so eine Annahme. So wirklich portabel bringt man das sowieso kaum hin...Vielen Dank für deine Hilfe, du hast mich echt weitergebracht!
