johnny: C++ und der Heap

Hi,

ich zerbreche mir seit neuestem den Kopf über folgendes Problem. Das Problem taucht in einer meiner Projekte öfter auf und ich weis nicht wirklich wie ich das lösen könnte.

Also erst mal zur Veranschaulichung folgendes Beispiel:

  
char* getXYZ(unsigned long* ul, const char* cc)  
{  
 // suche irgendwas  
 // ... code ...  
 // suche noch immer  
 // nochmal ... code ...  
  
 // Doch dann plötzlich folgendes Konstrukt:  
 unsigned long length = 1001;  
  
 char* hans = new char[length];  
  
 strncpy(hans, "Hallo Welt .... Auf Wiedersehen\r\n\0", 1001);  
  
 return hans;  
}  

Ich denke auf den Ersten blick sieht das wie eine ganz normale Klassenmethode.
Aber das Problem ist eigentlich nur am Schluss.
Dort wird auf dem Heap ein Array mit 1001 Elementen freigeschaufelt und hans zugewiesen.
Dann bekommt hans noch ein bisschen Inhalt und Schließlich wird hans zurückgegeben bzw. seine Adresse.

Doch nachdem hans zurückgab existiert er noch immer.
Seine Variable wird schon lange nicht mehr vom Programm benötigt.
Er existiert noch immer, (das Schwein! Verzeihung.).
Oder etwa doch nicht?

Wenn hans auf dem Stack wäre würde er doch wieder freigegeben werden? Aber um den Stack muss sich der Programmierer kümmern, also ich.
Aber auf dem Stack kann ich ihn nicht werfen da die Größe wahnsinnig Variabel ist von 1 Zeichen bis hin zu 10000.
Oder soll ich die Funktion alloca() verwenden?
Aber soviel ich weis ist die ein wenig langsamer wie new?
Oder bin ich schon wieder falsch?

Wenn dem allem nicht so ist und hans doch freigegeben wird, ist mein Posting nicht mehr weiter relevant und somit als erledigt anzusehen.

Eine Möglichkeit die mir einfallen würde, wäre hans nach dem Funktionsaufruf wenn sein Inhalt weiterverarbeitet wurde freizugeben.

Beispiel:

  
  
char* sepp = getXYZ(pointer1, pointer2);  
  
printf("%s\n", sepp); // quasi verarbeitung  
  
if(sepp != NULL) {  
 delete[] sepp;  
 sepp = NULL;  
}  
  
// Alles in Ordung! Sepp und/bzw. Hans wurden gelyncht!  
  

