Instanz exestiert nach Aufruf des Destruktors immer noch!



  • Hallo.

    Folgende klasse erstmal:

    class dog
    {
      int age;
    
      public:
        dog() {}
        ~dog() {}
    
        void setage(int value) { age = value; }
        int getage() const { return age; }
    };
    

    und das rahmenprogramm:

    int main()
    {
       dog donald;
       donald.setage(5);
       cout<<donald.getage();
       // bis hier hin ist alles wunderbar
    
       donald.~dog(); // hund muss jetzt tot sein
    
       cout<<donald.getage(); // geht immer noch!!!! Warum zum teufel?????
    
       return 1; //  :rage: 
    }
    

    Kann mich jemand aufklaeren bitte? Ist donald nach dem destruktoraufruf das, was man als 'leiche' bezeichnet? dieser leiche kann ich ueber setage() aber immer noch werte zuweisen, dass geht dann allerdings zu weit.

    bitte um hilfe.



  • der dtor lässt den speicher des objekts selbst in ruhe, aber du kannst nie wissen, ob sich ein objekt nach einem dtor aufruf noch in einem gültigen zustand befindet, (außer jetzt bei deinem eigenen)
    ein dtor zerstört quasi den sinn des objekts, den inhalt. nicht aber den speicher selbst, das ist sache der implementation.
    er macht nicht mehr, als dass, was du in ihn hineinschreibst



  • achso. gut zu wissen. danke.



  • Machen wir doch einmal ein Experiment:

    Erster Fall:

    #include <iostream>
    #include <conio.h>
    using std::cout;
    using std::endl;
    
    class dog 
    { 
      private:
        int age; 
    
      public: 
        dog() {cout << "ctor " << this << endl;} 
        ~dog() {cout << "dtor" << endl;} 
        void setage( int value ) { age = value; } 
        int getage() const { return age; } 
    };
    
    int main() 
    { 
      dog donald; 
      donald.setage(5); 
      cout << donald.getage() << endl; 
      donald.~dog(); // Hund donald müsste jetzt tot sein ?!
      cout << donald.getage() << endl; // Geht immer noch!!!! Warum zum Teufel????? 
    
      dog wuffi; 
      wuffi.setage(9); 
      cout << wuffi.getage() << endl; 
      cout << donald.getage() << endl; // ... und jetzt?
    
      dog waffi; 
      waffi.setage(13); 
      cout << waffi.getage() << endl; 
      cout << donald.getage() << endl; // ... und jetzt?
    
      getch();
      return 0; // nach ISO-C++-Standard (1998) überflüssig 
    }
    

    donald wird im Stack nicht durch wuffi oder waffi überschrieben!

    Zweiter Fall:

    #include <iostream>
    #include <conio.h>
    using std::cout;
    using std::endl;
    
    class dog 
    { 
      private:
        int age; 
    
      public: 
        dog() {cout << "ctor " << this << endl;} 
        ~dog() {cout << "dtor" << endl;} 
        void setage( int value ) { age = value; } 
        int getage() const { return age; } 
    };
    
    int main() 
    { 
      dog* pdog;
      {
        dog donald;
        pdog = &donald; 
        donald.setage(5); 
        cout << donald.getage() << endl; 
        //donald.~dog(); // Hund donald müsste jetzt tot sein ?!
      }//dtor 
      cout << pdog->getage() << endl; // Geht immer noch!!!! Warum zum Teufel????? 
      dog wuffi; 
      wuffi.setage(9); 
      cout << wuffi.getage() << endl; 
      cout << pdog->getage() << endl; // ... und jetzt?
    
      dog waffi; 
      waffi.setage(13); 
      cout << waffi.getage() << endl; 
      cout << pdog->getage() << endl; // ... und jetzt?
    
      getch();
      return 0; // nach ISO-C++-Standard (1998) überflüssig 
    }
    

    Nun überschreibt wuffi den Speicherbereich von donald! Der eigentlich für donald gedachte Zeiger beinhaltet die Adresse des Objektes wuffi.

    Interessante Frage: Warum sorgt der durch '}' aufgerufene Dtor für Speicherfreigabe im Stack, während im ersten Fall der selbst erzeugte Dtor einfach "nichts" macht? 😉



  • Erhard Henkes schrieb:

    Interessante Frage: Warum sorgt der durch '}' aufgerufene Dtor für Speicherfreigabe im Stack, während im ersten Fall der selbst erzeugte Dtor einfach "nichts" macht? 😉

    der Dtor macht aber immer das gleiche



  • wirklich? 😉
    schau Dir doch einmal die Speicheradressen der Objekte in Experiment 1 und Experiment 2 an! Kann man ja per "cout << &hund << hund.getage() << endl;"
    ausgeben.
    Zumindest beim Dev-C++ ergibt dies unterschiedliche Ergebnisse. In Fall 1 wird der Speicher nicht frei gegeben, in Fall 2 erfolgt dies.



  • Ein interessanter Fall. Während beim Dev-C++ der Speicher von "donald" in Fall 2 freigegeben und sofort überschrieben wird, ist dies beim MSVC++ 6 nicht so. Hier wird auch in Fall 2 neuer Speicher verwendet. Jedoch schreiten in Fall 1 zum Programmende ( nach main(){...} ) - im Debug-Modus testbar - 3(!) Destruktoren zur Tat, während in Fall 2 nur noch 2 Dtor's notwendig sind.

    Das Verhalten bezüglich Speicherüberschreibung (ja oder nein) ist also abhängig vom Compiler. 😉



  • der dtor macht wirklich immer das gleiche.

    naemlich alle ressourcen des objektes freigeben. allerdings nicht das objekt selber.

    beispiel:

    T* p=new T;
    //1 T objekt
    p->~T(); //gekillt
    p=new (p) T(); //placement new
    //neu erstellt
    delete p;
    

    wenn p->~T() das objekt p freigeben wuerde, waere ein placement new nicht realisierbar.

    der grund warum dein objekt aufeinmal weg ist, ist }

    denn bei einem } gehen objekte aus dem scope. das bedeutet:
    der Dtor wird aufgerufen UND das objekt geloescht.

    in etwa so:

    {
      T t; //1
      t.foo(); //2
    } //3
    wird zu
    
    p=alloc(sizeof T);
    construct_T(p);
    //1
    foo(p);
    //2
    destruct(p);
    dealloc(p);
    //3
    

    mit dem dtor hat das nix zu tun.



  • Das ist richtig. Dtor und '}' sind zwei verschiedene Dinge, die aber in der Praxis oft zusammen fallen, weil '}' eben den Dtor aufruft. Frage: Darf ein Objekt überschrieben werden, wenn sein Dtor ausgeführt wurde oder wird das immer erst durch '}' gelöscht? '}' ruft den Dtor ja noch einmal auf, selbst wenn er bereits aufgerufen wurde. In vielen Büchern/Tutorials findet man, dass der Dtor den Speicher des Objektes frei gibt. Die Beispiele von oben zeigen, dass der Speicher erst nach '}' erneut benutzt wird.

    Im C++-Standard (12.4) findet man diesbezüglich:
    Once a destructor is invoked for an object, the object no longer exists;
    the behavior is undefined if the destructor is invoked for an object whose lifetime has ended (3.8). [Example: if the destructor for an automatic object is explicitly invoked, and the block is subsequently left in a manner that would ordinarily invoke implicit destruction of the object, the behavior is undefined.]

    "the object no longer exists" wird von den Compilern so nicht umgesetzt, wie man sieht, denn man kommt sowohl an das "destruierte" Objekt und als auch an seine Daten heran, wenn auch nach Standard "undefiniert". Diese Zombie-Objekte sind doch genau genommen eine Herausforderung für jeden Puristen. Offensichtlich wie so oft eine Frage der Performance, weil man keine Zeit damit verlieren will, Objekte zu "nullen".



  • Erhard Henkes schrieb:

    "the object no longer exists" wird von den Compilern so nicht umgesetzt, wie man sieht, denn man kommt sowohl an das "destruierte" Objekt und als auch an seine Daten heran, wenn auch nach Standard "undefiniert".

    warum sollte man nullen? was bringt das, ausser mehr kosten?

    das objekt existiert nicht mehr - sein speicher aber schon.

    T* t=new T;
    t->~T();

    der speicher von t bleibt erhalten - waere ja katastrophal wenn er verschwinden wuerde.

    ich verstehe nicht ganz worauf du hinaus willst.



  • Wird der Speicher nicht einfach bloß freigegeben also "unlocked"? Das muss ja nicht zur Folge haben, dass er auch gleich überschrieben wird... Demzufolge funktionieren verwaiste Zeiger immer noch. Allerdings gibt es nach der Speicherfreigabe keine Garantie mehr, dass der Speicher nicht überschrieben wird....

    Ausserdem redet einer von statischer Reservierung auf dem Stack und ein anderer von dynamischer Allokierung auf dem Heap....
    Den Pointer, der auf den dynamisch Allokierten Speicherbereich zeigt, zu NULLen würde nur ein MemoryLeak verursachen..... Man muss den Speicher mit "delete" freigeben.

    Das {}-Paar setzt eine Stackframe (richtiger Begriff ? 😮 ) auf. Nach Ende des {}-Paars wird der Speicher freigegeben....

    Compiler reservieren Stack mit statischer Addressauflösung:

    // Stackgröße ist 0Bytes also Stackpointer 0x00000000
      dog* pdog;   
      {                  // erzeugen neuer Stackframe
        dog donald;      // Stack erhöht durch sizeof(class dog), Stackpointer+=sizeof(class dog)
        pdog = &donald;  // zeigt auf 0x00000000, also den Anfang des Stacks, wo auch das Objekt liegt
        donald.setage(5);  
        cout << donald.getage() << endl;  
        //donald.~dog(); 
      }//dtor            // Stackframe wird aufgelößt, Stackpointer wieder 0x00000000
                         // der Stack ist aber noch nicht überschrieben und pdog, nun ein verwaister Zeiger, welcher immernoch auf den
                         // Speicher des Objekts zeigt. Dieser kann jedoch Jederzeit überschrieben werden...
      cout << pdog->getage() << endl; 
      dog wuffi;         // Da der Stack jetzt wieder am Anfang steht, wird der alte Inhalt überschrieben...
      wuffi.setage(9);  
      cout << wuffi.getage() << endl;  
      cout << pdog->getage() << endl; // ... und jetzt? 
    
      dog waffi;  
      waffi.setage(13);  
      cout << waffi.getage() << endl;  
      cout << pdog->getage() << endl; // ... und jetzt? 
    
      getch(); 
      return 0; // nach ISO-C++-Standard (1998) überflüssig  
    }
    

    (wäre meine Begründung, erklärt auch das im 1.Fall nicht überschrieben wird)

    wenn mein Senf stimmt, müsste

    // Stackgröße ist 0Bytes also Stackpointer 0x00000000
      dog* pdog;   
      {                  // erzeugen neuer Stackframe
        dog donald;      // Stack erhöht durch sizeof(class dog), Stackpointer+=sizeof(class dog)
        pdog = &donald;  // zeigt auf 0x00000000, also den Anfang des Stacks, wo auch das Objekt liegt
        donald.setage(5);  
        cout << donald.getage() << endl;  
        //donald.~dog(); 
      }//dtor            // Stackframe wird aufgelößt, Stackpointer wieder 0x00000000
                         // der Stack ist aber noch nicht überschrieben und pdog, nun ein verwaister Zeiger, welcher immernoch auf den
                         // Speicher des Objekts zeigt. Dieser kann jedoch Jederzeit überschrieben werden...
    
      char buff[sizeof(class dog)]="";      // Stack reservieren
                                            // Wird der Stack bloß reserviert und nicht überschrieben, kann der verwaiste Zeige 
                                            // bis zum Programmende seine Funktionalität behalten..         
      memset(buff,' ', sizeof(class dog) ); // Stack überschrieben
    
      cout << pdog->getage() << endl; 
      dog wuffi;         // Da der Stack jetzt wieder am Anfang steht, wird der alte Inhalt überschrieben...
      wuffi.setage(9);  
      cout << wuffi.getage() << endl;  
      cout << pdog->getage() << endl; // ... und jetzt? 
    
      dog waffi;  
      waffi.setage(13);  
      cout << waffi.getage() << endl;  
      cout << pdog->getage() << endl; // ... und jetzt? 
    
      getch(); 
      return 0; // nach ISO-C++-Standard (1998) überflüssig  
    }
    

    Die beiden Zeilen dürften bewirken, das donald "tot" ist 😃

    Lange Rede kurzer Sinn.... : Der Speicher wird freigegeben aber nicht automatisch überschrieben/gelöscht ( gilt für Heap und Stack )mit 0000000 oder so.... Er steht also für neue Reservierungen zur Verfügung

    gruß



  • donald.~dog(); // hund muss jetzt tot sein 
    cout << donald.getage(); // geht immer noch!!!! Warum zum teufel?????
    

    Dies war der Ausgangspunkt. Es verwunderte den unbedarften Nutzer eben, dass das "Objekt" (nicht nur der Speicherbereich mit seinen noch nicht überschriebenen Inhalten) noch existiert und über seinen Namen ohne Compilerwarnung ansprechbar ist. Der Destruktor wird bei '}' nochmals aufgerufen.

    Aber mal eine ganz andere Frage: wie kann man unterscheiden/feststellen, ob ein Objekt "lebendig" oder nur noch ein "Zombie" ist? 😃



  • puh kenn ich nicht....

    Unter Windows kann man mit IsBad*-Calls abfragen ob man von Addresse lesen darf... Aber von der Addresse kann man ja trotzdem noch lesen... wie gesagt sorry kA

    gruß



  • Gibt es da wirklich keine Möglichkeit zu unterscheiden, ob ein Objekt "lebendig" oder nur noch ein "Zombie" ist? 😕



  • Erhard Henkes schrieb:

    Gibt es da wirklich keine Möglichkeit zu unterscheiden, ob ein Objekt "lebendig" oder nur noch ein "Zombie" ist? 😕

    nein, wozu auch?

    es gibt in C++ keine Zombie-Objekte
    den dtor darf man nur im zusammenhang mit placement new aufrufen.



  • "es gibt in C++ keine Zombie-Objekte."

    In der Theorie vielleicht nein, in der Praxis aber wohl (s.o.).
    Wir haben hier bisher vor allem über Objekte auf dem Stack (also ohne new erzeugt) gesprochen, nicht auf dem heap. Die kann man durch einen "destructor praecox" in so eine Art "zombies" verwandeln. Nun geht es um die Detektion dieser Objektspezies. Ein lustiges Thema. 😃



  • wo ist da n zombie objekt?

    donald.~dog();
    darfst du eingach nicht machen - hoechstens um nachher ein placement new anzubringen - aber selbst da wuerde ich stark an dem programmierer zweifeln.

    es ist zwar moeglich ein objekt zum zombie zu machen - doch wer das tut ist selber schuld - denn es ist nicht vorgesehen.



  • Definiere Zombie.

    Wir haben hier bisher vor allem über Objekte auf dem Stack (also ohne new erzeugt) gesprochen, nicht auf dem heap. Die kann man durch einen "destructor praecox" in so eine Art "zombies" verwandeln. Nun geht es um die Detektion dieser Objektspezies. Ein lustiges Thema

    Laut C++ Lebenszyklus-Modell existiert mit Betreten des Destruktors kein Objekt mehr. Es existiert nur noch ein haufen toter Bits. Keine Ahnung ob das deiner Definition von Zombie gleichkommt. Erkennen kannst du diese Objektspezie freilich nicht, da hier keine Objekte mehr existieren.
    Daran würde übrigens auch das Ausnullen des Speichers nichts ändern.

    Man darf nicht den Namen eines Objekts (den genauen Standard-Begriff weiß ich im Moment nicht) oder die Referenzen auf ein Objekt mit dem Objekt selbst verwechseln. Der Destruktor zerstört ein Objekt. Der Name verliert mit dem Ende des Scopes seine Gültigkeit. Das eine hat mit dem anderen aber nicht direkt etwas zu tun.
    Bei auto-Objekten fällt beides im Normalfall zusammen. Mit Scope-Ende verschwindet also der Name und es wird der Dtor des Objekts aufgerufen.

    Wenn du vorher selbst den Dtor aufrufst, existiert wie gesagt kein Objekt mehr. Der Name referenziert danach kein Objekt, sondern toten Speicher.

    Wie auch immer. Für solche Sachen braucht man keine Objekte im C++-OOP-Sinne. Das geht genauso gut mit C und Zeigern.



  • Wie kann man unterscheiden, ob ein "Name" (ist ja ein zeiger) ein gültiges Objekt oder einen toten Bithaufen, der nur noch ein ungültiges Abbild des Objektes ist, adressiert?



  • Wie kann man unterscheiden, ob ein "Name" (ist ja ein zeiger) ein gültiges Objekt oder einen toten Bithaufen, der nur noch ein ungültiges Abbild des Objektes ist, adressiert?

    Gar nicht. Wie ich oben bereits schrieb.



  • Thomas Strasser, C++ Programmieren mit Stil, 2. Auflage, 2003, S.187:
    "Elementfunktionen, die automatisch aufgerufen werden (wie etwa Konstruktor, Destruktor etc.), sollten nicht von Hand aus aufgerufen werden."

    Thomas Strasser, C++ Programmieren mit Stil, 2. Auflage, 2003, S.196:
    "Konstruktoren und Destruktoren sind Initialiserungs- bzw. De-Initialiserungsfunktionen. Sie sind nicht verantwortlich für das Anlegen bzw. Freigeben des Objekts selbst bzw. dessen Instanzvariablen. Das wird vom C++-System übernommen."


Anmelden zum Antworten