Zugriff auf zerstörtes Objekt möglich



  • hallo

    ich frage mich gerade warum das hier geht:

    MyClass *pMyClass = new MyClass();
    	MyClass2 *pMyClass2 = new MyClass2();
    
    	pMyClass->pMyClass2 = pMyClass2;
    
    	delete pMyClass2;
    	pMyClass2 = NULL;
    
    	/*	Warum geht das hier? Der Speicher von pMyClass2 wurde ja wieder freigegeben
    		und auf NULL gesetzt. pMyClass->pMyClass2 zeigt ja auch auf das Objekt!	*/
    	pMyClass->pMyClass2->i = 5;
    
    	delete pMyClass;
    

    1. für die klasse MyClass2 wird speicher angefordert. Das objekt nennt sich dann pMyClass2.
    2. pMyClass->pMyClass2 wird dann der zeiger vom oben genannten objekt zugewiesen.
    3. nun wird das oben genannte objekt zerstört, also der zeicher freigegeben und der pointer auf NULL gesetzt.

    WARUM also, kann mann trotzdem noch mit "pMyClass->pMyClass2->i = 5;" darauf zugreiffen? wiegesagt, pMyClass->pMyClass2 zeigt ja auf pMyClass2 und somit dachte ich, egal wieviele zeiger es gibt, sobald der hauptzeiger, der den speicher reserviert hat, eliminiert wird, sind diese auch alle tot. was ja mein beispiel damit demonstrieren soll.

    🙄



  • Du hast zwar das Objekt gelöscht, aber nicht alle Pointer auf Null gesetzt.
    Du müsstest noch pMyClass->pMyClass2 auf Null setzen. pMyClass->pMyClass2 zeigt zwar auf ein zerstörtes Objekt, aber im Speicher liegt noch der veraltete ungültige Wert.



  • Zur Ergänzung: pMyClass->pMyClass2 und MyClass2 *pMyClass2 sind zwei selbstständige Variablen. Ein Pointer ist eine Variable wie jede andere auch. Deshalb mußt du auch beide auf Null setzen, es reicht nicht nur den einen Pointer auf Null zu setzen.

    Mit RAII kann man die Schwierigkeit lösen, oder halt Smartpointer einsetzen (z.B. std::tr1::shared_ptr<T> ).



  • egal wieviele zeiger es gibt, sobald der hauptzeiger, der den speicher reserviert hat, eliminiert wird, sind diese auch alle tot

    Richtig.

    Allerdings musst du etwas beachten.

    Du weist hier:

    pMyClass->pMyClass2 = pMyClass2;
    

    der Klasse einen Zeiger auf den Speicher zu. Das heisst, dass du nun 2 Zeiger hast, die auf eine bestimmte Stelle im Code zeigen.

    Und hier gibst du den Speicher frei:

    delete pMyClass2;
        pMyClass2 = NULL;
    

    und setzt den Zeiger pMyClass2 auf 0. Nun hast du aber in der Klasse pMyClass immernoch einen Zeiger pMyClass2, der auf denselben Speicherbereich zeigt. Dieser ist nun aber ungültig, kannst aber trotzdem noch darauf zugreifen. (Was aber undefiniert ist, da der Speicher bereits freigegeben ist.). Sofern der Bereich noch nicht überschrieben worden ist, wird das Objekt noch genau gleich im Speicher vorhanden sein, da er aber sofort überschrieben werden darf, nachdem er freigegeben ist, ist der Zugriff danach undefiniert.

    Und um das klarzustellen. Es spiel überhaupt keine Rolle, wie du etwas benenntst. Das hat nie folgen darauf, mit was das es verknpüpft ist.

    Mal kurz grafisch, was du machst:

    [0x34f212 : Zeiger 1] --------
                                  |-> [ Speicherbereich (gültig) ]
    [0x6542f1 : Zeiger 2] --------
    

    Beide Zeiger (der freie und der in der Klasse) zeigen auf denselben Bereich und beide sind gültig.

    [0x0: Zeiger 1] -/-/-/-/-/-/-/-
                                  |-> [ Speicherbereich (ungültig) ]
    [0x6542f1 : Zeiger 2] --------
    

    Dann gibst du den Speicher per delete (pMyClass) frei und setzt den Zeiger auf 0. Wie du siehst hast du aber immernoch einen Zeiger, der auf den Speicher zeigt, da du den Bereich aber freigegeben hast, ist er nicht mehr gültig.

    Das Verhalten, dass du erwartest, kannst du mittels Smart Pointern erreichen, die du in boost findest.
    Schau dir mal die Verbindung shared_ptr Objekt + weak_ptr an, das wäre genau das, was du hier beschrieben hast.



  • hallo Bulli

    pMyClass->pMyClass2 zeigt zwar auf ein zerstörtes Objekt, aber im Speicher liegt noch der veraltete ungültige Wert.

    in anderen worten heisst das soviel wie:
    sowohl pMyClass2 und pMyClass->pMyClass2 verwalten die gleiche speicheradresse aber sie haben beide unterschiedliche zeiger adressen und somit wird die adress eines zeigers selbst mit NULL auf 0 gesetzt, stimmt das? jeder zeiger hat ja neben der eigentlichen speicheradresse noch eine eigene adresse, nähmlich die, wo der eigentliche zeiger selber liegt. kleines pseudo beispiel:

    (pSelf = die adresse der zeigervariable selber, pAdr = die adresse des zu verweisenden zeiger)
    
    pPointer1 = new int; //pSelf: 0000001A, pAdr: 0000001B
    pPointer2 = new int; //pSelf: 0000002A, pAdr: 0000002B
    
    pPointer2 = pPointer1; //(pPointer2) pSelf: 0000002A, pAdr: 0000001A
    
    delete pPointer1;
    pPointer1 = NULL;
    
    //und jetzt kommt meine vermutung
    /* (pPointer1) pSelf: 00000000, pAdr: 00000000
       (pPointer2) pSelf: 0000002A, pAdr: 00000000 */
    
    //und dann
    
    delete pPointer2; //würde eine exception werfen...
    pPointer2 = NULL; //das sollte aber den unten beschriebenen teil ausmachen
    
    /* (pPointer1) pSelf: 00000000, pAdr: 00000000
       (pPointer2) pSelf: 00000000, pAdr: 00000000 */
    


  • Dir sollte auch klar sein das delete nicht den Speicherbereich des Objekts löscht bzw. jedes Byte davon auf NULL setzt sondern es wird lediglich dem Kernel (also Windows) gesagt das der Speicherbereich nicht mehr benötigt wird und wieder verwendet werden kann. Wie die anderen schon sagten musst Du den Pointer auch auf NULL setzten da dieser ja noch auf den freigegeben Speicherbereich verweist.

    mfg xerox



  • Wenn ich dein pSelf und pAdr richtig verstanden habe, dann müsste es eher so aussehen:

    (pSelf = die adresse der zeigervariable selber, pAdr = die adresse des zu verweisenden zeiger)
    
    pPointer1 = new int; //pSelf: 0000001A, pAdr: 0000001B
    pPointer2 = new int; //pSelf: 0000002A, pAdr: 0000002B
    
    pPointer2 = pPointer1; //(pPointer2) pSelf: 0000002A, pAdr: 0000001B
    
    delete pPointer1;
    pPointer1 = NULL;
    
    //und jetzt kommt meine vermutung
    /* (pPointer1) pSelf: 0000001A, pAdr: 00000000
       (pPointer2) pSelf: 0000002A, pAdr: 0000001B */
    
    //und dann
    
    delete pPointer2; //würde eine exception werfen...
    pPointer2 = NULL; //das sollte aber den unten beschriebenen teil ausmachen
    
    /* (pPointer1) pSelf: 0000001A, pAdr: 00000000
       (pPointer2) pSelf: 0000002A, pAdr: 00000000 */
    


  • hallo drakon. du bist dann wohl erschienen wo ich noch am tippen war 🤡
    vielen dank für deinen beitrag das klingt logisch. demnach sollte mein letztes beispiel hinkommen? eine letzte frage hätte ich noch bezüglich dereferenzierung.

    wenn man mit dem -> operator auf ein member zugreifft der ein zeiger ist, dereferenziert man aber noch nicht, oder? sprich:

    Object->Pointer = Pointer Hier spricht man von was genau?
    *Object->Pointer = *Pointer Hier sollte es meiner meinung nach dereferenzierung sein.

    und noch eine sache diesbezüglich. ich lass, man könne für die dereferenzierung (was ja nichts anderes heisst, als den wert eines zeigers zu ändern), auch ->* verwenden. nun frage ich mich aber, ob das wirklich so ist, weil mein compiler ist damit nicht einverstanden. somit ergibt folgendes bei mir einen compiler fehler:

    *Object->Pointer = value;
    das widerum aber nicht...
    *Object->Pointer = value;



  • @ Bulli wenn ich das richtig sehe bleibt pSelf. Aber warum? setzt also ein NULL pAdr auf 00000000 und ein delete nichts davon sondern gibt wie bereits erwähnt nur den speicher frei. genau so wie wenn ich einen vogelkäfig öffne (delete), der vogel fleigt dann irgendwann raus, darauf habe ich aber keinen einfluss. so?



  • zugriffer schrieb:

    @ Bulli wenn ich das richtig sehe bleibt pSelf. Aber warum? setzt also ein NULL pAdr auf 00000000 und ein delete nichts davon sondern gibt wie bereits erwähnt nur den speicher frei. genau so wie wenn ich einen vogelkäfig öffne (delete), der vogel fleigt dann irgendwann raus, darauf habe ich aber keinen einfluss. so?

    Das Beispiel mit dem Vogelkäfig ist eigentlich ganz gut. Wenn du mit new Speicher anforderst, dann öffnest du den Käfig, setzt einen Vogel rein und schliesst ihn wieder. Dann ärgerst du den Vogel mit einem Stab und wenn du delete machst, öffnest du den Käfig. Ob der Vogel rausfliegt (also Speicherinhalt verändert wird), oder nicht ist nicht bestimmt. Der Vogel kann also munter drin bleiben, aber jeder Zeit einfach so rausfliegen und dann hast du ein Problem. 😉

    Das mit dem auf NULL setzen. Wenn du das hier hast:

    int i = 5;
    i = 0;
    

    setzt veränderst du die Adresse von i beim zweitem mal auch nicht. Und ein Zeiger ist schlussendlich auch nichst anderes, als einfach eine Variable mit einer Adresse im Speicher. Der Wert dieses Objektes ist dann halt eine andere Adresse im Speicher, wo dann irgendendetwas anderes liegt. Wenn du dem Zeiger einen Wert zu weisst, dann ist das auch nichts anderes,als bei der Zuweisung an einen Integer.

    Also das Konstrukt ->* hat man eigentlich nur, wenn man einen Funktionszeiger aufrufen will, da du da meisten einen Zeiger auf ein Objekt hast und dann den Funktionszeiger noch dereferenzieren musst, was dann so etwas in der Art ergibt.

    Der -> Operator dereferenziert nicht, nein. Allerdings kannst du den -> Operator für eine Klasse überladen und dann kann eine Dereferenzierung "übersprungen" werden, was bei smart Pointern gemacht wird.



  • vielen dank euch allen. eine frage noch zur aussage von xerox_

    Dir sollte auch klar sein das delete nicht den Speicherbereich des Objekts löscht bzw. jedes Byte davon auf NULL setzt sondern es wird lediglich dem Kernel (also Windows) gesagt das der Speicherbereich nicht mehr benötigt wird und wieder verwendet werden kann

    das ist interessant. ich dachte das dass ganze ein bisschen anderst abläuft. aus vielerlei gründen. unteranderem weil C++ plattformunabhängig ist und es auch mit keinem system in verbindung gebracht wird. gut, dass tut es mit deiner aussage ja auch nicht, sowas regelt wohl der C++ standard. der new und delete operator unterliegen ja z.B. einer speziellen regelung, anderst als bei anderen sprachfeatures.

    ich dachte eigentlich bislang das der delete operator dem prozessor mitteilt, das speicher an adresse X freigegeben werden muss. ich für mich weiss, dass grundsätzlich assembler nichts mit dem betriebssystem zu tun hat, ein betriebssystem aber natürlich seine finger im spiel hat wenn applikationen ausgeführt werden. und da der assembler teil für ein delete meiner meinung nach ja nichts mit einem eingreifen auf system ebene zu tun haben sollte, war für mich eigentlich eben der gedanke, dass dies so geschehen würde die gedacht.

    dann habe ich aber gleich eine frage zur bitmanipulation. C++ ist ja dafür bekannt, das man sehr hardware nahe programmierung ausschöpfen kann. für mich ist die bitmanipulation z.B. der grösste teil davon. wird dies komplett ohne systemeingriff getan? angenommen ich verwende sämtliche bitwise operatoren (|, &, ^, ~ etc.). ist das dann ein direktes arbeiten von:

    C++ -> assembler -> cpu -> arbeitsspeicher?



  • ergenzung: kann man irgendwo nachlesen wie der new und delete operator komplett in assembler ausschaut? das würde mich wirklich mal interessieren. leider kann ich die assembler files, die mein C++ compiler mir ausgibt nicht dazu verwenden um schlauer zu werden. dort wird hald eben new und delete einfach als extern deklariert, mehr nicht. sämtliche anderen C++ sprachfeatures sind in direktem assembler code zu sehen. was eben sehr interessant ist. man muss erst einmal drauf kommen, das der geschrieben C++ code nicht umbedingt auch so in assembler vorliegt. 😋

    z.B. eine deklarationen einer klasse und anschliesende definition der methoden ergibt noch lange keinen assembler code. im nachhinein ja auch logisch, weil was soll der prozessor machen? erst wenn man die klasse verwendet ergibt sich der eigentliche assembler dazu. bis ich sowas gesehen habe, ging ich immmer der annahme das selbst definitionen in assembler übersetzt werden. aber erst das benutzen und das zusammengehörige verhalten macht den eigentlichen code aus. grundsätzlich sollte ein sauberer C++ code ja auch so aggieren, aber dennoch, sehr interessant :xmas1:



  • zugriffer schrieb:

    ergenzung: kann man irgendwo nachlesen wie der new und delete operator komplett in assembler ausschaut? das würde mich wirklich mal interessieren. leider kann ich die assembler files, die mein C++ compiler mir ausgibt nicht dazu verwenden um schlauer zu werden. dort wird hald eben new und delete einfach als extern deklariert, mehr nicht.

    Versuch es vielleicht mal mit einem anderen Compiler. Der Assemblercode kann je nach Plattform, Architektur und Compiler unterschiedlich sein.



  • okay danke. noch eins: darum heisst es dereferenzierung wenn man den wert eines zeiger ändert und nicht wenn man den zeiger ändert? also:

    dereferenzierung heisst:
    *Pointer = *Pointer2;

    warum heisst folgendes nicht dereferenzierung:
    Pointer = Pointer2;

    so zeigt Pointer ja auf einen anderen speicherbereich. macht der begriff dereferenzierung hier nicht mehr sinn? eine referenz ist ja nichts anderes als ein gültiger zeiger. ein gültiger zeiger heisst nichts anderes als das er auf einen reservierten speicherbereich zeigt. und da dereferenzieren ja eine referenz ändert.... 😕



  • Dereferenzieren heisst die Referenz (also die Indirektion) entfernen. Also direkten Zugriff auf das Objekt gewähren. Dafür wird der operator* verwendet (bei Membern auch operator-> ).

    zugriffer schrieb:

    darum heisst es dereferenzierung wenn man den wert eines zeiger ändert und nicht wenn man den zeiger ändert?

    Du drückst dich unklar aus. Der Wert eines Zeigers ist eine Adresse auf einen Speicherbereich. Wenn man diesen Wert ändert, ändert man auch den Zeiger. Wenn man den Wert des verwiesenen Speicherbereichs ändert, ist das etwas anderes.

    zugriffer schrieb:

    und da dereferenzieren ja eine referenz ändert.... 😕

    Auch hier scheinst du sehr verwirrt zu sein. Vergiss erst mal die C++-Referenz, die hat hiermit eigentlich nicht viel zu tun. Referenzen können nämlich weder geändert werden noch muss man sie explizit dereferenzieren.

    Also nochmals: Du dereferenzierst einen Zeiger, indem du seinem Verweis folgst und auf den verwiesenen Speicherbereich zugreifst.

    int var = 5;
    int zpx = 3002;
    int* ptr = &var;      // ptr ist ein Zeiger, der auf die int-Variable var zeigt.
    *ptr = 7;             // ptr wird dereferenziert, der Wert von var wird auf 7 gesetzt.
    ptr = &zpx;           // ptr wird geändert, ohne dereferenziert zu werden. Er zeigt nun auf etwas anderes (zpx).
    


  • danke. das ist mir soweit schon klar. ich gebe zu meine formulierung war nicht die beste. 🙂 ich finde es nur merkwürdig das wenn man den speicherinhalt, auf den ein zeiger verweisst, verändert, dies dann dereferenzieren heisst. wie nennt man den sowas:

    ptr2 = ptr2;

    referenzieren gibt es ja nicht :). und wäre sowieso falsch weil eben mit diesem beispiel wird der zeiger selbst geändert. gibt es einen begriff für für die zuweisung eines zeiger an einen anderen zeiger?

    also, ohne indirektionsoperator ist es kein dereferenzieren, mit indirektionsoperator ist es ein dereferenzieren. ich frage mich blos warum es nicht genau umgekehrt wer fall ist. woher kommt der begriff "dereferenzieren" sonst noch? kann man den begriff mit einem real world beispiel verknüpfen?

    int var1 = 5;
    int var2 = 10;
    var1 = var2;

    ist ja auch kein dereferenzieren und doch ist es genau das gleiche: wert an wert zuweisen. wenn das nun aber auch dereferenzierung bezeichnet wird, dann ist es okay. dann wurde einfach für die schreib/lese zugriffe der begriff dereferenzierung genommen.



  • darf ich noch zwei fragen stellen? 🕶

    wie bekommt man eigentlich die adresse des zeiger selber? &ptr liefert hierbei ja die adresse auf die der zeiger zeigt, nicht aber die adresse des zeiger selber.

    und wozu dienen zeiger auf zeiger? ich glaube dann habe ich es endlich 🕶



  • @zugriffer:

    Der Assembler-Code für new oder delete würde dir garantiert nichts sagen. Ist meist etwas kompliziert und reichlich unübersichtlich. Was new/delete macht ist auch nicht ganz richtig beschrieben worden. Ob, und wenn, wo, ein Betriebssystem da mitspielt ist im Standard z.B. nicht definiert.
    "new" gibt dir einfach eine Adresse die du verwenden darfst, und mit "delete" teilst du dem System mit dass du die Adresse nichtmehr benötigst. Zusätzlich "initialisiert" new noch das Objekt, d.h. der Konstruktor wird aufgerufen, und delete "zerstört" das Objekt, d.h. der Destruktor wird aufgerufen.

    Wenn du auf ein Objekt nach "delete" noch zugreifst verletzt du damit 2 "Übereinkommen":

    1. Der Destruktor des Objekts wurde ausgeführt, und ein Objekt das zerstört wurde "existiert nichtmehr", d.h. man darf auch nicht darauf zugreifen
    2. Du hast dem System mitgeteilt dass du den Speicherbereich nichtmehr verwendest, d.h. du darfst das auch nichtmehr tun

    Vonwegen "Dereferenzieren" - damit bezeichnet man den Vorgang wenn man quasi einer Referenz zum referenzierten Objekt folgt. Eine Referenz wäre sowas wie ne Adresse (Strasse + Hausnummer), und das referenzierte Objekt wäre ein Haus. Wenn ich mir die Strasse + Hausnummer angucken, und dann dorthingehe, dann habe ich die Adresse "dereferenziert". Ob ich jetzt auf das Haus zugreife, das angucke, reingehe etc. ist dabei egal.

    D.h. folgendes nennt man dereferenzieren:

    int x = 0;
    int* p = &x;
    
    *p; // <- hier wird p dereferenziert -- auf das "Haus" zuzugreifen ist nicht nötig damit man es als Dereferenzieren bezeichnet
    

    Wenn ich dagegen die alte Strasse + Hausnummer weglösche, und eine neue hinschreibe, dann wäre das eine einfache Zuweisung. Denn dazu muss ich ja weder zum alten noch zum neuen Haus hingehen.



  • zugriffer schrieb:

    darf ich noch zwei fragen stellen? 🕶

    wie bekommt man eigentlich die adresse des zeiger selber? &ptr liefert hierbei ja die adresse auf die der zeiger zeigt, nicht aber die adresse des zeiger selber.

    Nein, "&ptr" liefert die Adresse von "ptr", und wenn "ptr" ein Zeiger ist, dann ist das die Adresse des Zeigers. Die Adresse auf die "ptr" zeigt, ist einfach "ptr".

    und wozu dienen zeiger auf zeiger? ich glaube dann habe ich es endlich 🕶

    Wozu dienen Zeiger überhaupt? Wenn du das beantworten kannst, dann hast du auch die Antwort auf deine Frage.
    Zeiger auf Zeiger sind dafür gut, damit man mit Zeigern auch alles das machen kann, was man mit Hilfe von Zeigern mit Nicht-Zeigern machen kann 😃
    (OK, das muss man jetzt nicht verstehen, auch wenn der Satz IMO korrekt ist)

    Also nochmal anders: Zeiger verwendest du z.B. wenn du einer Funktion die Adresse von irgendwas mitgeben willst, damit diese Funktion dieses irgendwas ändern kann.
    OK. Jetzt kann es aber vorkommen, dass dieses irgendwas selbst ein Zeiger ist. In dem Fall hast du dann einen Zeiger auf einen Zeiger.

    Wenn man modernes C++ programmiert, sind die Fälle, in denen man Zeiger auf Zeiger braucht, allerdings recht selten. Sehr selten. Kann mich nicht erinnern wann das letzte mal war dass ich einen gebraucht hätte.



  • guten morgen hustebaer und danke für deine beiträge. nun, was zeiger ansich sind und wie man sie verwendet ist mir schon klar. nur eben zeiger auf zeiger (int **ptr) ergibt mir ohne gutes beispiel noch keinen sinn. man kann damit ja auf einen zeiger zugreifen der selbst auf einen speicherbereich zeigt.

    Jetzt kann es aber vorkommen, dass dieses irgendwas selbst ein Zeiger ist. In dem Fall hast du dann einen Zeiger auf einen Zeiger

    und eben, was soll das bringen? mit einem normalen zeiger den ich als parameter übergebe, kann ich den zeiger selbst ändern oder nur den wert der im speicher steht. wozu also einen doppel-moppler zeiger?


Log in to reply