Speicherfreigabe primitiver Datentypen



  • Hallo Forum,

    hier geht es um eine allgemeine Frage, die mich seit Längerem beschäftigt, aber mich bisher nicht genügend anspornte, dem gezielt auf den Grund zu gehen. Dennoch:

    Zu welchem Zeitpunkt wird Speicherplatz für primitive Datentypen (int, char, etc.), den man innerhalb einer Funktion durch das Deklarieren einer Variable (int a, etc.) alloziert, vom Prozess/OS (?) wieder freigegeben? Geschieht dies beim Rücksprung aus einer Funktion, die z.B. innerhalb der main Funktion aufgerufen wird, oder erst bei der kompletten Beendigung des Prozesses?

    Und wie sieht es mit Speicher aus, der z.B. in Schleifen alloziert wird? Z.B.:

    for (i=0;i<10;i++) {
       char c = 'a';
       printf("%c\n", c);
    }
    

    Wird dieser beim Verlassen der Schleife gelöscht oder auch erst beim Rücksprung aus der die Schleife enthaltenen Funktion? Vielen Dank schon mal für jegliche Hilfe!



  • Der Speicher von Variablen, die auf dem Stack liegen (also alle, die nicht mit malloc() angefordert wurden oder static sind) wird freigegeben, sobald der aktuelle Scope verlassen wird.
    Ein Scope ist ein Bereich zwischen zwei geschweiften Klammern. Bei jeder schließenden Klammer werden die Variablen aus dem Speicherbereich davor freigegeben.


  • Mod

    Das ist nicht genauer festgelegt, aber automatische Variablen verhalten sich so, als ob ihre Lebenszeit mit dem Ende des Gültigkeitsbereiches endet. Also wenn die geschweifte Klammer der Ebene zugeht, in der sie erstellt wurden.

    Du darfst dir da aber keine richtige Freigabe a là free() vorstellen. Solche Variablen müssen nicht einmal wirklich existieren (im Sinne dass sie eine Speicheradresse haben). Selbst wenn sie existieren sind sie (bei jeder gängigen Implementierung) bloß ein paar Offsets im Programmcode, die relativ zu einem Zeiger auf ein allgemeines Gebiet im Speicher (den Stack) zu sehen sind, in dem ein Programm (genauer: ein Thread) seine automatischen Variablen ablegt. Guckst du hier, wie das genau funktioniert:
    http://en.wikipedia.org/wiki/Callstack
    Daraus folgt dann auch, dass das "Erstellen" oder "Freigeben" dieser Variablen gar keines ist und instantan von statten geht (bis auf eventuelle einmalige Verschiebung des Stackzeigers).



  • Theoretisch bei Blockende, wenn die Variable ihren Gültigkeitsbereich verlässt. In diesem Fall würde die Variable c nach jedem Schleifendurchlauf "freigegeben" und im nächsten eine neue "angefordert." Das ist das Modell, das du beim Programmieren im Kopf haben solltest.

    Praktisch läuft das in der Regel so (alloca und VLAs mal außen vorgelassen), dass das alles bei Einsprung und Verlassen der Funktion passiert. Der Compiler weiß, wie viel Speicher die Funktion zur Laufzeit maximal brauchen wird und erzeugt Code, der bei Einsprung in die Funktion den Stackzeiger entsprechend erniedrigt (der Stack wächst üblicherweise nach unten) und ihn bei Verlassen wieder zurücksetzt. Das ist laufzeittechnisch halt am billigsten. Es kann auch passieren, dass eine Variable gar keine Entsprechung im Speicher hat - in deinem Beispiel ist es beispielsweise durchaus denkbar, dass c die ganze Zeit in einem Register liegt oder gar ganz wegoptimiert wird.



  • Vielen Dank für die Antworten!



  • Bezüglich der oben genannten Angaben bin ich ob des nun angetroffenen Problems etwas verwirrt.

    Und zwar führt folgendes Programm zu folgender Ausgabe:

    #include <stdio.h>
    #include <string.h>
    
    int print_this(const char *msg) {
    
        char cmd[50];
    
        printf("!!! this is cmd: %s\n", cmd);
        strncpy(cmd, msg, strlen(msg));
        printf("this is message: %s\n", cmd);
    
        return 0;
    }
    
    int main(void) {
    
        print_this("hello wohlland");
        print_this("fara");
    
        return 0;
    
    }
    

    Ausgabe:

    !!! this is cmd: @�S
    this is message: hello wohlland
    !!! this is cmd: hello wohlland
    this is message: farao wohlland
    

    Wie kommt dies zustande? Was verpasse ich an dieser Stelle? Eigentlich müsste die dritte Zeile der Ausgabe doch ebenfalls garbage values ausspucken, sofern die Variable cmd stets ihre Gültigkeit verliert. Nun kann man annehmen, dass bei der Allozierung immer auf denselben Speicherplatz zugegriffen wird, wo zufällig noch der alte Wert drin steht. Fügte man dem cmd stets ein '\0' hinzu, dürfte der zuvor angenommene Wert immerhin egal sein. Doch hier wird eine Funktion aufgerufen und vor allem wieder beendet. Wieso wird trotzdem immer dieselbe Speicheradresse (oder wie man es auch immer korrekt ausdrückt) angesprochen und somit alte Werte aufgerufen? Wieso wird beim ersten Aufruf der Rest der garbage values nach Kopieren des Strings gewissermaßen korrekt abgeschnitten, während beim zweiten Aufruf der alte Wert wieder aufgetan wird? Und vor allem das wichtigste: Wie entgehe ich diesem Problem?

    Dies Problem tritt bei mir nämlich ebenfalls im Zusammenhang mit einem struct auf, den ich leider nicht erst mit memset(cmd, '\0', 50) bearbeiten kann.

    Gruß!



  • Es wird eben der Speicherbereich recyled und es steht halt noch das vom vorigen Mal drin. Bzw. kann das sein. Verlässt man sich darauf, ist man im Undefined Behaviour!

    Mach ein strlen(msg) + 1 um den String korrekt zu terminieren und verwende ihn erst, wenn er entsprechend initialisiert ist!



  • Das +1 hat das Problem zumindest für das oben genannte Beispielproblem gelöst. Wie kann man diesem Problem allerdings begegnen, wenn man die Größe des benötigten Speicherplatzes nicht vorher weiß und daher in einem struct z.B. mit Pointern arbeitet?

    Sei z.B. die folgende Struktur gegeben:

    struct example {
    
      char *text;
      < ... andere Deklarationen >
    };
    

    Nun möchte ich unterschiedlich große Texte einlesen und deklariere hierfür in einer Schleife nacheinander Variablen vom struct example. Angenommen, ich kann für jeden Text die Größe bestimmen und lese sie darauf basierend in genügend große char-Arrays ein. Anschließend setze ich den Pointer auf Position 0 des char-Arrays. Sähe dann in etwa so aus:

    while (irgendwas) {
       struct example *ex = (struct example *)malloc(sizeof(struct example));
    
       char text_buffer[bekannte_größe];
       get_text(text_buffer);
       ex->text = text_buffer;
    }
    

    Wie kann ich gewährleisten, dass alle Pointer auf die 'richtigen' Speicherbereiche zeigen und nicht auf wiederverwertete, in denen sich noch Reste voriger Definitionen befanden?


  • Mod

    Lern die Grundregeln wie Gültigkeitsbereiche und Zeichenketten in C funktionieren! In deinem Beispiel ist alles ungültig, ich bin mir nicht einmal sicher, was du machen willst. Soll dein struct einen Text unbekannter Länge verwalten, der aber garantiert in bekannte_größe passt? Das ginge so:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    #define MAX_LENGTH 50
    
    typedef struct simple_string
    {
      char *data;
    } *simple_string;
    
    simple_string construct_simple_string()
    {
      return calloc(1, sizeof(struct simple_string));
    }
    
    int read_simple_string(simple_string this, FILE *in)
    {
      // Nicht gerade elegant, aber du hattest danach gefragt:
      char buffer[MAX_LENGTH];
      if (fgets(buffer, MAX_LENGTH, in))
        {
          unsigned length = strlen(buffer);
          free(this->data);
          this->data = malloc(length + 1);
          memcpy(this->data, buffer, length + 1);
          return 1;
        }
      else return 0;
    }
    
    void print_simple_string(simple_string this, FILE *out)
    {
      if (this->data)
        fprintf(out, "Dieser String hat sein Datensegment an Addresse %p. An dieser Stelle steht der Inhalt: %s", this->data, this->data);
      else
        fputs("Dieser String ist leer\n", out);
    }
    
    void destruct_simple_string(simple_string this)
    {
      if (this)
        free(this->data);
      free(this);
    }
    
    int main()
    {
      simple_string foo = construct_simple_string();
      while (read_simple_string(foo, stdin))
        {
          print_simple_string(foo, stdout);
        }
      destruct_simple_string(foo);    
      return 0;
    }
    

    Du wirst je nach System eventuell feststellen, dass der Speicher durchaus wieder verwendet wird. Trotzdem läuft alles korrekt. Hier ein Beispielrun von meinem Linuxsystem:

    $> ./a.out 
    fd.bgn sknfskn kwe
    Dieser String hat sein Datensegment an Addresse 0x1b02030. An dieser Stelle steht der Inhalt: fd.bgn sknfskn kwe
    knfkwn4k3n jk4nr 3
    Dieser String hat sein Datensegment an Addresse 0x1b02030. An dieser Stelle steht der Inhalt: knfkwn4k3n jk4nr 3
    jnwjrnjkrnkw3n 
    Dieser String hat sein Datensegment an Addresse 0x1b02030. An dieser Stelle steht der Inhalt: jnwjrnjkrnkw3n 
    4knfkntr3ke n 4n k jknjk n4k n2jk n2jkn3 k2
    Dieser String hat sein Datensegment an Addresse 0x1b02050. An dieser Stelle steht der Inhalt: 4knfkntr3ke n 4n k jknjk n4k n2jk n2jkn3 k2
    


  • per schrieb:

    Wie kann ich gewährleisten, dass alle Pointer auf die 'richtigen' Speicherbereiche zeigen und nicht auf wiederverwertete, in denen sich noch Reste voriger Definitionen befanden?

    Das Speicher 'richtig', also gültig ist, das gewährleisten dir die Funktionen malloc und calloc, wenn sie einen Wert ungleich NULL liefern.
    Dieser Speicher bleibt so lange während der Programmausführung gültig, bis du ihn mittels der Funktion free freigibst.

    Das Speicher 'richtig', also gültig ist, gewährleisten dir u.a. auch Variablen/Arrays für Speicher bekannter, konstanter Größe.
    Auf diesen Speicher kannst du auch über Zeiger zugreifen.

    Was im Speicher unmittelbar vor und nach der Allokation drin stand,
    ob es irgendwelche 'Reste' sind oder nicht, ist in beiden Fällen prinzipiell irrelevant, denn du bist für den Inhalt verantwortlich.
    Du kannst den Speicher sinnvoll initialisieren, im Falle calloc übernimmt das die Funktion für dich.

    SeppJ hat dir ja schon die richtigen Stichworte genannt wie z.B. die Gültigkeitsbereiche von Arrays/Variablen.



  • Gestern Nacht ist mir mein grundlegendes Unverständnis ebenfalls bewusst geworden. In dem in jedem Fall ungenügenden Beispiel sollte "bekannte_größe" einfach nur versinnbildlichen, dass die Größe bei jeder Iteration festgestellt werden kann. Da sie jedoch variiert, war mir nicht klar, wie oder ob so etwas in einem struct gut gehandelt werden kann.

    Dass mein Beispiel nicht funktionieren kann, liegt dann sicherlich daran, dass der für den Text verwandte Speicher nach jedem Durchlauf seine Gültigkeit verliert und Pointer nun einmal nicht über eigene, gewissermaßen autonome Speicherbereiche verfügen, sondern nur auf anderweitig allozierte Bereiche zeigen. Danke schon einmal für die Hinweise und das ausführliche Beispiel!


Anmelden zum Antworten