Wo lässt man Vektoren leben - Stack oder Heap?



  • Servus,

    als Java-Coder ist für mich diese Speichergedöns ja recht neu. Ich dachte immer auf dem Stack liegt alles was Methodenlokal ist und da räumt C++ automatisch auf. Sehr schön, also packe ich alles auf den Stack, wenn ich den Heap nicht unbedingt brauche.

    In meinem letzten Thread habe ich ausgezeichnete Hinweise erhalten, was bei meinem Attribut vom Typ 'vector<string>' so alles schief lief, darunter hieß es, dass ich im Konstruktur doch mein Attribut 'sequences' vom Typ 'vector<string>' nicht auf dem Heap via new anlegen soll.

    Warum eigentlich, als Attribut ist es doch Teil eines sehr komplexen Objekts, das hat doch seinen Lebensort nach meinem bisherigen Wissen auf dem Heap!

    Was habe ich vergessen?

    Hier geht es übrigens zum alten Thread mit den Codeschnippseln:
    http://www.c-plusplus.net/forum/viewtopic-var-t-is-275315.html



  • Sehr schön, also packe ich alles auf den Stack, wenn ich den Heap nicht unbedingt brauche.

    Genau. 🙂

    Also Du hast einen vector<string> und willst diesen mit new anlegen, weil er Teil eines groesseren Objektes ist? Es ist richtig, dass das Ding im Heapspeicher liegt, aber man sollte auch new und delete vermeiden, wenn es geht, weil Du dann nicht den Speicher selbst aufraeumen musst und weil Du z.B. auch den copy-Constructor und den Zuweisungsoperator (sehr oft) nochmal ueberschreiben musst. Ausserdem hast Du eine weitere Indirektion auf den vector, welche Du dir sparen kannst. Der Zeiger wird schliesslich dereferenziert und Du kommst auf den Inhalt. Hast Du ein Objekt als automatische Variable angelegt, geschieht der Zugriff direkt.

    Du siehst, es gibt viele Nachteile. Und was hast Du damit gewonnen, dass Du den vector durch new anlegst?



  • Ich sehe schon, dass es Nachteile von new und delete gibt. Ich sehe aber keine Option, wie das mit meinem OOP-Verständnis ohne Heap gehen kann. In meinem Header-File stehen doch die Attribute und die Methodenköpfe. Im Cpp-File steht der Konstruktor, der die Attribute aus dem Header-File bestueckt. Ja, wie soll das dann gehen die Attribute Methodenlokal zu bestuecken?

    Oder wird etwa im Headerfile der Vektor schon initialisiert ?! Ich verstehe also immer noch nicht, wie ich ein Attribut methodenlokal sinnmachend bestücken kann?



  • Jay1980 schrieb:

    Ich sehe schon, dass es Nachteile von new und delete gibt. Ich sehe aber keine Option, wie das mit meinem OOP-Verständnis ohne Heap gehen kann. In meinem Header-File stehen doch die Attribute und die Methodenköpfe. Im Cpp-File steht der Konstruktor, der die Attribute aus dem Header-File bestueckt. Ja, wie soll das dann gehen die Attribute Methodenlokal zu bestuecken?

    Oder wird etwa im Headerfile der Vektor schon initialisiert ?! Ich verstehe also immer noch nicht, wie ich ein Attribut methodenlokal sinnmachend bestücken kann?

    class Foo
    {
    public:
       Foo();
    private:
      std::vector<std::string> m_vec;
    };
    
    Foo::Foo()
    {
        m_vec.push_back("Hello World");
        std::string s("Oh my gawd");
        m_vec.push_back(s);
    }
    

    Ohne new und mit Sinn. Du must es einfach nicht machen, Vector wird die Objekte kopieren und selber auf den Heap legen. Hast Du Zeiger, also zu Objekten die im Heap liegen, nimm boost::ptr_vector.
    rya.



  • Zunächst mal: Objekte sind in C++ keine Referenztypen, wie du das von Java gewohnt bist. Wenn du

    struct point {
      int x;
      int y;
    };
    
    // ...
    
    point p
    

    schreibst, liegen an &p zwei Integer relativ direkt hintereinander im Speicher. (Es steht dem Compiler frei, zwischen Membervariablen etwas unbenutzten Speicher einzufügen, sog. Padding, um dem Prozessor die Adressierung zu erleichtern)

    Wenn du dann schreibst

    struct rect {
      point top_left;
      point bottom_right;
    };
    

    liegen genauso zwei point-Objekte relativ direkt hintereinander im Speicher, nicht etwa Verweise darauf.

    Zweitens: RAII

    RAII ist die übliche Methode, in C++ sinnvolle Speicherverwaltung zu betreiben. RAII steht für "Resource Acquisition Is Initialization", und es handelt sich dabei im Grunde um die Idee, Resourcen aller Art sofort bei der Anforderung auf dem Stack zu verankern, damit man sie nicht verliert. In diesem Fall handelt es sich dabei um Heap-Speicher. Nimm folgendes Beispiel:

    #include <cstdlib>
    #include <ctime>
    #include <iostream>
    #include <memory>
    
    struct base {
      virtual void foo() const = 0;
      virtual ~base() { }
    };
    
    struct derived_1 : base {
      virtual void foo() const { std::cout << "foo 1\n"; }
      virtual ~derived_1() { std::cout << "~1\n"; }
    };
    
    struct derived_2 : base {
      virtual void foo() const { std::cout << "foo 2\n"; }
      virtual ~derived_2() { std::cout << "~2\n"; }
    };
    
    base *factory_function(unsigned x) {
      if(x % 2 == 0) return new derived_1();
      return new derived_2();
    }
    
    int main() {
      std::srand(std::time(0));
    
      {
        std::auto_ptr<base> anchor(factory_function(std::rand()));
        anchor->foo();
      }
    
      std::cout << "Main-Ende\n";
    }
    

    Bei std::auto_ptr handelt es sich um eine Klassenvorlage, die Heap-Objekte auf dem Stack (oder wo immer die auto_ptr-Instanz gerade liegt) verankert. Wird ein auto_ptr-Objekt zerstört, zerstört es das Heap-Objekt, das ihm anvertraut wurde gleich mit - in diesem Fall da, wo anchor seinen Geltungsbereich verlässt.

    std::vector ist mit std::auto_ptr insofern vergleichbar, als dass es sich ebenfalls um einen Anker handelt - die Daten, die std::vector verwaltet, liegen nicht im std::vector-Objekt selbst, sondern auf dem Heap. Wird das std::vector-Objekt zerstört, zerstört es die Daten, die es verwaltet. Schreibst du also

    std::vector<std::string> vec;
    

    hast du erstmal nicht viel mehr gemacht, als einen Zeiger und ein bisschen Buchführung auf den Stack zu legen. Schreibst du dann

    vec.resize(20);
    

    hast du 20 std::string-Objekte auf dem Heap, die von vec verwaltet werden. Schreibst du stattdessen

    vec.push_back("foo");
    

    hast du ein std::string-Objekt auf dem Heap, das von vec verwaltet wird und seinerseits einen anderen Speicherbereich auf dem Heap verwaltet, in dem "foo" steht.



  • std::string ein Wrapper der dir die Speicherverwaltung abnimmt. Ich kenn die Interna nicht wirklich aber es läuft in etwa so ab:
    Intern verwaltet ein neu erstellter std::string eine gewisse Menge an HeapSpeicher. Wenn du deinen string zB über append() erweiterst wird intern gecheckt ob das noch Platz hat, bei Bedarf wird neuer Platz in höhe von circa (bisherigeGröße+neuerText)*BonusUmStändigesUmkopierenZuVerhindern(>1.0) angefordert, das bisherige und neue dort rein kopiert und das bisherige freigegeben.

    Bei Klassen macht es Sinn alles was möglich ist auf den Stack zu packen ... es sei denn es handelt sich um "große" Klassen die viel Platz auf dem Stack verbrauchen, dann ist es evtl Sinnvoller die Klassen anders zu bauen oder halt per new/delete im Heap zu allokieren.

    class StackVerschwender {
      int vielSpeicher[1024*1024];                                     // belegt 1024*1024*sizeof(int) STACK
    public: 
      Verschwender() {};
      int * meinIntSpeicher() { return vielSpeicher; }
    };
    
    class StackSparer1 {
      std::vector<int> auchVielSpeicher;              // belegt ein bisserl STACK für Verwaltung des vectors  
    public:
      StackSparer1() { auchVielSpeicher.reserve(1024*1024); }       // reserviert 1024*1024*sizeof(int) HEAP
      int * meinIntSpeicher() { return &(auchVielSpeicher.at(0));}  // nicht nett ;)
    };
    
    class StackSparer2 {
      int * auchVielSpeicher;                                      // belegt sizeof(POINTERtoINT) aufm STACK 
    public:
      StackSparer2() : auchVielSpeicher(new int[1024*1024]) {  }   // belegt 1024*1024*sizeof(int) aufm HEAP 
      ~StackSparer2() { delete [] auchVielSpeicher; }              // räumt den Speicher auch wieder frei
    
      int * meinIntSpeicher() { return auchVielSpeicher;}
    };
    
    class Egomane {
    public:
      Egomane() : aufmHeap( new StackVerschwender() {}           // braucht mächtig viel Platz für irgendwas
      ~Egomane() { delete aufmHeap; }                            // räumt hinter sich auf
    
      StackVerschwender   aufmStack;                                 
      StackVerschwender * aufmHeap;                                  
      StackSparer1        jaWoDenn;
      StackSparer2        naDort;
    };
    


  • Jay1980 schrieb:

    Ich sehe schon, dass es Nachteile von new und delete gibt. Ich sehe aber keine Option, wie das mit meinem OOP-Verständnis ohne Heap gehen kann. In meinem Header-File stehen doch die Attribute und die Methodenköpfe. Im Cpp-File steht der Konstruktor, der die Attribute aus dem Header-File bestueckt. Ja, wie soll das dann gehen die Attribute Methodenlokal zu bestuecken?

    Oder wird etwa im Headerfile der Vektor schon initialisiert ?! Ich verstehe also immer noch nicht, wie ich ein Attribut methodenlokal sinnmachend bestücken kann?

    Eigentlich geht es nicht darum, den Vector nicht auf dem Heap anzulegen, sondern darum, ihn nicht selber explizit auf dem Heap anzulegen (also nicht new/delete zu verwenden). In einer freistehenden Funktion wird der Vector dann auf dem Stack angelegt, als Member innerhalb eines anderen Objekts da, wo der Speicher des Objekts her kommt. Wird das Objekt selber auf dem Heap angelegt, liegt folglich auch der Vector dort.



  • [Korinthenkackerei]
    Im Zusammenhang mit new ist es eigentlich nicht richtig, vom Heap zu sprechen, auch wenn ich selber es viel zu oft mache 😞 .
    In allen guten C++ Büchern ist das Ziel von new der free store (Freispeicher). Herb Sutter unterteilt den Speicher in fünf Bereiche : const data, stack, free store, heap, global/static.
    Einige Compiler können zwar new mit Hilfe von malloc implementieren, heap und free store sind aber nicht das Gleiche.
    [/Korinthenkackerei]



  • Also erstmal zur Terminology: Stack/Heap werden zwar umgangssprachlich gebraucht, sind aber nicht/kaum im C++ Standard zu finden. Da heißt das nämlich - nach der deutschen Übersetzung - "Automatischer Speicher" und "Freispeicher".

    Jay1980 schrieb:

    ...als Java-Coder...

    In meinem letzten Thread habe ich ausgezeichnete Hinweise erhalten, was bei meinem Attribut vom Typ 'vector<string>' so alles schief lief, darunter hieß es, dass ich im Konstruktur doch mein Attribut 'sequences' vom Typ 'vector<string>' nicht auf dem Heap via new anlegen soll.

    Warum eigentlich, als Attribut ist es doch Teil eines sehr komplexen Objekts, das hat doch seinen Lebensort nach meinem bisherigen Wissen auf dem Heap!

    Die Frage ist: Warum willst Du Deinen Vektor per new anlegen, wenn er doch zum Objekt gehört? Das ist ja gerade eine Stärke von C++, dass die Grenzen zwischen den "normalen" Typen (int, double, ...) und benutzerdefinierte Typen (wie zB vector<int>) verwischen. Warum legst Du nicht auch alle ints unt doubles per new an? Das könnte ich genauso fragen.

    Es ist praktisch zwischen folgenden zwei Beziehungen zu unterscheiden:
    1. Objekt A kennt Objekt B.
    2. Objekt A besteht aus/hat Objekt B.

    Ersteres wird typischerweise durch etwas Zeiger-ähnliches modelliert. Zweiteres kann man auch durch etwas Zeiger-ähnliches modellieren. Das ist aber oft sehr ungeschickt. Und manchmal kann man es nicht verhindern. In Deinem Beispiel -- was wohl auch als Beispiel für die 2. Beziehung gilt -- sollte die Klasse so aussehen:

    class MeineKlasse {
      std::vector<int> dings;
      ...
    };
    

    Das befreit Dich automatisch davon, Dich um die Erzeugung / Zerstörung des Vektors zu kümmern. Es erspart Dir auch das Schreiben bzw explizite Abschalten von Kopier-Konstruktor und Zuweisungsoperator. Denn Wenn Du das hier schreibst:

    class MeineKlasse {
      std::vector<int>* dings;
    public:
      MeineKlasse() : dings(new vector<int>) {}  
    };
    

    kann es passieren, dass Du
    - viele Speicherlecks bekommst
    - mehrere MeineKlasse-Objekte auf denselben Vektor verweisen

    Um zurück auf die zwei Beziehungen zu kommen. Ersteres implementiert man typischerweise mit Zeigern. Da kann auch nicht viel schief gehen, ausser, dass ein Objekt zerstört wird, wobei andere Objekte noch Verweise enthalten. Für die zweite Beziehung lohnt es sich, Abstand von Zeigern zu nehmen. So kann auch nicht viel schiefgehen. Es ist auch möglich, eine "hat-ein"-Beziehung über Zeiger zu implementieren. Dann muss man sich aber selbst um Kopierkonstruktor, Zuweisungsoperator und Destruktor kümmern. Das Zeigerelement sollte dann auch zumindest privat sein.


Anmelden zum Antworten