Richtige verwendung von new in C++



  • Hallo,

    viele bzw. alle Bücher über C++ die ich kenne, geben einen grobe Einführung in die Sprache und erklären die Verwendung von new & Co. ausführlich. Leider schweigen alle Werke über den sinnvolle Einsatz. Es heißt meistens "große Objekte" und "Objekte, die langlebig sind" werden mit new erstellt.
    Doch wie sieht das in der Praxis aus? Wer ist für die Freigabe (Erzeuger, Besitzer, aktuelle Verwender, ...?) zuständig? Oder kann man hierüber keine generelle Aussage treffen?

    Gut, wenn ich z.B. ein Bild lade, dann lege ich den Platz im Heap an und lade es hinein. Benötige ich es nicht mehr --> delete. Doch wie sieht es mit größeren Objekte während der Bearbeitung aus? Ein Beispiel könnte man in der numerischen Integration suchen. Hier wird durchaus sehr viel Speicher benötigt, aber dieser lässt sich schlecht abschätzen und viele Klassen sind davon bedroffen. z.B. eine Klasse "Rational", "Polynom", ...

    Gibt es im Netz ein gutes "Beispiels-Projekt", wo man sieht, wie dies in der Praxis umgesetzt wird?
    Oder jemand von euch etwas Zeit und könnte das eine Hobby-Prog. das etwas näher bringen?

    Gruß,
    Thomas



  • generelle aussagen dazu zu treffen, sind schwer. es kommt fast immer auf den
    kontext an.

    zum einen, die größe. der stack ist begrenzt und nimmt sicher kein 10MB foto auf.
    bei kleinen typen oder build-in typen macht new kaum sinn. dafür ist der stack
    da.

    new ist dynamisch, wenn du dagegen auf dem stack speicher anlegst, musst du
    die größe vorher festlegen. in 90% der fälle verschenkt du speicher, weil du
    zu viel angelegt hast, und in 10% stürzt das programm ab, weil der platz
    doch mal nicht ausreichte. bei bildern, oder allgemein dateien, die variable
    größe haben -> new. für eine immer_64_byte_große_config_datei reicht auch der stack.

    sehr wichtig für diese entscheidung ist auch der scope. wenn ich das bild nach
    dem laden noch weiter brauche, spricht das für new. allgemein geben lade-funktionen
    meist ein heap-pointer zurück.

    für z.b. polymorphie macht new auch bei kleinen klassen sinn, oder wenn die
    anzahl oder typ der klassen vorher nicht feststeht.

    die freigabe ist so eine sache, entweder man schreibt zur lade-funktion noch
    eine destroy-funktion, oder man baut eine "Release" methode in das zurück-
    gegebene objekt ein (z.b. COM). in diesen fällen ist der benutzer für das
    freigeben verantwortlich. macht man eine manager-oberklasse, kann man dieser
    auch das aufräumen überlassen.

    meist verwendet man aber keine rohen zeiger, die man sich bei new abholt,
    sondern stl konstrukte wie std::vector oder std::list. für zeiger gibt es
    von boost smartpointer. dann muss man sich kaumnoch ums löschen kümmern.

    dein beispiel mit der numerischen integration würde ich mit new, bzw mit
    std::vector umsetzen, da viele objekte vom selben typ zusammenkommen.



  • Hmm. Ich denke man kann die Frage in der Praxis nicht so allgemein beantworten. Die einen machen es so, die anderen so. Man kann den Speicher "von Hand" anfordern, freigeben oder man lässt z.B das löschen einem Smart Pointer.

    Wenn man einen grösseren Speicherblock lediglich für eine Funktion, respektive Berechnung braucht, dann wird man da den Speicher holen und dann am Ende einfach wieder freigeben. Man kann das von Hand tun, oder wie gesagt einen Smart Pointer tun lassen, was halt gerade die Anforderungen besser erfüllt.

    Mit langlebig ist gemeint, dass man im Vornherein nicht weiss, wie lange ein Objekt lebt, respektive in welchem Scope es sich befindet. (Bei einer Berechnung wäre das ja meist recht klar, aber da es halt ein grosses Obejekt ist, holt man es halt dynamisch, um den Stack nicht zu überstrapazieren).



  • Ich gebe Dir mal ein Beispiel aus meinem aktuellen Projekt. Es handelt sich dabei um Framework für ein Spiel.
    Ich habe eine Klasse ResourceManager. Diese lädt alle Resourcen in Gruppen entweder aus XML oder über Klassenaufrufe. Wenn ich jetzt eine Gruppe anlege, dann könnte ich einfach ein Objekt mit objekt a erstellen und dieses in meine ResourcenListe kopieren. Das geht aber nicht so einfach, da im Destruktor dieser Objekte ( es handelt sich um selbst angelegte Container für Objekte aus OpenGL ) die Resourcen wieder zerstört werden. Also muss ich verhindern dass dieses Objekt zerstört wird wenn ich meine Lade-Funktion verlasse. Das mache ich mit new. Denn im Gegensatz zu "normal" angelegten Objekten lebt dieses Objekt weiter bis ich es mit delete zerstöre. Das passiert dann beim entladen einer Resourcengruppe oder beim zerstören des Resourcenmanagers was wiederrum von einem Controller gesteuert wird, damit die Objekte freigegeben werden bevor OpenGL geschlossen wurde.
    Ich hoffe das war verständlich und ich möchte dir als Beispiel noch einen Codeauszug präsentieren:

    // Texturlader
    NLITexture* NLTextureLoader::getTexture(const char* name, const u8* data, u32 size)
    {
        if ( !glIsEnabled(GL_TEXTURE_2D) ) {
            glEnable(GL_TEXTURE_2D);
        }
        corona::File* file = NULL;
        corona::Image* img = NULL;
    
        // Load the image from memory
        file = corona::CreateMemoryFile(data, size);
        img = corona::OpenImage(file, corona::PF_DONTCARE, corona::FF_AUTODETECT);
        if (!img)
        {
            NLError(std::string("Cannot load Image '") + name + std::string("'."));
            return NULL;
        }
        u32 id = this->makeGLTexture(img->getPixels(), img->getWidth(), img->getHeight(), getColorsFromFormat(img->getFormat()));
        delete img;
    
        // Hier wird das handle angelegt und mit new alloziert. Nach der Rückgabe
        // des Texturhandles ist dieses weiterhin vorhanden und wird im Resourcen-
        // Manager zerstört bei Bedarf.
        NLITexture *handle = new NLTexture(name, id);        
        return handle;
    }
    


  • new wird für eine dynamische statt einer statischen Speicherbelegung, z.B. für einen grossen Array, eingesetzt. Lohnen tut sich dann, wenn man erst während der Laufzeit des Programmes weiss, wieviel man konkret braucht. Sobald man den angeforderten Speicher nicht mehr braucht, gibt man ihn mit delete wieder frei. Sinnvoll ist das aber nur, wenn man es mit einem wirklich grösseren Speicherbedarf zu tun haben kann.



  • Stell dir doch mal vor, du hast ein Spiel.

    Jedes Objekt im Spiel ist auch in deinem Code ein Objekt.

    Jetzt taucht ein neues Objekt auf.
    Wie realisierst du es?

    Genau, du erstellst mit new ein neues Objekt und speicherst die Addresse in deiner Objektliste.



  • Icematix schrieb:

    Stell dir doch mal vor, du hast ein Spiel.

    Jedes Objekt im Spiel ist auch in deinem Code ein Objekt.

    Jetzt taucht ein neues Objekt auf.
    Wie realisierst du es?

    Genau, du erstellst mit new ein neues Objekt und speicherst die Addresse in deiner Objektliste.

    Das ist sicher richtig. Doch das versteht der Fragesteller nicht. Würde er es verstehen, hätte er mit new / delete keine Probleme und hätte nie gefragt!



  • Hallo,

    danke für eure Beiträge. Endlich ist dank eurer Hilfe etwas Licht im Wald zu sehen.

    Eines hätte ich da noch anzumerken. Die Funktion von Scorcher24 gibt ein Image zurück, das im Heap liegt.

    img = corona::OpenImage(file, corona::PF_DONTCARE, corona::FF_AUTODETECT);
    //..
    delete img;
    

    Woher weiß man, ob das Objekt im Heap liegt oder im Stack. Das Objekt könnte doch auch von der Klasse verwaltet werden.

    Gut bei Image ist es wohl klar, aber bei anderen Funktionen vielleicht nicht zu 100%. Sollten die Funktionen mit gewisse Schlüsselwörter wie "open", "load", ... beginnen. Oder muss in der Doku der Return Type + Speicherplatz explizit angegeben werden. Letzteres führt meiner Meinung nach sehr leicht zu Problemen.

    Etwas Offtopic, aber trotzdem von sehr hohem Interesse. Bei pthread ist es möglich, bei pthread_create der auszuführenden Funktion Parameter zu übergeben. Für mehrere Parameter benutzt man eine struct. So weit, so gut. Ich frage mich, kann man das vielleicht auch etwas anders umsetzen. Und zwar ohne struct, damit folgendes möglich wird.

    class A {
    
      string val;
    
    public:
      A(string s) : val(s) { };
    }
    
    class B {
    
      string val, val2;
    
    public:
      B(string s, string ss) : val(s), val2(ss) { };
    }
    
    class C {
    
      string val;
      int valInt
    
    public:
      A(int i, string s) : val(s) valInt(i) { };
    }
    
    template<class T>
    T* createObject( ... )
    {
      return new T(/** Was auch immer, nichts funktioniert **/);
    }
    
    int main(int argc, const char* argv[]) {
      A* aObj = createObject<A>("abc");
      B* bObj = createObject<B>("abc", "ssDef");
      C* cObj = createObject<C>(154, "abc");
    }
    

    Das größte Problem ist die variable Anzahl und Typen. C++ bietet hierzu zu wenige Möglichkeiten. Hilft hier vielleicht Boost?

    Warum so etwas? Sagen wir mal, darüber zerbreche ich mir schon seit Jahren den Kopf und komme nicht drauf. Eine große Last würde abfallen 🙂

    Gruß,
    Thomas



  • Eines hätte ich da noch anzumerken. Die Funktion von Scorcher24 gibt ein Image zurück, das im Heap liegt.

    img = corona::OpenImage(file, corona::PF_DONTCARE, corona::FF_AUTODETECT);
    //..
    delete img;
    

    Woher weiß man, ob das Objekt im Heap liegt oder im Stack. Das Objekt könnte doch auch von der Klasse verwaltet werden.

    Das erfährt man bei der Nutzung der Dokumentation von corona, einer Imagelibrary. Diese gibt einen Pointer zurück, den man selbst wieder zerstören muss. Nicht gerade aktueller Standard, ich weiss, aber die Lib ist ein wenig betagt.
    Wichtig war mir eigentlich der Code am Ende, an dem ich das Textur Objekt erzeuge und zurückgeben zur weiteren Verwaltung.
    Aber bei Libraries erfährt man sowas aus der Doku bzw sollte man. Wenn nicht, ist die Doku schlecht :).

    /edit
    Ein wichtiger Grundsatz noch:
    Ein new erfordert delete.
    Ein new [] erfordert delete [].
    🙂

    rya.



  • Wenn nicht, ist die Doku schlecht :).

    Die gibt es wohl wie Sand am Meer.

    /edit
    Ein wichtiger Grundsatz noch:
    Ein new erfordert delete.
    Ein new [] erfordert delete [].
    🙂

    Klar, spielst du auf den zweiten Beitrag an? Gut, aber darum geht es mir nicht. Gibt es ein Möglichkeit, das zu lösen?



  • Hallo Siassei,

    bzgl. deiner Offtopic-Frage:

    Im aktuellen C++ Standard gibt es nur die Möglichkeit, dafür verschiedene (überladene) Template-Funktionen anzubieten, d.h.

    template<class T>
    T* createObject()
    {
      return new T();
    } 
    
    template<class T, typename P1>
    T* createObject(const P1 &p1)
    {
      return new T(p1);
    } 
    
    template<class T, typename P1, typename P2>
    T* createObject(const P1 &p1, const P2 &p2)
    {
      return new T(p1, p2);
    } 
    
    ...
    
    // mittels BOOST_PP kamm man zwar die Schreibarbeit etwas verkürzen, aber dann wird es m.E. noch unübersichtlicher
    

    Im nächsten C++ Standard C++0x wird es dann die Vereinfachung über 'variadic templates' geben.



  • Im nächsten C++ Standard C++0x wird es dann die Vereinfachung über 'variadic templates' geben.

    Stimmt, da war doch was. Geil, und gcc 4.4 unterstützt das bereits 🙂

    Danke, Thomas



  • Falls Du noch nicht rausgefunden hast, wie so etwas in C++0x aussehen würde:

    #include <iostream>
    #include <utility> // std::forward
    #include <memory>  // std::unique_ptr
    
    template<class T, class... Args>
    inline std::unique_ptr<T> make_unique(Args&&...args)
    {
       return { new T(std::forward<Args>(args)...) };
    }
    
    int main()
    {
      auto upi = make_unique<int>(42);
      std::cout << *upi << '\n';
    }
    

    ...sollte gehen. Lass mich mal zählen, wieviele C++0x Neuigkeiten bei dem Beispiel enthalten sind ... Variadische Templates, std::unique_ptr, Rvalue-Referenzen, die neue Initialisierungs-Syntax und Typinferenz (auto), also 5.

    😃

    Gruß,
    SP



  • Danke Sebastian für deinen Beitrag, ehrlich gesagt hatte ich bis jetzt noch keine Zeit, das Studium fordert zur Zeit seinen Tribut 🙂

    Mal sehen, std::forward leitet die rValue weiter und es wird der Move-Konstruktor von T aufgerufen. Was ist, wenn es keinen Move-Operator gibt, wird dann der Copy-Konstruktor aufgerufen?
    unique_ptr dient zum Verwalten des rohen Zeigers. Mhh, diesen kann ich nicht per rValue zurück geben, da der unique_ptr beim verlassen der Methode nicht mehr existiert.

    Habe ich da was falsch verstanden? Ich frage nach, da das ein Hobby von mir ist und ich die neuen Funktionen noch nicht zu 100% begreife.

    Gruß,
    Thomas



  • Siassei schrieb:

    Mal sehen, std::forward leitet die rValue weiter und es wird der Move-Konstruktor von T aufgerufen.

    std::forward = generell weiterleiten, ob lvalue oder rvalue ist egal. Es ist notwendig, da benannte Rvalue-Referenzen nach der Initialisierung wie Lvalues behandelt werden. Diese Details sind aber wohl off-topic. Ich konnte es nur nicht lassen, dieses kleine C++0x Beispiel abzuschicken. 🙂

    Siassei schrieb:

    Was ist, wenn es keinen Move-Operator gibt, wird dann der Copy-Konstruktor aufgerufen?

    Genau.

    Siassei schrieb:

    unique_ptr dient zum Verwalten des rohen Zeigers. Mhh, diesen kann ich nicht per rValue zurück geben, da der unique_ptr beim verlassen der Methode nicht mehr existiert.

    Doch, und genau das passiert ja auch im dem Beispiel. Du kannst einen unique_ptr nicht kopieren, aber dafür "umziehen lassen". Hier wird nur ein int erzeugt und am ende von main automatisch (im Destruktor von upi ) wieder freigegeben.

    Gruß,
    SP


Log in to reply