Objekt auf Stack allozieren oder auf Heap?



  • Hallo Leute!

    Ich habe bis jetzt hauptsächlich in C#, Java und C programmiert. Jetzt habe ich vor an der Uni ein Projekt in C++ zu machen und habe eine kleine Frage zum Thema wo man eine Instanz einer Klasse am Besten platziert.

    In C# und Java müssen ja alle Instanzen einer Klasse mit new alloziert werden, in C++ kann innerhalb einer Funktion/Methode eine Instanz einer Klasse auch auf dem Stack erzeugt werden. Was ist in C++ eher üblich, die Allokation auf dem Stack oder auf dem Heap mit new?



  • Wennimmer möglich Stack.
    Wenn Stack mal nicht geht, dann Heap.
    Aber dann obacht aufpassen, daß Du was DU mit new anlegst an anderer Stelle auch wieder mit delete löscht.
    Wenn möglich keine rohen Arrays nehmen, sondern std::vector.



  • volkard schrieb:

    Wenn möglich keine rohen Arrays nehmen, sondern std::vector.

    Wann ist das nicht möglich, mal abgesehen von dem Fall, das man keinen std::vector hat?



  • Gegenfrage: Hast du schonmal new benutzt, wo es nicht anders ging? Dann war es nötig.

    Zum Beispiel, wenn man eine Fabrikmethode hat. Diese gibt einen Zeiger auf ein polymorphes Basisobjekt zurück. Da hast du ohne new keine Chance.



  • Wenn das die Antwort auf meine Frage sein soll, solltest du mal shauen, was ich zitiert hab.



  • ObjectivePro++ schrieb:

    volkard schrieb:

    Wenn möglich keine rohen Arrays nehmen, sondern std::vector.

    Wann ist das nicht möglich, mal abgesehen von dem Fall, das man keinen std::vector hat?

    z.B. an Stellen wo man nur ein kleines Array mit fixer Größe brauch, wo der Heap zuviel Overhead wäre; Wobei dieses Argument auch nur zählt, wenn kein TR1 (mit der Klasse array) möglich ist.



  • ObjectivePro++ schrieb:

    Wenn das die Antwort auf meine Frage sein soll, solltest du mal shauen, was ich zitiert hab.

    Sorry, ich habe wohl noch halb geschlafen 🙂



  • CSharp3000 schrieb:

    Was ist in C++ eher üblich, die Allokation auf dem Stack oder auf dem Heap mit new?

    So, wie Du das formulierst, ist die Antwort ganz einfach. Du erzeugst Objekte im Freispeicher ("Heap") genau dann, wenn Du selbst die Lebenszeit des Objekts kontrollieren willst und Dir das, was die Allozierung im automatische Speicher ("Stack") bietet, nicht reicht. "Riesengroße Objekte" (im Sinne von sizeof) legt man auch ungerne im automatischen Speicher an, weil er typischerweise stark begrenzt ist, im Gegensatz zum Freispeicher.

    Ich halte es auch noch für wichtig auf folgendes hinzuweisen: In C++ setzt man typischerweise Objekte aus anderen Sub-Objekten zusammen:

    class BausteinA {...};
    class BausteinB {...};
    
    class Transmogrifizierer
    {
      BausteinA a;
      BausteinB b;
      ...
    };
    

    statt

    class Transmogrifizierer
    {
      BausteinA * a;
      BausteinB * b;
    };
    

    was natürlich auch möglich ist. Aber im zweiten Fall enthält ein Transmogrifizierer-Objekt nicht die Teilobjekte von Typen BausteinA und BausteinB selbst, sondern nur Zeiger.

    Je nachdem, was Du machen willst, und was für eine Beziehung die einzelnen Objekte haben sollen, wählst Du das eine oder andere. Typischerweise nehmen Umsteiger (Java/C#->C++) den zweiten Ansatz auch für Fälle, in denen der erste viel angebrachter wäre. Das erste kann man sich als "besteht-aus"- bzw "hat-ein"-Beziehung vorstellen. Wenn so ein Objekt als Ganzes kopiert wird, werden auch automatisch die Subobjekte mit kopiert. Wenn so ein Objekt zerstört wird, werden auch automatisch die Subobjekte zerstört. Im zweiten Fall hast Du nur Zeiger als "Subobjekte". So modelliert man typischerweise die "kennt-ein"-Beziehung zwischen Objekten. Diese Zeiger werden auch mit kopiert bzw mit zerstört -- aber nicht das, worauf sie zeigen. Wenn Dir dieses Verhalten also nicht passt, ist das wahrscheinlich der falsche Ansatz. Es gibt aber auch Fälle, in denen man tatsächlich eine "hat-ein"-Beziehung über ein Zeigerelement modelliert. Man muss sich dann aber selbst um die Verwaltung kümmern:

    class foo
    {
      int * dings;
    public:
      foo();
      ~foo();
      foo(foo const& x);
      foo& operator=(foo const& x);
    };
    
    foo::foo()
    : dings(new int)
    {}
    
    foo::~foo()
    {
      delete dings;
    }
    
    foo::foo(foo const& x)
    : dings(new int)
    {
      *dings = *x.dings;
    }
    
    foo& foo::operator=(foo const& x)
    {
      *dings = *x.dings;
      return *this;  
    }
    

    Dies ist nur ein Beispiel. Für diesen Fall ist es natürlich viel praktischer, das int-Objekt direkt als Subobjekt anzulegen, statt sich einen Zeiger zu merken. Damit kann man sich auch den ganzen Verwaltungskram sparen (Kopieren, Zuweisen, Zerstören). Man kommt aber nicht immer drum herum. std::vector wird zB ähnlich implementiert. Und das muss auch so sein. Die Elemente des Vektors gehören logisch gesehen zum Vektor dazu, werden aber nicht direkt im Vektor-Objekt gespeichert sondern separat im Freispeicher angelegt, weil sich die Größe dynamisch ändern können soll.

    Falls Speicher aus irgendwelchen Gründen dynamisch angefordert bzw andere Resourcen akquiriert werden sollen, lohnt es sich, die Verantwortung für das Freigeben/Löschen frühst möglich abzugeben. Das hat u.a. auch etwas mit Ausnahmesicherheit zu tun. Wenn Du Speicher anforderst und später eine Ausnahme fliegt, dann sollte der reservierte Speicher trotzdem freigegeben werden. try/finally braucht man dazu nicht. Dazu sind Destruktoren da. Man kann diese Verantwortung dank selbst definierbarer Kopieroperationen und Destruktoren an Objekte abtreten. Das nennt sich dann "RAII". Die Idee ist, dass man Resourcen an Objekte "bindet", so dass diese Objekte sich um die Verwaltung kümmern. Beispiel:

    class foo {
      boost::scoped_ptr<int> p;
    public:
      foo() : p(new int) {}
      void set(int i) {*p = i;}
      int get() const {return *p;}
    };
    

    Hier muss ich nicht selbst einen Destruktor schreiben. Das Subobjekt p kümmert sich schon um die Freigabe des im Freispeicher angelegten int-Objekts. Und wenn ein foo-Objekt zerstört wird, wird auch automatisch das p-Subobjekt zerstört, welches den delete-Operator auf den intern gespeicherten Zeiger anwendet. Da ein scoped_ptr nicht kopierbar ist, kann der Compiler keine Kopieroperationen für foo selbst generieren. Das ist gut so, denn diese würden eh nicht das richtige tun. Ich muss mich darum also auch nicht kümmern -- es sei denn, das Objekt soll wirklich kopierbar werden. Anderes Beispiel:

    class BloedeMatrix
    {
      double* coeffs;
      int m,n;
    public:
      BloedeMatrix() : coeffs(0), m(0), n(0) {}
      BloedeMatrix(int m, int n) : coeffs(new double[n*m]), m(m), n(n) {}
      BloedeMatrix(BloedeMatrix const& x);
      BloedeMatrix& operator=(BloedeMatrix const& x);
      ~BloedeMatrix() {delete[] coeffs;}
      double& operator()(int i, int j);
      double operator()(int i, int j) const;
      int rows() const {return m;}
      int cols() const {return n;}
    };
    
    class SchlaueMatrix
    {
      std::vector<double> coeffs;
      int m,n;
    public:
      BloedeMatrix() : m(0), n(0) {}
      BloedeMatrix(int m, int n);
      double& operator()(int i, int j);
      double operator()(int i, int j) const;
      int rows() const {return m;}
      int cols() const {return n;}
    };
    

    Bei der "blöden Implementierung" wusste der Autor offensichtlich das RAII Prinzip nicht zu schätzen. Er hat zwar dran gedacht, dass er Kopieroperationen und Destruktor selbst implementieren muss, hätte es aber noch einfacher haben können, indem er statt einem Zeiger einen std::vector als Datenelement benutzt, siehe Implementierung von SchlaueMatrix.

    ... wow ... da ist ja doch so einiges zusammen gekommen ... 😃

    kk



  • Wow, danke für die vielen Antworten!

    Jetzt ist es mir um einiges klarer. Ich hätte allerdings noch eine Zusatzfrage zum Zusammensetzen von Objekten aus Subobjekten:
    Wenn man weiß, dass ein Subobjekt groß ist, wird man es dann direkt in der Klasse deklarieren oder wird man eher nur einen Pointer in der Klasse deklarieren und das Objekt im Konstruktor mit new allozieren?

    Also würde man es eher so machen (und eine Instanz von MeineKlasse eher nicht auf dem Stack erzeugen)

    class GrosserBaustein { ... };
    
    class MeineKlasse
    {
    private:
        GrosserBaustein a;
        ...
    };
    

    oder so (bzw. mit scoped_ptr anstelle eines normalen Pointers)

    class GrosserBaustein { ... };
    
    class MeineKlasse
    {
    private:
        GrosserBaustein *a; // a wird im Konstruktor mit new erzeugt und im Destruktor zerstört
        ...
    };
    


  • Das Beispiel von Dir macht keinen Unterschied. Im Gegenteil. Beispiel 2 macht Dir mehr Arbyte. Ausser Du musst genau kontrollieren wann der Baustein im Destruktor zerstört wird.
    Ein Beispiel wo man es wirklich mit new machen muss, ist wenn man als beispiel eine pure virtual Klasse nimmt.
    Kleines Beispiel aus der Praxis:

    class IRenderer
    {
    public:
       virtual void render() = 0;
    };
    
    class DirectXRenderer : public IRenderer
    {
    public:
       virtual void render() { DoFunkyDXStuff(); }
    };
    
    class OpenGLRenderer : public IRenderer 
    {
    public:
       virtual void render() { DoFunkyOpenGLStuff(); }
    };
    
    class Factory
    {
    public: 
        void init(int type)
        {
          if ( type == USE_RENDERER_DIRECTX)
              m_renderer = new DirectXRenderer();
          else
             m_renderer = new OpenGLRenderer();
        }
    
        ~Factory() 
        {
          delete m_renderer;
        }
    
    private:
      IRenderer* m_renderer;
    
    };
    

    In so einem Fall hast Du keine andere Wahl.
    HTH
    rya.



  • CSharp3000 schrieb:

    Wenn man weiß, dass ein Subobjekt groß ist, wird man es dann direkt in der Klasse deklarieren oder wird man eher nur einen Pointer in der Klasse deklarieren und das Objekt im Konstruktor mit new allozieren?

    Kann man so allgemein nicht sagen. Erstmal halte ich das Beispiel für sehr weit hergeholt. Wie groß ist denn groß?! Dann kommt es noch drauf an, wie Du die Klasse benutzen willst. Baust Du das Subobjekt direkt ein gilt sizeof(GrosserBaustein)<=sizeof(MeineKlasse). Das hat natürlich Implikationen. Man würde ein MeineKlasse-Objekt ungern im automatischen Speicherbereich anlegen wollen, weil's dort dann auch viel Speicher frisst, von dem es nicht allzuviel gibt. Aber man kann es natürlich trotzdem noch dynamisch anlegen:

    void foo() {
      ...
      const boost::scoped_ptr<MeineKlasse> mksptr (new MeineKlasse);
      MeineKlasse & meinObjekt = *mksptr;
      ...
    }
    

    Und dann so tun, als wäre "meinObjekt" ein im automatischen Speicher lebendes Objekt. Die Lebenszeit ist dieselbe, das Objekt sitzt in Wirklichkeit nur im Freispeicher.

    Im Zuge von "C++0x" (kommender C++ Standard) ist aber auch ein Zeiger-artiges Element nicht schlecht. Angenommen, Objekte des Typs GrosserBaustein sind nicht oder nur unter hohem Aufwand kopierbar, Beispiel:

    class GrosserBaustein
    {
    public:
      int big_fat_array[1000000];
    };
    

    dann kann man es mit C++0x Mitteln immerhin erreichen, Objekte des Typs MeineKlasse effizient "bewegbar" zu machen, also, dass man sie effizient von einem Ort zu einem anderen Ort im Speicher bewegen kann (keine Kopie, sondern ein "Umziehen"):

    class MeineKlasse
    {
      std::unique_ptr<GrosserBaustein> gb;
    public:
      MeineKlasse()
      : gb(new GrosserBaustein)
      {}
      MeineKlasse(MeineKlasse&&) = default;
      MeineKlasse& operator=(MeineKlasse&&) = default;
    };
    

    std::unique_ptr und die =default-Syntax gehören noch nicht zum offiziellen Standard. Aber so könnte ein C++0x Programm aussehen. std::unique_ptr heißt so, weil ein solches Objekt alleiniger Besitzer einer Resource ist (unique ownership) Und "Ownership" impliziert hier wieder die Verantwortung für die Freigabe. Wenn ein MeineKlasse-Objekt zerstört wird, wird auch der unique_ptr zerstört (da ein Teil davon) und weil dieser sich für das, worauf er zeigt, verantwortlich fühlt, wird auch das GrosserBaustein-Objekt gelöscht. Im Prinzip also so ähnlich wie boost::scoped_ptr, unterstützt aber ein paar Operationen mehr. ZB kann man den Zeiger wieder aus dem Objekt herauslösen, ohne dass es automatisch gelöscht wird. Ein unique_ptr ist auch "bewegbar". Die =default-Syntax hier soll den Compiler dazu veranlassen, einen sogenannten Move-Constructor und einen Move-Assignment-Operator zu erzeugen. Damit kannst Du dann zB solche Dinge schreiben:

    MeineKlasse erzeugmal()
    {
      MeineKlasse dings;
      ...
      return dings;
    }
    
    int main() {
      MeineKlasse o = erzeugmal();
    }
    

    Dadurch, dass das Objekt nur einen Zeiger enthält (unique_ptr) ist es schön kompakt und klein. Man kann es wegen den Move-Operationen schnell durch die Gegend schieben, ohne dass irgendetwas aufwändig kopiert werden muss. Von daher hat ein Zeiger-Element natürlich seine Vorteile.

    Genauso gut hätte ich hier aber auch einfach std::vector als Datenelement für MeineKlasse nehmen können, statt einen unique_ptr, der auf ein dickes Array zeigt. Das ist nämlich fast dasselbe (Speicherlayout-technisch) mit dem Vorteil, dass std::vector eine schönere und flexiblere Schnittstelle hat. 🙂

    Wenn Du Laufzeitpolymorphie brauchst, kommst Du um "Indirektionen" (also Zeiger/Referenzen) nicht herum. Spätestens dann, brauchst Du sie.

    kk



  • Danke an beide für eure Antworten.
    Ich werde dann sehen wie mein C++ Projekt läuft. Wenn ich irgendwo fest hängen sollte werde ich mich vielleicht wieder im Forum melden.


Anmelden zum Antworten