Johnny

  1. Hallo,

    Ich denke auf den Ersten blick sieht das wie eine ganz normale Klassenmethode.

    Naja, ohne Klasse drum herum sieht's eher aus wie eine normale Funktion. ;-)

    Dort wird auf dem Heap ein Array mit 1001 Elementen freigeschaufelt und hans zugewiesen.
    Dann bekommt hans noch ein bisschen Inhalt und Schließlich wird hans zurückgegeben bzw. seine Adresse.

    Ja (mehr oder weniger).

    Doch nachdem hans zurückgab existiert er noch immer.

    Nein. Der Speicherbereich, auf den "hans" in der Methode gezeigt hat, existiert noch immer, die Variable "hans" existiert nicht mehr.

    Was ist denn genau ein Zeiger? Ein Zeiger ist nichts anderes als eine Zahl, der die Programmiersprache eine besondere Bedeutung zuweist. In C/C++ ist es sogar problemlos möglich, Zeiger in Zahlen umzuwandeln und umgekehrt, Beispiel:

    #include <iostream>  
    #include <string.h>  
      
    using namespace std;  
      
    int main (int argc, char **argv) {  
       char *zeiger = new char[6];  
       strcpy (zeiger, "hallo");  
       cout << (unsigned long long) (zeiger) << endl;  
       return 0;  
    }
    

    Der Code wandelt Dir den Zeiger "zeiger" in eine Zahl um. Was ist nun diese Zahl? Der Ort der Zahl im Speicher des Programms.

    Was macht nun new in C++ bzw. malloc in C? Sie reservieren soundsoviele Bytes Speicher im Heap und geben die Adresse dieses Bereichs zurück.

    Die Variable des Zeigers (in Deinem Fall "hans") ist für den Compiler selbst vergleichbar mit einer Integer-Variable. Wenn Du nun also den Zeiger zurückgibst:

    Seine Variable wird schon lange nicht mehr vom Programm benötigt.
    Er existiert noch immer, [...] Oder etwa doch nicht?

    Stell Dir vor, Du hättest folgenden Code:

    int doppelte (int zahl) {  
      int ergebnis = zahl * 2;  
      return ergebnis;  
    }
    

    Hier weist Du der Variable "ergebnis" eine Zahl zu und gibst diese zurück. Die Variable "ergebnis" existiert danach nicht mehr, der WERT, den sie hatte, jedoch weiterhin.

    Das gleiche passiert, wenn Du einen Zeiger zurückgibst: Die Zahl, d.h. die Adresse des Speicherbereichs, wird ganz analog zu einer int-Variable zurückgewiesen.

    Der Speicherbereich selbst ist auf dem Heap angelegt worden, also gibt es keinen automatisierten Mechanismus, den Speicherbereich wieder loszuwerden, daher bleibt der auch NACH Ausführung der Funktion erhalten.

    Wenn hans auf dem Stack wäre würde er doch wieder freigegeben werden?

    Ja. Das erreichst Du z.B. so:

    char hans[length];

    Wenn Du den jetzt zurückgibst, wird Dein Programm gehörig auf die Schnauze fliegen, weil Du einen Zeiger auf einen Speicherbereich zurückgibst, der ungültig ist.

    Aber um den Stack muss sich der Programmierer kümmern, also ich.

    Du meinst den Heap, oder? Um den Stack kümmert sich in C der Compiler von selbst, d.h. wenn Du eine Funktion aufrufst, passiert dies oder jenes. Und in Assembler musst Du Dich um ALLES selbst kümmern. ;-)

    Aber auf dem Stack kann ich ihn nicht werfen da die Größe wahnsinnig Variabel ist von 1 Zeichen bis hin zu 10000.

    Ich verstehe Dein konkretes Problem nicht?

    Oder soll ich die Funktion alloca() verwenden?

    Nein! Das ist genauso, als ob Du obiges char hans[length]; machen würdest! Wenn Du dann einen Zeiger darauf zurückgibst, ist der nicht mehr gültig nach Beendigung der Funktion! Ferner: alloca() steht nicht auf jedem System zur Verfügung und in der Dokumentation derselben Funktion wird explizit davor gewarnt, die einzusetzen, außer man weiß genau, was man tut. Ich würde behaupten, dass man bei normalen Programmen NIE über Notwendigkeit für alloca() stolpert.

    Aber soviel ich weis ist die ein wenig langsamer wie new?

    Ob alloca() langsamer oder schneller als new ist, weiß ich nicht. Aber alloca() würde ich an Deiner Stelle nicht verwenden. Der EINZIGE sinnvolle Anwendungszweck von alloca() ist, einen Speicherbereich zu reservieren, der DEFINITIV nur für die Laufzeit der Funktion gelten soll und nie länger. Und der einzige Grund, der mir einfiele, warum man nicht einfach trotzdem den Heap nehmen kann und vor dem Funktionsende einfach free / delete machen kann, ist wenn man in C innerhalb der Funktion eine weitere Funktion aufruft, die per longjmp() auf eine per setjmp() festgelegte Stelle springt, die in einer aufrufenden Funktion liegt und man damit keine Kontrolle über das Ende der Funktion hat. Allerdings: setjmp()/longjmp() haben nur SEHR WENIGE sinnvolle Anwendungsmöglichkeiten (ich habe die noch nie gebraucht, obwohl ich C schon EINIGE Zeit lang mache) und mich würde es schon arg wundern, wenn Du die tatsächlich sinnvoll (!) in Deinem Code einsetzen könntest - zumal ich keine Ahnung habe, wie sich longjmp() auf C++ auswirkt (es wurde ja für C konzipiert).

    Wenn dem allem nicht so ist und hans doch freigegeben wird, ist mein Posting nicht mehr weiter relevant und somit als erledigt anzusehen.

    Du musst hier klar unterscheiden: Zwischen der Variable, die die Adresse des Speicherbereichs enthält und dem Speicherbereich selbst. Die Variable, die die Adresse enthält, ist nicht mehr da nach dem Ende der Funktion, der Speicherbereich selbst bleibt erhalten, falls der Speicherbereich auf dem Heap alloziert wurde und wird "zerstört", falls er auf dem Stack alloziert wurde. Wenn Du den NACH dem Ende der Funktion weiterverarbeite willst, MUSST Du ihn daher auf dem Heap allozieren - und Dich dann manuell um die Aufräumarbeiten kümmern, sobald Du ihn nicht mehr brauchst.

    Eine Möglichkeit die mir einfallen würde, wäre hans nach dem Funktionsaufruf wenn sein Inhalt weiterverarbeitet wurde freizugeben.

    Beispiel:
    [...]

    Das Beispiel ist richtig: So müsstest Du das machen.

    Viele Grüße,
    Christian

    1. Hallo,

      Vielen Dank für die Erklärungen, welche aber nicht nötig gewesen wären.
      Ich wollte mein Problem in den Vordergrund stellen und habe dabei die Exakte Ausdrucksweise in den Hintergrund gestellt(Es musste ja auch schnell gehen ...). Trotzdem Vielen Dank.

      Die Variable, die die Adresse enthält, ist nicht mehr da nach dem Ende der Funktion, der Speicherbereich selbst bleibt erhalten, falls der Speicherbereich auf dem Heap alloziert wurde und wird "zerstört", falls er auf dem Stack alloziert wurde. Wenn Du den NACH dem Ende der Funktion weiterverarbeite willst, MUSST Du ihn daher auf dem Heap allozieren - und Dich dann manuell um die Aufräumarbeiten kümmern, sobald Du ihn nicht mehr brauchst.

      Eine Möglichkeit die mir einfallen würde, wäre hans nach dem Funktionsaufruf wenn sein Inhalt weiterverarbeitet wurde freizugeben.

      Beispiel:
      [...]

      Das Beispiel ist richtig: So müsstest Du das machen.

      Wenn ich für das ganze jetzt eine Klasse schreibe die all das Automatisch erledigt?

        
      class Heap {  
      private:  
       char* str;  
      public:  
       Heap() { str = NULL; }  
       ~Heap() { if(str != NULL) { delete[] str; str = NULL } }  
       void set(char* str) {}  
       char* get(void) { return str; }  
      };  
        
      {  
       Heap heap;  
       getXYZ(a_,a__,&heap);  
       heap.get(); // gibt Array auf Heap zurück das von der getXYZ erzeugt wurde  
       printf("%s", heap.get());  
      // Wenn nun das Ende dieses Blocks erreicht ist z.B. Funktionsende wird der Destruktor von Heap aufgerufen der den Heap wieder freigibt. So kann verhindert werden das ich es vergesse.  
      // Was hältst du von der Idee?  
      }  
      
      

      johnny

      1. Hallo johnny,

        Wenn ich für das ganze jetzt eine Klasse schreibe die all das Automatisch erledigt?

        Wozu? Was bringt Dir das? Wenn Du eine Unterfunktion aufrufen willst und nicht willst, dass die etwas auf dem Heap alloziert, dann alloziert doch in der AUFRUFENDEN Funktion etwas auf dem Stack.

        Beispiel:

        int getXYZ (char *buf, size_t bufsize /*, weitere Parameter */) {  
           // erstelle IRGENDWIE einen Wert und schreibe ihn in den Puffer  
           // und stelle sicher, dass bufsize nicht überschritten wird  
           // (muss nicht strncpy sein, kann auch etwas anderes wie z.B.  
           // snprintf o.ä. sein)  
           strncpy (buf, /* der wert */, bufsize);  
           // z.B. 0 == erfolg  
           return 0;  
        }
        

        Damit has Du dann eine Funktion, der Du einen bestehenden Puffer und dessen Maximalgröße übergeben kannst, die ihn irgendwie füllt (was auch immer Du machen willst).

        Wenn Du nun die Funktion aufrufen willst, kannst Du ganz einfach folgendes machen:

        void andereFunktion (void) {  
          char buf[1001];  
          int ret = getXYZ (buf, 1001 /*, weitere Parameter */);  
          // irgendwas mit ret und buf anstellen  
          // funktion beendet sich, buf wird automatisch zerstört  
          // da er jedoch in DIESER funktion alloziert wurde und nicht in  
          // getXYZ, fliegt Dir auch nichts um die Ohren  
        }
        

        Wozu also extra eine Klasse dafür erstellen?

        Bzw. wenn Du Dir extra eine Klasse für so etwas erstellen willst, dann würde ich eher die Methode empfehlen, die APR verwenden. APR ist die Apache Portabel Runtime und ist eine C-Bibliothek. Allerdings lässt sich das Grundprinzip der Speicherverwaltung, die diese nutzt, auch auf C++ übertragen, müsstest Du halt selbst programmieren (außer Du findest irgendwo bereits eine vorgefertige Bibliothek dafür). Das Konzept von APR sind sogenannte "Memory Pools". Bei Subversion, das auch APR verwendet, gibt's ne nette Einführung für diese Memory Pools (http://svnbook.red-bean.com/en/1.1/ch08s05.html) - allerdings ist die natürlich in C.

        In C++ würdest Du halt von der API her im einfachsten Fall sowas in der Art machen:

        class MemoryPool {  
          /* private-gedöns */  
          public:  
            MemoryPool ();  
            ~MemoryPool ();  
            void *alloc (size_t bytes);  
        };
        

        Was würde die Implementierung von alloc() machen? Sie würde auf irgend eine Weise einen Speicherbereich der Größe »bytes« allozieren und den dann zurückgeben. Das Programm könnte den dann verwenden. Freigegeben werden würde der Speicherbereich dann erst im Destruktor, der ALLE von diesem Objekt aus allozierte Speicherbereiche zerstören würde. Ein Beispiel für die Verwendung sähe dann so aus:

        void tuIrgendwas (void) {  
          MemoryPool pool;  
          // 10-zeichen große zeichenkette hier anlegen  
          char *zeiger1 = (char *) pool.alloc (10 * sizeof (char));  
          // zeichenkette aus der unterfunktion anlegen lassen  
          char *zeiger2 = getXYZ (&pool /*, weitere Parameter */);  
          // tu irgendwas  
          // am Ende wird pool zerstört und damit auch alle allozierten Zeiger  
        }
        

        Und getXYZ sähe dann so aus:

        char *getXYZ (MemoryPool *pool /*, weitere Parameter */) {  
          // rechne irgendwas  
          char *ergebnis = pool->alloc (1001 * sizeof (char));  
          strncpy (ergebnis, /* wert */, 1001);  
          return ergebnis;  
        }
        

        Das ganze lässt sich natürlich auch noch auf so Dinge wie "Sub-Pools" erweitern, d.h. Pools, die "Kinder" von anderen Pools sind. Schau Dir einfach mal die Apache Portable Runtime an (http://apr.apache.org/) - die kann zwar noch deutlich mehr als Memory Pools und vieles davon brauchst Du in C++ nicht (weil's da wg. OOP besseres gibt und APR ein C-Projekt ist), aber Du kannst Dir da wirklich mal die Pools ansehen.

        Ich habe Dir hier jetzt nur mal das Grobkonzept dargelegt, der eigentliche Witz an Memory Pools ist jedoch die Performance - d.h. sie wurden ursprünglich entwickelt, um die Performance gegenüber new / malloc zu steigern. Wenn Du diese Pools nach meiner Beschreibung sehr naiv implementieren würdest, dann hättest Du zwar die Vorteile, was das Programmieren angeht, aber die Performance wäre schlechter als die von new / malloc. Wenn Du dagegen Zeit investierst, Dir ein brauchbares Konzept für diese Pools zu überlegen, dann können die sogar noch schneller sein, als new  / malloc - und dann hättest Du zwei Vorteile auf einmal. Da sich andere Leute schon sehr viele schlaue Gedanken zu dem Thema gemacht haben, empfielt es sich in meinen Augen, mal nach dem Stichwort zu suchen und zu sehen, welche Lösungen es da bereits gibt (beachte jedoch, das manche Leute einen einzigen GLOBALEN Memory-Pool für die ganze Applikation wollen, um einfach nur mehr Performance zu bekommen, während andere Leute multiple Pools wollen, um eben innerhalb von Funktionen Speicher schmerzfrei allozieren zu können).

        Viele Grüße,
        Christian