Destruktor Fragen



  • Hallo,
    einige Verständsfragen zur Vernichtung von Objekten:
    (man merkt hier kaum, ob man es richtig macht).

    1. Korrekt, dass dies hier:

    class A 
    {
    	Object* globalobj;
    };
    

    das erfordert?

    A:~A()
    {
    	delete globalobj;
    }
    

    2. New in einer Funktion erfordert auch
    delete in einer Fkt?

    void test1()
    {
       Object1 *obj = new Object1();
       delete obj;
    }
    

    - wie lange lebt obj hier, wenn man kein delete macht?
    - wenn man test1 verlässt, kann man dann nicht mehr auf
    das Objekt zugreifen aber der Speicher ist noch besetzt?
    - was ist, wenn test1 immer wieder aufgerufen wird,
    ihne obj zu deleten?
    - welcher Destruktor wird durch delete obj
    aufgerufen und welchen Inhalt hat dieser?

    3. verhalten sich Objekte im Konstruktor anders als bei
    test1 ?

    A::A()
    {
    	Object2 *obj = new Object2();
    }
    

    4. Ist das korrekt?

    void test2()
    {
       for (int i = 0; i < size; i++)
    	{		
    		Object *object = new Object(i);		
    	}	
    
    	delete object[];
    }
    

    5. Benutzt man seit Smartpointer überhaupt noch new?

    Danke.


  • Mod

    1. Das erfordert gar nichts. Es kommt nicht die Polizei, wenn du es nicht machst, und das Programm wird ganz normal übersetzt werden und ist normal ausführbar. Willst du jedoch ein korrektes Programm ohne Leaks schreiben, dann willst du aber wahrscheinlich etwas in der Art machen. Außerdem willst du dafür noch viel mehr machen. Google mal "RAII", "Rule of 3", "Rule of 5". Im Vorgeschmack für deine Frage 5: Eigentlich willst du viel weniger machen, google dazu "Rule of 0".

    2. Siehe 1. für allgemeine Kommentare. Zusatzkommentar: Ein dynamisches Objekt, welches nur für die Länge einer Funktion lebt, ist ziemlich sinnlos. Wieso nicht automatisch? Dazu sind "normale" Variablen schließlich da.
    -obj selber, also der Pointer, lebt bis zum abschließenden }. Das, worauf obj zeigt lebt, sofern es nie deletet wird, ewig.
    -Wenn die Funktion verlassen wurde und mit obj die letzte Referenz auf das new-Objekt stirbt, dann lebt dieses weiter, aber ist verloren -> Leak.
    -Wenn man den Leak mehrmals ausführt, wird immer mehr leaken, bis der Speicher voll ist und der Prozess mit einer entsprechenden Exception abstürzt
    -delete ruft einen Destruktor passend zum benutzten Pointertyp auf, oder, wenn das ein Typ mit einem virtuellen Destruktor ist, den passenden dynamischen Typ. Also bloß nicht den Pointertyp umcasten und darüber deleten!

    3. Falls ein Konstruktor eine Exception wirft, dann wird der Destruktor nicht aufgerufen. Das heißt, es liegt in der Verantwortung des Programmierers, diesen Fall aufzuräumen. Das ist oftmals sehr unschön, daher vermeidet man normalerweise Ressourcenbelegung im Konstruktorkörper, sondern benutzt Initialisierungslisten um damit Halterklassen (z.B. Smartpointer) zu initialisieren, denn hinter initialisierten Membern wird automatisch aufgeräumt.

    4. Offensichtlich nicht, denn das compiliert nicht einmal. Würde man es compilierfähig machen, indem man die Deklaration von object aus der Schleife zieht, dann würde es:
    a) Leaken ohne Ende
    b) Undefiniertes Verhalten zeigen, weil delete[] mit normalem new gemischt wurde
    -> totaler Müll

    5. Nein, nutzt man nicht. Wobei ich nicht sagen würde "seit", sondern schon lange vorher nicht. Womit alle deine Fragen hinfällig sind, denn nichts von dem gezeigten solltest du jemals tun!



  • TauCeti schrieb:

    1. Korrekt, dass dies hier:

    class A 
    {
    	Object* globalobj;
    };
    

    das erfordert?

    A:~A()
    {
    	delete globalobj;
    }
    

    Jain. Kommt drauf an. Eher nicht. Richtig ist, dass zu jedem new ein delete gehört -- es sei denn, Du hast kein Problem mit Speicherlecks. Du musst Dir beim Design überlegen, wer für die Verwaltung von einer Resource verantworlich ist und demnach die Resource auch wieder freigeben muss. Die Frage ist jetzt, was genau "Object" ist und warum Du das Datenelement "globalobj" genannt hast. Das klingt nämlich so, als wenn mehrere A-Objekte sich auf dasselbe "Object" über "globalobj" beziehen können sollen. Dann wäre das mit Deinem Destruktor eine sehr schlechte Idee. Du darst ein Objekt ja höchstens einmal löschen, wenn Du es mit new angelegt hast. Und so, wie Du es konstruiert hast, ist ein mehrfahres Löschen nicht ausgeschlossen. Falls jedes A sein eigenes Object hat, musst Du Dich fragen, ob folgendes nicht besser ist:

    class A {
        Object myObj; // nix Zeiger, nix delete.
    };
    

    Prinzipiell gibt es dann noch folgendes Muster:

    class A {
        unique_ptr<Object> myObj;
    };
    

    Hier musst Du auch keinen Destruktor selbst definieren. Wenn ein A Objekt zerstört wird, wird automatisch *myObj auch zerstört, weil myObj ein Zeiger ist, der sich alleinig dafür verantwortlich fühlt, worauf er zeigt ("unique ownership"). Das kann u.a. sinnvoll sein, wenn Object polymorph ist, also mindestens eine virtuelle Methode hat.

    Oder auch das hier ohne eigenen Destruktor:

    class A {
        Object* habe_ich_mir_von_wem_anders_ausgeliehen;
    };
    

    In diesem Fall würde ein A-Objekt ein anderes Object "kennen", es aber nicht besitzen und dementsprechend auch nicht versuchen, es zu löschen. Das ist schon legitim so. Es kann aber passieren, dass wenn Du beim Design einen Fehler gemacht hast, dass das Objekt an anderer Stelle zu früh gelöscht wird, was diesen rohen Zeiger hier baumeln lassen würde. Das kannst Du dem Zeiger leider auch nicht ansehen.

    Ganz wichtig: Wenn Du Dich genötigt siehst, einen eigenen Destruktor zu definieren, weil du darüber irgendwas wieder freigeben willst, dann musst Du auf die Dreierregel achten. Die hast Du in deinem ersten Beispiel schon verletzt. Habe ich schon gesagt, dass das ganz wichtig ist? C++ ist, was das angeht, nicht gerade tolerant. Die Compiler warnen dich auch leider nicht, obwohl seit C++11 compiler-generierte Kopieroperationen bei einem benutzerdefinierten Destruktor "deprecated" sind. Das ist schon traurig und nicht besonders freundlich gegenüber Neulingen. 😞

    TauCeti schrieb:

    2. New in einer Funktion erfordert auch
    delete in einer Fkt?

    void test1()
    {
       Object1 *obj = new Object1();
       delete obj;
    }
    

    Nein, ein new in einer Funktion erfordert nicht unbedingt ein delete in derselben. Es kommt drauf an. Dein Beispiel ist relativ sinnfrei; denn für dieses Szenario, wo das Objekt wieder zerstört werden soll, bevor die Funktion beendet wird, kannst Du Dir diesen Zeiger-Hickhack komplett sparen:

    void test1()
    {
       Object1 obj;
    }
    

    Bezogen auf dein eigenes Beispiel:

    TauCeti schrieb:

    - wie lange lebt obj hier, wenn man kein delete macht?

    Jedes new erfordert ein delete. Kommt kein delete, hängt das Obekt für ewig im Speicher. Deswegen willst Du so selten wie möglich new und delete benutzen. Das ist einfach nervig, sowas manuell zu verwalten.

    TauCeti schrieb:

    - wenn man test1 verlässt, kann man dann nicht mehr auf
    das Objekt zugreifen aber der Speicher ist noch besetzt?

    Ja. Das ist dann ein Speicherleck.

    TauCeti schrieb:

    - was ist, wenn test1 immer wieder aufgerufen wird,
    ohne obj zu deleten?

    Das ist dann immer noch ein Speicherleck. Mehr und mehr Speicher wird verschwendet. Irgendwann wird deine Kiste langsam oder das Betriebssystem schießt dann deinen Prozess ab.

    TauCeti schrieb:

    - welcher Destruktor wird durch delete obj
    aufgerufen und welchen Inhalt hat dieser?

    Das kommt drauf an, wie Object1 defniert ist.

    TauCeti schrieb:

    3. verhalten sich Objekte im Konstruktor anders als bei
    test1 ?

    A::A()
    {
    	Object2 *obj = new Object2();
    }
    

    Nein. Das wäre hier genauso ein Speicherleck.

    TauCeti schrieb:

    4. Ist das korrekt?

    void test2()
    {
       for (int i = 0; i < size; i++)
    	{		
    		Object *object = new Object(i);		
    	}	
    
    	delete object[];
    }
    

    Nein. Das kompiliert gar nicht. Die Variable "object" existiert ja nur innerhalb der Schleife. Nach der Schleife gibt es keine Variable namens "object" mehr. Die Syntax für das delete mit der Klammer ist auch falsch.

    TauCeti schrieb:

    5. Benutzt man seit Smartpointer überhaupt noch new?

    Ich würde fast behaupten, dass es seit C++14 eigentlich keine Notwendigkeit mehr für new gibt. Du hast std::make_unique und std::make_shared aus dem <memory> -Header als Ersatz. Es gibt sicherlich auch Ausnahmen. Da fällt mir aber im Moment nichts zu ein.



  • SeppJ schrieb:

    5. Nein, nutzt man nicht. Wobei ich nicht sagen würde "seit", sondern schon lange vorher nicht. Womit alle deine Fragen hinfällig sind, denn nichts von dem gezeigten solltest du jemals tun!

    krümelkacker schrieb:

    Ich würde fast behaupten, dass es seit C++14 eigentlich keine Notwendigkeit mehr für new gibt. Du hast std::make_unique und std::make_shared aus dem <memory>-Header als Ersatz. Es gibt sicherlich auch Ausnahmen. Da fällt mir aber im Moment nichts zu ein.

    Wie funktionert der Mechanismus als Ersatz für "new"? Wie werden denn dann Instanzen erzeugt? Hat jemand einen Link dazu?
    Danke.



  • Hallo

    In der Regel, also wenn es keinen Grund dagegen gibt, legt man die Instanzen direkt auf dem Stack an: Object Instance{ 123 };
    Wenn man dynamischen Speicher braucht, dann nutzt man Smartpointer. Modernes Standard-C++ kennt zwei besitzende Smartpointer-Typen: std::unique_ptr und std::shared_ptr . Eine gute Faustregel ist, immer unique_ptr zu nutzen, es sei denn, es gibt einen Grund dagegen. Beide dieser Smartpointer kommen mit ihrer je eigenen make-Funktion: std::make_unique und std::make_shared . Möchte ich nun eine Instanz auf dem Heap anlegen, dann würde ich schreiben: auto InstancePtr = std::make_unique< Object >( 123 );
    InstancePtr ist hier vom Typ std::unique_ptr< Object > . Alle Funktionsargumente der make-Funktionen werden an den Konstruktor geforwarded.

    LG



  • TauCeti schrieb:

    Wie funktionert der Mechanismus als Ersatz für "new"? Wie werden denn dann Instanzen erzeugt?

    new bzw deren modernere Alternativen sind nur dazu da, Platz im Freispeicher zu reservieren und dort etwas abzulegen. In vielen Fällen kann man das Containern überlassen. Und Container selbst kann man zum Beispiel im automatischen Speicher anlegen. Das kann dann so aussehen:

    #include <algorithm>
    #include <iostream>
    #include <vector>
    
    int main() {
        std::vector<int> data;
        int temp;
        while (std::cin >> temp) { // solange Zahlen einlesen bis Ende
            data.push_back(temp);
        }
        std::sort(data.begin(), data.end()); // Eingabe sortieren
        for (int x : data) {
            std::cout << x << '\n'; // ...und wieder ausgeben.
        }
        return 0;
    }
    

    Hier findest du kein new. Und dennoch wird der Freispeicher genutzt. Das erledigt aber std::vector für uns. Und das ist gut so. Der Vektor lebt "gefühlt" als lokale Variable im automatischen Speicher. Unter der Haube kümmert sich der vector für uns um die Freispeicherverwaltung seiner Elemente.

    Neben std::vector gibt's natürlich auch noch einige andere praktische Datenstrukturen, std::deque, std::map, std::unordered_map, etc.

    Ohne new und mit den Containern aus der Standardbibliothek kommt man schon ziemlich weit! Ich benutze auch fast nix anderes.

    Falls es dann doch nicht reicht, gibt's in <memory> noch etwas was dich interessieren könnte. Da fallen mir aber keine guten Anwendungsbeispiele für ein, gerade. Nur zu Demonstrationszwcken:

    struct ding {
       double x, y;
    
       ding(double x, double y) : x(x), y(y) {}
    };
    
    int main() {
       auto ob = ding(3.1415, 99.0);                   // OK
       auto rp = new ding(55.4, 33.2);                 // Bitte vermeiden!
       auto up = std::make_unique<ding>(123.0,456.0);  // OK
       auto sp = std::make_shared<ding>(2.17,66.6);    // OK
    
       // ob: ding   (das Objekt direkt selbst im Stack lebend)
       // rp: ding*  (roher Zeiger, der in den Freispeicher reinzeigt, sich
       //            aber dafür nicht verantwortlich fühlt)
       // up: unique_ptr<ding>  (fühlt sich als alleiniger Besitzer des im
       //                       im Freispeicher lebenden ding-Objekts)
       // sp: shared_ptr<ding>  (fühlt sich als einer von potentiell mehreren
       //                       Besitzern des Freispeicher Objekts, wo jetzt
       //                       auch in der Nähe ein Referenzzähler dranhängt)
    }
    

    Alle ding-Objekte bis auf das, worauf rp zeigt, werden hier automatisch zerstört und deren Speicher wieder freigegeben.


Anmelden zum Antworten