Frage zur Benutzung von new und delete bei einem Beispiel


  • Mod

    timC++Anfang schrieb:

    Okay, danke SeppJ.
    Die "Korrektur" reicht aber aus, oder ? Ein zusätzliches "Delete" von ptier ist nicht notwendig, weil ich den Speicher schon durch Mammal_Array[2] freigegeben habe, oder ?

    Jain. Es wäre sogar falsch ptier zu deleten, weil man delete nicht doppelt auf ein Objekt anwenden darf. Aber das eigentliche Problem ist, dass hier überhaupt new und delete manuelle vom Programmierer aufgerufen werden. Normalerweise macht man das in C++ so, dass das automatisch erfolgt und sich daher gar nicht erst die Frage stellt, ob man es falsch oder richtig gemacht hat. Das wäre die wahre Lösung für dein Problem.



  • Okay, nochmals Danke SeppJ.
    Du meinst wohl smart pointer,oder ? Mir ging es eigentlich nur um das Verständnis bei new und delete. Was passiert eigentlich mit nicht freigegebenen Speicher bei Beendigung des Programms und was passiert bei "doppelter" Freigabe des Speichers?



  • Nachdem der Prozess beendet ist, steht der Speicher dem Betriebssystem wieder zur Verfügung. Allerdings werden die Destruktoren der Objekte nicht aufgerufen. Hätten die z.B. etwas ausgeben sollen, so geschieht dies nicht.

    Was passiert, wenn Speicher mehrfach freigegeben wird, ist undefiniert. Meistens stürzt das Programm irgendwann ab.



  • manni66 schrieb:

    Nachdem der Prozess beendet ist, steht der Speicher dem Betriebssystem wieder zur Verfügung.

    Das steht nicht fest. Moderne Kernel tun so was. Windows und Linux und BSD bspw. - das muss aber nicht für alle gelten. Ich habe bereits Systeme mit einem Heap für alle Prozesse gesehen, in dem, wenn ein Prozess beendet wurde, der Speicher nicht wieder freigegeben wurde.

    Faustregel: wenn du was reservierst, musst du es auch wieder freigeben.



  • Hallo Tim,

    ich habe Dir das Programm mal in 'modernes ' C++ übersetzt.

    #include <iostream>
    #include <memory>
    #include <array>
    
    class Mammal
    {
    public:
        virtual ~Mammal() =0;   // pure abstract; Instanz von Mammal ist nicht möglich!
        virtual void Speak() const { std::cout << "???" << std::endl; } // Default
    };
    inline Mammal::~Mammal() {}
    
    class Dog : public Mammal { 
    public: 
        void Speak() const override { std::cout << "Wau Wau" << std::endl; } 
    };
    class Cat : public Mammal {
    public: 
        void Speak() const override { std::cout << "Miauuuu" << std::endl; } 
    };
    
    class Any : public Mammal {};
    
    int main()
    {
        std::array< std::unique_ptr< Mammal >, 3 > Mammal_Array; // hier ist man sicher, das alle (Smart-) mit nullptr vorbelegt sind
        for( auto& mammal: Mammal_Array ) // ein for( i=0; i<3; .. wäre redundant zur vorhergehenden Zeile
        {
            std::cout << "(1)Hund (2)Katze: ";
            int choice; // die Variable erst deklarieren, wenn man sie benötigt
            if( (std::cin >> choice).fail() )   // IMMER mit fehlerhafter Eingabe rechnen
            {
                std::cerr << "Fehler bei der Eingabe; vorzeitiger Abbruch" << std::endl;
                break;
            }
            switch( choice )
            {
            case 1:
                mammal = std::make_unique< Dog >();
                break;
            case 2:
                mammal = std::make_unique< Cat >();
                break;
            default:
                mammal = std::make_unique< Any >();
                break;
            }
        }
        for( auto& mammal: Mammal_Array )
        {
            if( mammal )    // rechne mit nicht belegten Pointern
                mammal->Speak();
        } 
        return 0;
    }
    

    Der 'dickste Hund' in Deinem ursprünglichen Beispielprogramm ist die Kombination von nicht initialisierten Pointer-Variablen und der Nichtbeachtung des Fehlerfalls beim Einlesen von choice .
    Im 'wahren Leben' kann das zu Fehlern führen, die sich durch undefiniertes Verhalten auszeichnen und schwer zu finden sind. Oder anders ausgedrückt: Minimale Kundenzufriedenheit bei maximalem Aufwand.

    SeppJ schrieb:

    Faustregel: Wenn new oder delete direkt im Code auftauchen, ist der Code höchstwahrscheinlich nicht gut...

    So isses!

    Wenn Du noch Fragen dazu hast, so frage bitte ...

    Gruß
    Werner


  • Mod

    class Mammal 
    { 
    public: 
        virtual ~Mammal() =0;   // pure abstract; Instanz von Mammal ist nicht möglich! 
        virtual void Speak() const { std::cout << "???" << std::endl; } // Default 
    }; 
    inline Mammal::~Mammal() {}
    

    Warum nicht

    class Mammal 
    { 
    public: 
        virtual ~Mammal() = default;
        virtual void Speak() const = 0;
    };
    


  • SeppJ schrieb:

    Weil das denkbar schlechter Code ist. Nicht abgucken!

    Faustregel: Wenn new oder delete direkt im Code auftauchen, ist der Code höchstwahrscheinlich nicht gut...

    ... oder von jemandem, der sämtliche Sprachmittel genau kennt und weiß, in welcher Situation man welche am besten anwendet statt nur nachzuplappern, was gerade in Mode ist.


  • Mod

    versionsnummer schrieb:

    SeppJ schrieb:

    Weil das denkbar schlechter Code ist. Nicht abgucken!

    Faustregel: Wenn new oder delete direkt im Code auftauchen, ist der Code höchstwahrscheinlich nicht gut...

    ... oder von jemandem, der sämtliche Sprachmittel genau kennt und weiß, in welcher Situation man welche am besten anwendet statt nur nachzuplappern, was gerade in Mode ist.

    Nur wenn Schweine fliegen können. Ob ein bestimmter Code gut oder schlecht ist, hat jedenfalls nichts damit zu tun, wer ihn geschrieben hat.
    Andererseits lässt dein überflüssiger Kommentar sehr wohl auf deine Disposition schliessen.



  • camper schrieb:

    versionsnummer schrieb:

    SeppJ schrieb:

    Weil das denkbar schlechter Code ist. Nicht abgucken!

    Faustregel: Wenn new oder delete direkt im Code auftauchen, ist der Code höchstwahrscheinlich nicht gut...

    ... oder von jemandem, der sämtliche Sprachmittel genau kennt und weiß, in welcher Situation man welche am besten anwendet statt nur nachzuplappern, was gerade in Mode ist.

    Nur wenn Schweine fliegen können. Ob ein bestimmter Code gut oder schlecht ist, hat jedenfalls nichts damit zu tun, wer ihn geschrieben hat.
    Andererseits lässt dein überflüssiger Kommentar sehr wohl auf deine Disposition schliessen.

    Lustigerweise habe ich gerade in einem Code-Vorschlag in einem anderen Thread ein noch unsäglicheres " new std::shared_ptr<> " verwendet,
    das sich in diesem speziellen Fall durchaus nachvollziehbar rechtfertigen lässt 😮

    Die Faustregel, möglichst kein new/delete zu verwenden hat allerdings nichts mit "Mode" zu tun, sondern führt fast immer zu simplerem und weniger
    fehleranfälligem Code. Ich schliesse mich hier also der new/delete -ist-baba-Fraktion an, auch wenn ich einräume, dass es seltene Fälle gibt, wo sie Sinn machen können.

    Finnegan


  • Mod

    Finnegan schrieb:

    auch wenn ich einräume, dass es seltene Fälle gibt, wo sie Sinn machen können.

    Welche Fälle?



  • SeppJ schrieb:

    Finnegan schrieb:

    auch wenn ich einräume, dass es seltene Fälle gibt, wo sie Sinn machen können.

    Welche Fälle?

    Beispielsweise die seltenen Fälle in denen man das Ownership eines Objektes durch ein Interface durchreichen möchte, das keine Smart Pointer versteht -
    z.B. das Shared Ownership eines std::shared_ptr duch ein void* -Interface einer C-API, wo der Shared Pointer dann beispielsweise in einer Callback-Funktion
    freigegeben werden soll.

    Oder wie in dem Beispiel im verlinkten Thread, wo der lockfree-queue nur mit trivialen Datentypen umgehen kann (also keine Smart Pointer im queue, lediglich nackte Pointer),
    in diesem aber dennoch Shared Ownership verwaltet werden soll, damit sigergestellt ist dass das referenzierte Objekt so lange existiert wie noch ein Thread darauf zugreifen könnte.
    Es wird sicher noch andere Beispiele geben, aber wie gesagt, das werden wohl alles Ausnahmefälle sein.

    Ich gebe allerdings zu, dass man auch in den oben genannten Fällen nicht zwangsweise ein new benötigt - ein std::make_unique<T>().release() täte es natürlich auch -
    allerdings macht das bei simplen internen Objekten, deren Konstruktor nicht wirft keinen Unterschied.

    Finnegan


  • Mod

    Finnegan schrieb:

    Beispielsweise die seltenen Fälle in denen man das Ownership eines Objektes durch ein Interface durchreichen möchte, das keine Smart Pointer versteht -
    z.B. das Shared Ownership eines std::shared_ptr duch ein void* -Interface einer C-API, wo der Shared Pointer dann beispielsweise in einer Callback-Funktion
    freigegeben werden soll.

    Uns ist ein unmodern verfasstes Interface gegeben - natürlich macht das den es verwendenden Code entsprechend unmodern. Wenn wir RAII von vornherein konsequent durchgesetzt hätten, wäre auch niemals ein new notwendig gewesen. Genau wie const_cast in modernem C++ prinzipiell keine Daseinsberechtigung hat - nur weil bestimmte legacy APIs einfach keine const correctness haben (außer man implementiert irgend eine Methode zweimal und möchte sich Tipparbeit sparen 😛 ).

    Außerdem sollte nicht einfach new verwendet werden, weil irgendwelche Parameter rohe Zeiger sind.

    foo(new A, new B); // Kann lecken
    
    auto a = std::make_unique<A>();
    auto b = std::make_unique<B>();
    foo(a.release(), b.release()); // Leidet nicht unter dem selben Problem.
    

    Im Übrigen sehe ich nicht ein, weshalb Faustregeln wie diese nicht dogmatisiert werden sollen. Wir hatten genug Neulinge, die sich "passt schon, funzt ja" einreden und dann mit völlig schrottem Code antanzen. Lieber einfach kategorisch ablehnen, alle Ausnahmen sind so spezialisiert, dass sie im Falle des Falles auch hier diskutiert werden können.



  • Arcoth schrieb:

    Uns ist ein unmodern verfasstes Interface gegeben - natürlich macht das den es verwendenden Code entsprechend unmodern. Wenn wir RAII von vornherein konsequent durchgesetzt hätten, wäre auch niemals ein new notwendig gewesen.

    Ich bin mir da nicht so sicher. Wie implementiert man denn ohne new so etwas wie make_unique oder sogar std::vector wo ein reserve() zwar
    den Speicher für die Objekte reserviert, diese aber erst später konstruiert werden? Ich denke zumindest ein placement new wird benötigt -
    oder vielleicht die Möglichkeit den Konstruktor für ein nicht initalisiertes Objekt manuell aufzurufen 😉

    Arcoth schrieb:

    Im Übrigen sehe ich nicht ein, weshalb Faustregeln wie diese nicht dogmatisiert werden sollen. Wir hatten genug Neulinge, die sich "passt schon, funzt ja" einreden und dann mit völlig schrottem Code antanzen. Lieber einfach kategorisch ablehnen, alle Ausnahmen sind so spezialisiert, dass sie im Falle des Falles auch hier diskutiert werden können.

    Nur zur Erinnerung, ich bin nicht derjenige der sich gegen die Faustregeln auflehnt, im Gegenteil, ich hab sie sogar verteidigt (! s.o.).
    Ich hatte nicht vor hier eine Diskussion vom Zaun zu brechen, bei der ich eh auf eurer Seite bin, sondern es ging mir lediglich darum auf ein Beispiel zu verweisen,
    wo ichs zufällig gerade eben erst verwendet hatte. Dort wird via new lediglich der ( noexcept ) Copy Constructor von shared_ptr in einer privaten Methode aufgerufen,
    was man meines erachtens durchas auch mal mit new machen darf, wenn der Code dadurch kompakter wird. Nicht jeder Code ist so generisch dass man mit allem rechnen muss 😉

    Finnegan



  • hi,
    ich möchte mich hiermit bei allen für die Hilfe bedanken. 👍



  • Arcoth schrieb:

    [code="cpp"]
    foo(new A, new B); // Kann lecken

    auto a = std::make_unique<A>();
    auto b = std::make_unique<B>();
    foo(a.release(), b.release()); // Leidet nicht unter dem selben Problem.[/code

    Wo werden denn hier die Ressourcen freigegeben?
    [code="cpp"]class A
    {
    public:
    A::A(){cout << "Constructor\n";}
    A::~A(){cout << "Destructor\n";}
    };

    void foo(A *ptr)
    {
    cout << "Foo\n";
    }

    int main()
    {
    foo(make_unique<A>().release());
    }[/code]
    erzeugt bei mir die Ausgabe:
    Constructor
    Foo 😕


  • Mod

    Finnegan schrieb:

    Arcoth schrieb:

    Uns ist ein unmodern verfasstes Interface gegeben - natürlich macht das den es verwendenden Code entsprechend unmodern. Wenn wir RAII von vornherein konsequent durchgesetzt hätten, wäre auch niemals ein new notwendig gewesen.

    Ich bin mir da nicht so sicher. Wie implementiert man denn ohne new so etwas wie make_unique oder sogar std::vector wo ein reserve() zwar
    den Speicher für die Objekte reserviert, diese aber erst später konstruiert werden? Ich denke zumindest ein placement new wird benötigt -
    oder vielleicht die Möglichkeit den Konstruktor für ein nicht initalisiertes Objekt manuell aufzurufen 😉

    Danke für den Nitpick, aber ich rede hier natürlich von Code außerhalb der Standardbibliothek. 😉

    @Belli: Die Idee war, dass foo Besitz der Objekte ergreift.



  • Ach soooo ...

    foo(new A, new B); // Kann lecken
    

    lecken also für den Fall, dass beim zweiten Konstruktoaufruf was schief geht?!
    So weit hatte ich ursprünglich nicht gedacht ...
    Denn Besitz übernehmen könnte (bzw. müsste) foo schließlich auch so?!



  • ja, und du kannst, wenn es ein funktionsaufruf ist, nicht einmal garantieren, in welcher reihenfolge A und B konstruiert werden. wenn B::B fehlschlägt und nach A::A aufgerufen wird, dann gibt es keine möglichkeit mehr, das A-objekt freizugeben; foo wird auch niemals aufgerufen.



  • Hallo Arcoth,

    Arcoth schrieb:

    class Mammal 
    { 
    public: 
        virtual ~Mammal() =0;   // pure abstract; Instanz von Mammal ist nicht möglich! 
        virtual void Speak() const { std::cout << "???" << std::endl; } // Default 
    }; 
    inline Mammal::~Mammal() {}
    

    Warum nicht

    class Mammal 
    { 
    public: 
        virtual ~Mammal() = default;
        virtual void Speak() const = 0;
    };
    

    zu Speak: aus dem Beispielcode des TE ist meines Erachtens ersichtlich, dass es eine Default-Implementierung für die Methode Speak gibt. Das wollte ich dann genau so machen.
    zu ~Mammal: siehe Scott Meyers 'Effektiv C++' Item 14 "eine Basisklasse soll einen virtuellen Destruktor haben" und noch ein Item (wahrscheinlich aus 'Mehr Effektiv C++'), was ich jetzt nicht wieder finde, was besagt: "Mache Dir keine Instanzen von Basisklassen". Letzeres kann ich aus eigener (schmerzlicher!) Erfahrung bestätigen.

    Gruß
    Werner


Anmelden zum Antworten