Wieso gibt "das" keinen Memoryleak?



  • Ich denke, ein Teil des Problems ist, dass Du (theliquidwave) Arrays nicht 100%ig verstanden hast. Ein Array ist einfach nur eine Aneinanderreihung von mehreren Objekten. Das war's auch schon. Keine weitere Metainformationen (wie Länge), keine Indirektion (Ein Array ist ein Array und keine Zeiger-artige Referenz wie zB in Java).

    Zu den Typen:
    T[] --> Unvollständiger Typ, Array mit unbekannter Größe
    T[5] --> Vollständiger Typ, Array mit bekannter Größe

    Initialisierung (oder "Der Compiler zählt für uns"):

    int blah[] = {2,4,6};
    

    blah ist in Wirklichkeit vom Typ int[3]. Der Typ wird automatisch vom Compiler vervollständigt.

    char blupp[] = "hallo";
    

    blupp ist in Wirklichkeit vom Typ char[6] (inklusive Nullterminierung).

    Der Array-Typ spielt bei Funktionsparametern eine Sonderrolle:

    void foo(int xxx[]);
    

    ist dasselbe wie

    void foo(int *xxx);
    

    da der Compiler (nur bei Funktionsparametern) ein Postfix-[] auf oberster Ebene nach einem Präfix-* übersetzt. Wegen des "array to pointer decay"s, eine recht schnell greifende implizite Konvertierung von Array zum Zeiger des ersten Elements, sieht es so aus, als ob sich ein Array einer Funktion übergeben lässt. In Wirklichkeit wird nur der Zeiger auf das erste Element übertragen. Arrays lassen sich nicht so einfach Kopieren, wie "normale" Typen wie int, double, etc. Daher kannst Du ein Array auch nicht wirklich als Kopie einer Funktion übergeben oder ein Array zurückgeben (es sei denn, es wird in einer Klasse bzw Struktur verpackt).

    Als letztes will ich noch sagen, dass Deine Syntax falsch gewesen wäre. Angenommen, es gäbe Funktionen, die Arrays zurückgeben könnten, dann sähe eine Deklaration so aus

    int funktion()[5];
    

    für eine Funktion, die ein 5-elementiges int-Array zurückgibt. Natürlich muss man, wenn man die Funktion aufrufen will, wissen, die groß der Rückgabetyp ist. Daher müsste die Länge fest sein. Wie gesagt: Arrays sind Arrays und nicht etwas Zeiger-artiges, was eine Objektsammlung nur referenziert. Keine Indirektion! Wenn Du eine Funktion haben willst, die ein "Array" zurückgibt, ohne von der Signatur her zu verraten, wie groß es ist, muss das per Indirektion geschehen. ZB mit einem Vektor:

    vector<int> funktion();
    

    Arrays sind in C und C++ dermaßen "low level" (keine Indirektion, keine weiteren Metainformationen), dass sie nur in wenigen Fällen sinnvoll sind. Dynamische Arrays werden zB schön von std::vector<> gekapselt, so dass man sich um die Speicherverwaltung da weniger Sorgen machen muss.

    HTH,
    SP



  • Interessant und verständlich erklärt, danke!

    Gruß



  • Sebastian Pizer schrieb:

    Dynamische Arrays werden zB schön von std::vector<> gekapselt,

    ...und Arrays mit fixer Länge können mit std::tr1::array abgebildet werden.



  • theliquidwave schrieb:

    Ah, interessant, klingt logisch 😃
    Stimmt das mit dem Scope so, wie ich das beschrieben habe? Ist der Stack langsamer oder schneller als der Heap? Sollte man ihn benutzen oder nicht?

    Gruß

    Ich lege alle meine Objekte auf den Stack, wenn es keinen Grund gibt, sie auf den Heap zu legen. Stackobjekte werden schneller angelegt und werden automatisch gelöscht. Dadurch brauche ich keinen Müllsammler und habe dennoch keine Memoryleaks.

    Die Aussage, dass der Stack begrenzt ist, ist zwar richtig, aber in der Regel nicht wirklich relevant. Wenn ich beispielsweise einen std::vector auf den Stack lege und ihn mit ganz vielen Daten fülle, verbrauche ich dennoch nur ein paar wenige Bytes auf dem Stack. Der std::vector besteht in der Regel lediglich aus ein paar Zeigern, welche den Speicher letzten endes auf dem Heap verwalten. Das ist für den Anwender aber transparent.

    Man sollte sich halt überlegen, welche Objekte man wirklich wie braucht. Beispielsweise einen Puffer kann ich so anlegen:

    char buffer[8192];
    

    Dann habe ich 8k Stack verbraten. Oder aber so:

    std::vector<char> buffer(8192);
    

    Dann brauche ich nur ein paar Bytes Stack. Zudem kann ich die Grösse auch dynamisch festlegen. Beispielsweise kann ich den Wert aus einer Konfigurationsdatei lesen oder ich kann den Puffer nachträglich vergrössern.

    Also die Standardbibliothek bietet da schon sehr viel, um robuste Programme mit wenig Stackverbrauch zu schreiben.



  • tntnet schrieb:

    Beispielsweise einen Puffer kann ich so anlegen:

    char buffer[8192];
    

    Dann habe ich 8k Stack verbraten.

    Vorausgesetzt der Buffer befindet sich innerhalb einer Funktion. Diese 8k werden aber sofort wieder frei, wenn die Funktion verlassen wird. Bei 1MB Stack (Windows-System) sind 8k vernachlässigbar.

    tntnet schrieb:

    Oder aber so:

    std::vector<char> buffer(8192);
    

    Dann brauche ich nur ein paar Bytes Stack. Zudem kann ich die Grösse auch dynamisch festlegen.

    Und vielfach langsamere Zugriffe als bei einem primitven Array. Aber gut, oft ist Tempo nicht das Entscheidende.



  • C++Fan 2010 schrieb:

    tntnet schrieb:

    Oder aber so:

    std::vector<char> buffer(8192);
    

    Dann brauche ich nur ein paar Bytes Stack. Zudem kann ich die Grösse auch dynamisch festlegen.

    Und vielfach langsamere Zugriffe als bei einem primitven Array. Aber gut, oft ist Tempo nicht das Entscheidende.

    Das stimmt nicht. Der Standard ist so formuliert, dass ein std::vector ohne overhead implementierbar ist. Der Zugriff auf einzelne Zeichen mit dem operator[] wird in der Regel vom Compiler optimiert, so dass Zugriff auf std::vector vom Maschinencode identisch mit dem Zugriff auf ein Array ist.

    Sicher ist das Anlegen des vectors langsamer, da hier ja eine Allokation stattfindet. Aber prinzipell ist es kein Unterschied, ob ich einen std::vector oder einen Zeiger mit malloc/free verwende.

    Und wer es nicht glaubt kann ja so etwas machen:

    std::vector<char> bufferv(8192);
    char* buffer = &buffer[0];
    

    Jetzt erklär mir mal, wie der buffer jetzt langsamer sein kann?

    Übrigens: hier ist die Implementierung des operator[] des gcc 4.4.1

    reference
          operator[](size_type __n)
          { return *(this->_M_impl._M_start + __n); }
    

    Wenn man genau hinschaut, sieht man, dass hier lediglich der Zeiger mit einem offset dereferenziert wird.



  • tntnet schrieb:

    Übrigens: hier ist die Implementierung des operator[] des gcc 4.4.1

    reference
          operator[](size_type __n)
          { return *(this->_M_impl._M_start + __n); }
    

    Wenn man genau hinschaut, sieht man, dass hier lediglich der Zeiger mit einem offset dereferenziert wird.

    Im Regelfall sind Zugriffe auf fixe Arrays mit ein, zwei Maschinenbefehlen erledigt. Mit einem std::vector brauchst Du mindestens noch eine Indirektion (Pointerzugriff). Zudem können Allokation und Erweiterung eines vectors scheitern. Wie Du siehst hat alles seine Vor- und Nachteile.



  • C++Fan 2010 schrieb:

    tntnet schrieb:

    Übrigens: hier ist die Implementierung des operator[] des gcc 4.4.1

    reference
          operator[](size_type __n)
          { return *(this->_M_impl._M_start + __n); }
    

    Wenn man genau hinschaut, sieht man, dass hier lediglich der Zeiger mit einem offset dereferenziert wird.

    Im Regelfall sind Zugriffe auf fixe Arrays mit ein, zwei Maschinenbefehlen erledigt. Mit einem std::vector brauchst Du mindestens noch eine Indirektion (Pointerzugriff).

    Wieder falsch.

    Ein Zugriff auf einen std::vector kann genauso effizient sein, wie der Zugriff auf jedes andere Array, solange es keine fixe Adresse hat. Dabei stört nichtmal die unnötige Indirektion über _M_impl sonderlich.
    Da nur globale/statische Variablen eine fixe Adresse haben, und man die normalerweise recht selten verwendet -> std::vector muss Zugriffe nicht bremsen.

    Maximal kann der Compiler sich "ein Register sparen", nämlich wenn er Stack-Arrays direkt über den Stack-Pointer/Frame-Pointer adressiert. Um ein dynamisches Array schnell zu adressieren, müsste er die Startadresse in einem Register halten, was natürlich den umgebenden Code etwas langsamer machen kann.



  • C++Fan 2010 schrieb:

    tntnet schrieb:

    Übrigens: hier ist die Implementierung des operator[] des gcc 4.4.1

    reference
          operator[](size_type __n)
          { return *(this->_M_impl._M_start + __n); }
    

    Wenn man genau hinschaut, sieht man, dass hier lediglich der Zeiger mit einem offset dereferenziert wird.

    Im Regelfall sind Zugriffe auf fixe Arrays mit ein, zwei Maschinenbefehlen erledigt. Mit einem std::vector brauchst Du mindestens noch eine Indirektion (Pointerzugriff). Zudem können Allokation und Erweiterung eines vectors scheitern. Wie Du siehst hat alles seine Vor- und Nachteile.

    Also eine Allokation und Erweiterung eines fixen Arrays kann nicht scheitern, weil es einfach nicht geht. Ist das dann ein Vorteil eines fixen Arrays? Ein Smart ist sicher auch sicherer als ein Ferrari, weil ich damit nicht mit 300 Sachen gegen die Wand fahren kann.

    Und ein fixes Array ist ja auch nur ein Zeiger, welcher genauso eine Indirektion erfordert. Es ist einfach egal, ob ich einen Zeiger verwende oder ob ich das in einer Klasse kapsele und dennoch den Zeiger implizit verwende. Aber so ähnlich hat das hustbaer ja auch schon gesagt.



  • Also eine Allokation und Erweiterung eines fixen Arrays kann nicht scheitern, weil es einfach nicht geht. Ist das dann ein Vorteil eines fixen Arrays?

    Je nach Anwendungsfall, ja.

    Ein Smart ist sicher auch sicherer als ein Ferrari, weil ich damit nicht mit 300 Sachen gegen die Wand fahren kann.

    Wenn man hauptsächlich im Innenbereich von Großstädten fährt, braucht man keine 300 km/h Endgeschwindigkeit, dafür die bessere Parkplatzwahrscheinlichkeit. Wenn ich also tatsächlich kein dynamisches Array brauche, dann verzichte ich gerne auf eine zusätzliche Fehlerquelle.



  • theliquidwave schrieb:

    Das ist dann wohl auch der Grund, warum der Stack innerhalb von großen Schleifen nicht so gerne benutzt wird

    Vor allem bei Rekursionen mit hoher Tiefe kommt man mit dem Stack mitunter schnell in Bedrängnis...



  • Tyrdal schrieb:

    Wenn ich also tatsächlich kein dynamisches Array brauche, dann verzichte ich gerne auf eine zusätzliche Fehlerquelle.

    Und wenn ich eins brauche nehm ich n std::vector<T> und hab das dynamische Array mit automatischer Speicherverwaltung und ohne besagte Fehlerquelle.



  • Nein, mit besagter Fehlerquelle. Größenänderungen haben bei std::vector mit dem Heap zu schaffen und können daher schiefgehen. Ich hab nix gegen vector, aber daß Allokation auf dem Heap vorkommen kann ein Nachteil sein.



  • Tyrdal schrieb:

    Nein, mit besagter Fehlerquelle. Größenänderungen haben bei std::vector mit dem Heap zu schaffen und können daher schiefgehen. Ich hab nix gegen vector, aber daß Allokation auf dem Heap vorkommen kann ein Nachteil sein.

    Was passiert eher: Dass eine Heapallokation schiefgeht und eine handlebare Exception fliegt oder dass einem das Programm um die Ohren fliegt weil man ein vergleichbar großes Array auf dem Stack anlegen wollte?



  • Was ist wenn man die Wahrscheinlichkeit des Schiefgehens auf 0% beschränken möchte und der Stack berechenbar ist?



  • Tyrdal schrieb:

    Wenn ich also tatsächlich kein dynamisches Array brauche, dann verzichte ich gerne auf eine zusätzliche Fehlerquelle.

    Wofür es auch std::tr1::array gibt, womit man die Fehlerquellen im Umgang mit C-Arrays reduziert...



  • Tyrdal schrieb:

    Was ist wenn man die Wahrscheinlichkeit des Schiefgehens auf 0% beschränken möchte und der Stack berechenbar ist?

    Das ist doch der seltene Ausnahmefall. Mann kann sich immer Fälle konstruieren, wo das schlechtere plötzlich das bessere ist. Um beim Auto zu bleiben: früher gab es auch Argumente gegen den Sicherheitsgurt. Da kann man im Notfall dran hängen bleiben und dadurch nicht schnell genug aussteigen.

    Wir diskutieren doch hier gerade den sparsamen Umgang mit dem Stack. Der Stack ist deutlich eingeschränkter als der Heap. Vor allem kann ich auf einen Stacküberlauf eigentlich nicht reagieren. Auf einen std::bad_alloc-Exception sehr wohl. Will ich zuverlässige Programme, sollte ich für irgendwie relevante Datenmengen den Heap bevorzugen.

    Wenn ich keinen dynamischen Puffer brauche, dann nehme ich trotzdem einen std::vector und verändere die Grösse halt nicht.



  • Deswegen schrieb ich ja auch "je nach Anwendungsfall". Richtig lesen hilft.

    Wenn ich keinen dynamischen Puffer brauche, dann nehme ich trotzdem einen std::vector und verändere die Grösse halt nicht.
    

    Kann man machen, finde da aber std::tr1::array passender.



  • Tyrdal schrieb:

    Wenn ich keinen dynamischen Puffer brauche, dann nehme ich trotzdem einen std::vector und verändere die Grösse halt nicht.
    

    Kann man machen, finde da aber std::tr1::array passender.

    Was dann auch wieder eine Entscheidungsfrage zwischen Heap und Stack ist. tr1::arrays liegen IIRC komplett auf dem Stack.



  • pumuckl schrieb:

    tr1::arrays liegen IIRC komplett auf dem Stack.

    Oder besser: Die Elemente sind Subobjekte von tr1::array. Natürlich kann ich auch schreiben

    std::tr1::array<int,10> *p = new std::tr1::array<int,10>;
    delete p;
    

    🙂


Anmelden zum Antworten