C Speicherverwaltung

90

Ich habe immer gehört, dass man in C wirklich darauf achten muss, wie man den Speicher verwaltet. Und ich fange immer noch an, C zu lernen, aber bisher musste ich überhaupt keine Speicherverwaltungsaktivitäten durchführen. Ich stellte mir immer vor, Variablen freigeben und alle möglichen hässlichen Dinge tun zu müssen. Dies scheint jedoch nicht der Fall zu sein.

Kann mir jemand (mit Codebeispielen) ein Beispiel zeigen, wann Sie eine "Speicherverwaltung" durchführen müssten?

The.Anti.9
quelle
Guter Ort, um G4G
EsmaeelE

Antworten:

230

Es gibt zwei Stellen, an denen Variablen gespeichert werden können. Wenn Sie eine Variable wie folgt erstellen:

int  a;
char c;
char d[16];

Die Variablen werden im " Stack " erstellt. Stapelvariablen werden automatisch freigegeben, wenn sie den Gültigkeitsbereich verlassen (dh wenn der Code sie nicht mehr erreichen kann). Sie könnten sie als "automatische" Variablen bezeichnen, aber das ist aus der Mode gekommen.

Viele Anfängerbeispiele verwenden nur Stapelvariablen.

Der Stapel ist schön, weil er automatisch ist, hat aber auch zwei Nachteile: (1) Der Compiler muss im Voraus wissen, wie groß die Variablen sind, und (b) der Stapelspeicher ist etwas begrenzt. Beispiel: In Windows ist der Stapel unter den Standardeinstellungen für den Microsoft-Linker auf 1 MB festgelegt, und nicht alles ist für Ihre Variablen verfügbar.

Wenn Sie zur Kompilierungszeit nicht wissen, wie groß Ihr Array ist, oder wenn Sie ein großes Array oder eine große Struktur benötigen, benötigen Sie "Plan B".

Plan B heißt " Haufen ". Normalerweise können Sie Variablen erstellen, die so groß sind, wie es das Betriebssystem zulässt, aber Sie müssen dies selbst tun. Frühere Beiträge haben Ihnen gezeigt, wie Sie dies tun können, obwohl es auch andere Möglichkeiten gibt:

int size;
// ...
// Set size to some value, based on information available at run-time. Then:
// ...
char *p = (char *)malloc(size);

(Beachten Sie, dass Variablen im Heap nicht direkt, sondern über Zeiger bearbeitet werden.)

Sobald Sie eine Heap-Variable erstellt haben, besteht das Problem darin, dass der Compiler nicht erkennen kann, wann Sie damit fertig sind, sodass Sie die automatische Freigabe verlieren. Hier kommt die "manuelle Freigabe" ins Spiel, auf die Sie sich bezogen haben. Ihr Code ist nun dafür verantwortlich, zu entscheiden, wann die Variable nicht mehr benötigt wird, und sie freizugeben, damit der Speicher für andere Zwecke verwendet werden kann. Für den obigen Fall mit:

free(p);

Was diese zweite Option zu einem "bösen Geschäft" macht, ist, dass es nicht immer einfach ist zu wissen, wann die Variable nicht mehr benötigt wird. Wenn Sie vergessen, eine Variable freizugeben, wenn Sie sie nicht benötigen, belegt Ihr Programm mehr Speicher, als es benötigt. Diese Situation wird als "Leck" bezeichnet. Der "durchgesickerte" Speicher kann für nichts verwendet werden, bis Ihr Programm endet und das Betriebssystem alle seine Ressourcen wiederherstellt. Noch schlimmere Probleme sind möglich, wenn Sie versehentlich eine Heap-Variable freigeben, bevor Sie tatsächlich damit fertig sind.

In C und C ++ sind Sie dafür verantwortlich, Ihre Heap-Variablen wie oben gezeigt zu bereinigen. Es gibt jedoch Sprachen und Umgebungen wie Java- und .NET-Sprachen wie C #, die einen anderen Ansatz verwenden, bei dem der Heap selbst bereinigt wird. Diese zweite Methode, die als "Garbage Collection" bezeichnet wird, ist für den Entwickler viel einfacher, aber Sie zahlen eine Strafe für Overhead und Leistung. Es ist ein Gleichgewicht.

(Ich habe viele Details beschönigt, um eine einfachere, aber hoffentlich ebenere Antwort zu geben.)

Euro Micelli
quelle
3
Wenn Sie etwas auf den Stapel legen möchten, aber nicht wissen, wie groß es beim Kompilieren ist, kann alloca () den Stapelrahmen vergrößern, um Platz zu schaffen. Es gibt kein freea (), der gesamte Stapelrahmen wird gelöscht, wenn die Funktion zurückkehrt. Die Verwendung von alloca () für große Allokationen ist mit Gefahren verbunden.
DGentry
1
Vielleicht könnten Sie ein oder zwei Sätze über den Speicherort globaler Variablen hinzufügen
Michael Käfer
In C nie zurückgeben malloc(), seine Ursache UB, (char *)malloc(size);siehe stackoverflow.com/questions/605845/…
EsmaeelE
17

Hier ist ein Beispiel. Angenommen, Sie haben eine strdup () - Funktion, die eine Zeichenfolge dupliziert:

char *strdup(char *src)
{
    char * dest;
    dest = malloc(strlen(src) + 1);
    if (dest == NULL)
        abort();
    strcpy(dest, src);
    return dest;
}

Und du nennst es so:

main()
{
    char *s;
    s = strdup("hello");
    printf("%s\n", s);
    s = strdup("world");
    printf("%s\n", s);
}

Sie können sehen, dass das Programm funktioniert, aber Sie haben Speicher (über malloc) zugewiesen, ohne ihn freizugeben. Sie haben Ihren Zeiger auf den ersten Speicherblock verloren, als Sie strdup das zweite Mal aufgerufen haben.

Dies ist keine große Sache für diese kleine Menge an Speicher, aber betrachten Sie den Fall:

for (i = 0; i < 1000000000; ++i)  /* billion times */
    s = strdup("hello world");    /* 11 bytes */

Sie haben jetzt 11 Gig Arbeitsspeicher verbraucht (möglicherweise mehr, abhängig von Ihrem Speichermanager), und wenn Sie nicht abgestürzt sind, läuft Ihr Prozess wahrscheinlich ziemlich langsam.

Um dies zu beheben, müssen Sie free () für alles aufrufen, was mit malloc () erhalten wird, nachdem Sie es nicht mehr verwenden:

s = strdup("hello");
free(s);  /* now not leaking memory! */
s = strdup("world");
...

Hoffe dieses Beispiel hilft!

Mark Harrison
quelle
Diese Antwort gefällt mir besser. Aber ich habe eine kleine Nebenfrage. Ich würde erwarten, dass so etwas mit Bibliotheken gelöst wird. Gibt es nicht eine Bibliothek, die die grundlegenden Datentypen genau nachahmt und ihnen Funktionen zur Speicherfreigabe hinzufügt, damit sie bei Verwendung von Variablen auch automatisch freigegeben werden?
Lorenzo
Keine, die Teil des Standards sind. Wenn Sie in C ++ wechseln, erhalten Sie Zeichenfolgen und Container, die die automatische Speicherverwaltung durchführen.
Mark Harrison
Ich verstehe, also gibt es einige Bibliotheken von Drittanbietern? Könnten Sie sie bitte nennen?
Lorenzo
9

Sie müssen "Speicherverwaltung" durchführen, wenn Sie Speicher auf dem Heap anstelle des Stapels verwenden möchten. Wenn Sie nicht wissen, wie groß ein Array bis zur Laufzeit sein soll, müssen Sie den Heap verwenden. Beispielsweise möchten Sie möglicherweise etwas in einer Zeichenfolge speichern, wissen jedoch nicht, wie groß der Inhalt sein wird, bis das Programm ausgeführt wird. In diesem Fall würden Sie so etwas schreiben:

 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory
Jeremy Ruten
quelle
5

Ich denke, die prägnanteste Art, die Frage zu beantworten, um die Rolle des Zeigers in C zu betrachten. Der Zeiger ist ein leichter und dennoch leistungsstarker Mechanismus, der Ihnen immense Freiheit auf Kosten einer immensen Fähigkeit gibt, sich in den Fuß zu schießen.

In C liegt die Verantwortung dafür, dass Ihre Zeiger auf Ihr eigenes Gedächtnis verweisen, bei Ihnen und allein bei Ihnen. Dies erfordert einen organisierten und disziplinierten Ansatz, es sei denn, Sie verzichten auf Hinweise, was es schwierig macht, effektives C zu schreiben.

Die bisher veröffentlichten Antworten konzentrieren sich auf automatische (Stapel-) und Heap-Variablenzuweisungen. Die Verwendung der Stapelzuweisung sorgt zwar für automatisch verwalteten und bequemen Speicher, kann jedoch unter bestimmten Umständen (große Puffer, rekursive Algorithmen) zu dem schrecklichen Problem des Stapelüberlaufs führen. Es ist sehr systemabhängig, genau zu wissen, wie viel Speicher Sie auf dem Stapel zuweisen können. In einigen eingebetteten Szenarien können einige Dutzend Bytes Ihr Limit sein, in einigen Desktop-Szenarien können Sie Megabyte sicher verwenden.

Die Heap-Zuordnung ist der Sprache weniger eigen. Es handelt sich im Grunde genommen um eine Reihe von Bibliotheksaufrufen, die Ihnen den Besitz eines Speicherblocks bestimmter Größe gewähren, bis Sie bereit sind, ihn zurückzugeben ("frei"). Es klingt einfach, ist aber mit unermesslichem Kummer des Programmierers verbunden. Die Probleme sind einfach (zweimal oder gar nicht denselben Speicher freigeben [Speicherlecks], nicht genügend Speicher zuweisen [Pufferüberlauf] usw.), aber schwer zu vermeiden und zu debuggen. Ein hochdisziplinierter Ansatz ist in der Praxis absolut obligatorisch, aber die Sprache schreibt ihn natürlich nicht vor.

Ich möchte eine andere Art der Speicherzuweisung erwähnen, die von anderen Posts ignoriert wurde. Es ist möglich, Variablen statisch zuzuweisen, indem sie außerhalb einer Funktion deklariert werden. Ich denke, im Allgemeinen bekommt diese Art der Zuweisung einen schlechten Ruf, weil sie von globalen Variablen verwendet wird. Es gibt jedoch nichts, was besagt, dass der auf diese Weise zugewiesene Speicher nur als undisziplinierte globale Variable in einem Durcheinander von Spaghetti-Code verwendet werden kann. Die statische Zuweisungsmethode kann einfach verwendet werden, um einige der Fallstricke des Heaps und der automatischen Zuweisungsmethoden zu vermeiden. Einige C-Programmierer sind überrascht zu erfahren, dass große und hochentwickelte C-Embedded- und Spieleprogramme ohne Verwendung der Heap-Zuweisung erstellt wurden.

Bill Forster
quelle
4

Hier gibt es einige gute Antworten zum Zuweisen und Freigeben von Speicher. Meiner Meinung nach besteht die schwierigere Seite der Verwendung von C darin, sicherzustellen, dass der einzige Speicher, den Sie verwenden, der von Ihnen zugewiesene Speicher ist - wenn dies nicht richtig gemacht wird, was Sie beenden up with ist der Cousin dieser Site - ein Pufferüberlauf - und Sie überschreiben möglicherweise den Speicher, der von einer anderen Anwendung verwendet wird, mit sehr unvorhersehbaren Ergebnissen.

Ein Beispiel:

int main() {
    char* myString = (char*)malloc(5*sizeof(char));
    myString = "abcd";
}

Zu diesem Zeitpunkt haben Sie 5 Bytes für myString zugewiesen und mit "abcd \ 0" gefüllt (Zeichenfolgen enden mit null - \ 0). Wenn Ihre Zeichenfolgenzuordnung war

myString = "abcde";

Sie würden "abcde" in den 5 Bytes zuweisen, die Sie Ihrem Programm zugewiesen haben, und das nachfolgende Nullzeichen würde am Ende davon stehen - ein Teil des Speichers, der nicht für Ihre Verwendung zugewiesen wurde und sein könnte kostenlos, kann aber auch von einer anderen Anwendung verwendet werden - Dies ist der kritische Teil der Speicherverwaltung, bei dem ein Fehler unvorhersehbare (und manchmal nicht wiederholbare) Folgen hat.

Chris BC
quelle
Hier ordnen Sie 5 Bytes zu. Lösen Sie es, indem Sie einen Zeiger zuweisen. Jeder Versuch, diesen Zeiger freizugeben, führt zu undefiniertem Verhalten. Hinweis C-Strings überladen den Operator = nicht, es gibt keine Kopie.
Martin York
Es hängt jedoch wirklich von dem Malloc ab, das Sie verwenden. Viele Malloc-Operatoren richten sich nach 8 Bytes aus. Wenn dieses Malloc ein Kopf- / Fußzeilensystem verwendet, würde malloc 5 + 4 * 2 (4 Bytes für Kopf- und Fußzeile) reservieren. Das wären 13 Bytes, und malloc würde Ihnen nur zusätzliche 3 Bytes für die Ausrichtung geben. Ich sage nicht, dass es eine gute Idee ist, dies zu verwenden, da es nur Systeme gibt, deren Malloc so funktioniert, aber es ist zumindest wichtig zu wissen, warum etwas falsch machen könnte.
Kodai
Loki: Ich habe die Antwort bearbeitet, um sie strcpy()anstelle von zu verwenden =. Ich nehme an, das war die Absicht von Chris BC.
Echristopherson
Ich glaube an moderne Plattformen, dass der Hardware-Speicherschutz verhindert, dass Benutzerbereichsprozesse die Adressräume anderer Prozesse überschreiben. Sie würden stattdessen einen Segmentierungsfehler erhalten. Aber das ist nicht Teil von C an sich.
Echristopherson
4

Eine Sache zu erinnern ist, immer Ihre Zeiger auf NULL initialisiert werden , da ein nicht initialisierte Zeiger eine Pseudo - Zufall gültige Speicheradresse enthalten, die Zeiger Fehler voran gehen lautlos machen. Indem Sie erzwingen, dass ein Zeiger mit NULL initialisiert wird, können Sie immer abfangen, ob Sie diesen Zeiger verwenden, ohne ihn zu initialisieren. Der Grund dafür ist, dass Betriebssysteme die virtuelle Adresse 0x00000000 mit allgemeinen Schutzausnahmen "verbinden", um die Verwendung von Nullzeigern abzufangen.

Hernán
quelle
2

Möglicherweise möchten Sie auch die dynamische Speicherzuweisung verwenden, wenn Sie ein großes Array definieren müssen, z. B. int [10000]. Sie können es nicht einfach stapeln, denn dann, hm ... erhalten Sie einen Stapelüberlauf.

Ein weiteres gutes Beispiel wäre die Implementierung einer Datenstruktur, beispielsweise einer verknüpften Liste oder eines Binärbaums. Ich habe hier keinen Beispielcode zum Einfügen, aber Sie können ihn einfach googeln.

Serge
quelle
2

(Ich schreibe, weil ich der Meinung bin, dass die Antworten bisher nicht ganz zutreffen.)

Der Grund für die erwähnenswerte Speicherverwaltung ist, wenn Sie ein Problem / eine Lösung haben, bei der Sie komplexe Strukturen erstellen müssen. (Wenn Ihre Programme abstürzen, wenn Sie gleichzeitig zu viel Speicherplatz auf dem Stapel zuweisen, ist dies ein Fehler.) In der Regel ist die erste Datenstruktur, die Sie lernen müssen, eine Art Liste . Hier ist eine einzige verknüpfte, die mir auf den Kopf gestellt ist:

typedef struct listelem { struct listelem *next; void *data;} listelem;

listelem * create(void * data)
{
   listelem *p = calloc(1, sizeof(listelem));
   if(p) p->data = data;
   return p;
}

listelem * delete(listelem * p)
{
   listelem next = p->next;
   free(p);
   return next;
}

void deleteall(listelem * p)
{
  while(p) p = delete(p);
}

void foreach(listelem * p, void (*fun)(void *data) )
{
  for( ; p != NULL; p = p->next) fun(p->data);
}

listelem * merge(listelem *p, listelem *q)
{
  while(p != NULL && p->next != NULL) p = p->next;
  if(p) {
    p->next = q;
    return p;
  } else
    return q;
}

Natürlich möchten Sie ein paar andere Funktionen, aber im Grunde ist dies das, wofür Sie Speicherverwaltung benötigen. Ich sollte darauf hinweisen, dass es eine Reihe von Tricks gibt, die mit der "manuellen" Speicherverwaltung möglich sind, z.

  • Unter Verwendung der Tatsache, dass malloc (durch den Sprachstandard) garantiert ist, einen durch 4 teilbaren Zeiger zurückzugeben,
  • zusätzlichen Platz für einen eigenen finsteren Zweck zuweisen,
  • Erstellen von Speicherpools ..

Holen Sie sich einen guten Debugger ... Viel Glück!

Anders Eurenius
quelle
Das Erlernen von Datenstrukturen ist der nächste wichtige Schritt zum Verständnis der Speicherverwaltung. Das Erlernen der Algorithmen zum angemessenen Ausführen dieser Strukturen zeigt Ihnen die geeigneten Methoden zur Überwindung dieser Probleme. Aus diesem Grund finden Sie Datenstrukturen und Algorithmen, die in denselben Kursen vermittelt werden.
Aj.toulan
0

@ Euro Micelli

Ein Nachteil ist, dass Zeiger auf den Stapel nicht mehr gültig sind, wenn die Funktion zurückgegeben wird. Sie können also keinen Zeiger von einer Funktion auf eine Stapelvariable zurückgeben. Dies ist ein häufiger Fehler und ein Hauptgrund, warum Sie nicht nur mit Stapelvariablen auskommen können. Wenn Ihre Funktion einen Zeiger zurückgeben muss, müssen Sie sich mit der Speicherverwaltung befassen.

Jonathan Branam
quelle
0

@ Ted Percival :
... Sie müssen den Rückgabewert von malloc () nicht umsetzen.

Sie haben natürlich Recht. Ich glaube, das war schon immer so, obwohl ich keine Kopie von K & R zu überprüfen habe.

Ich mag nicht viele der impliziten Konvertierungen in C, daher verwende ich eher Casts, um "Magie" sichtbarer zu machen. Manchmal hilft es der Lesbarkeit, manchmal nicht, und manchmal wird ein stiller Fehler vom Compiler abgefangen. Trotzdem habe ich auf die eine oder andere Weise keine feste Meinung dazu.

Dies ist besonders wahrscheinlich, wenn Ihr Compiler Kommentare im C ++ - Stil versteht.

Ja ... du hast mich dort erwischt. Ich verbringe viel mehr Zeit in C ++ als in C. Vielen Dank, dass Sie das bemerkt haben.

Euro Micelli
quelle
@echristopherson, danke. Sie haben Recht - aber bitte beachten Sie, dass dieses Q / A aus dem August 2008 stammt, bevor Stack Overflow überhaupt in der öffentlichen Beta war. Damals haben wir noch herausgefunden, wie die Seite funktionieren soll. Das Format dieser Frage / Antwort sollte nicht unbedingt als Modell für die Verwendung von SO angesehen werden. Vielen Dank!
Euro Micelli
Ah, danke, dass Sie darauf hingewiesen haben - ich wusste nicht, dass der Aspekt der Website damals noch im Fluss war.
Echristopherson
0

In C haben Sie tatsächlich zwei verschiedene Möglichkeiten. Zum einen können Sie das System den Speicher für Sie verwalten lassen. Alternativ können Sie dies auch selbst tun. Im Allgemeinen möchten Sie so lange wie möglich bei ersteren bleiben. Der automatisch verwaltete Speicher in C ist jedoch äußerst begrenzt, und Sie müssen den Speicher in vielen Fällen manuell verwalten, z.

ein. Sie möchten, dass die Variable die Funktionen überlebt, und Sie möchten keine globale Variable haben. Ex:

Strukturpaar {
   int val;
   Strukturpaar * next;
}}

Strukturpaar * new_pair (int val) {
   Strukturpaar * np = malloc (Größe von (Strukturpaar));
   np-> val = val;
   np-> next = NULL;
   return np;
}}

b. Sie möchten dynamisch zugewiesenen Speicher haben. Das häufigste Beispiel ist ein Array ohne feste Länge:

int * my_special_array;
my_special_array = malloc (sizeof (int) * number_of_element);
für (i = 0; i

c. Sie wollen etwas wirklich schmutziges tun. Zum Beispiel möchte ich, dass eine Struktur viele Arten von Daten darstellt, und ich mag Union nicht (Union sieht soooo chaotisch aus):

Strukturdaten { int data_type; long data_in_mem; }; Struktur Tier {/ * etwas * /}; struct person {/ * etwas anderes * /}; struct animal * read_animal (); struct person * read_person (); / * In der Hauptsache * / Strukturdatenprobe; sampe.data_type = input_type; switch (input_type) { case DATA_PERSON: sample.data_in_mem = read_person (); brechen; case DATA_ANIMAL: sample.data_in_mem = read_animal (); Standard: printf ("Oh hoh! Ich warne dich, das nochmal und ich werde dein Betriebssystem bemängeln"); }}

Ein langer Wert reicht aus, um ALLES zu halten. Denken Sie daran, es zu befreien, sonst werden Sie es bereuen. Dies ist einer meiner Lieblingstricks, um Spaß in C: D zu haben.

Im Allgemeinen möchten Sie sich jedoch von Ihren Lieblingstricks (T___T) fernhalten. Sie werden Ihr Betriebssystem früher oder später beschädigen, wenn Sie sie zu oft verwenden. Solange Sie * alloc und free nicht verwenden, können Sie mit Sicherheit sagen, dass Sie noch jungfräulich sind und der Code immer noch gut aussieht.

Magie
quelle
"Sehen Sie, ein langer Wert reicht aus, um ALLES zu halten" -: / wovon sprechen Sie? Auf den meisten Systemen beträgt ein langer Wert 4 Byte, genau wie ein int. Der einzige Grund, warum es hier in die Zeiger passt, ist, dass die Größe von long zufällig mit der Zeigergröße übereinstimmt. Sie sollten jedoch wirklich void * verwenden.
Score_Under
-2

Sicher. Wenn Sie ein Objekt erstellen, das außerhalb des Bereichs existiert, in dem Sie es verwenden. Hier ist ein erfundenes Beispiel (denken Sie daran, dass meine Syntax deaktiviert ist; mein C ist verrostet, aber dieses Beispiel veranschaulicht das Konzept immer noch):

class MyClass
{
   SomeOtherClass *myObject;

   public MyClass()
   {
      //The object is created when the class is constructed
      myObject = (SomeOtherClass*)malloc(sizeof(myObject));
   }

   public ~MyClass()
   {
      //The class is destructed
      //If you don't free the object here, you leak memory
      free(myObject);
   }

   public void SomeMemberFunction()
   {
      //Some use of the object
      myObject->SomeOperation();
   }


};

In diesem Beispiel verwende ich während der Lebensdauer von MyClass ein Objekt vom Typ SomeOtherClass. Das SomeOtherClass-Objekt wird in mehreren Funktionen verwendet, daher habe ich den Speicher dynamisch zugewiesen: Das SomeOtherClass-Objekt wird beim Erstellen von MyClass erstellt, während der Lebensdauer des Objekts mehrmals verwendet und nach dem Freigeben von MyClass freigegeben.

Wenn dies echter Code wäre, gäbe es natürlich keinen Grund (abgesehen vom möglichen Stapelspeicherverbrauch), myObject auf diese Weise zu erstellen, aber diese Art der Objekterstellung / -zerstörung wird nützlich, wenn Sie viele Objekte haben und genau steuern möchten Wenn sie erstellt und zerstört werden (damit Ihre Anwendung beispielsweise nicht 1 GB RAM für die gesamte Lebensdauer verbraucht) und in einer Windows-Umgebung, ist dies als von Ihnen erstellte Objekte (z. B. Schaltflächen) ziemlich obligatorisch. müssen weit außerhalb des Anwendungsbereichs einer bestimmten Funktion (oder sogar einer Klasse) existieren.

Der Schlumpf
quelle
1
Heh, ja, das ist C ++, nicht wahr? Erstaunlich, dass es fünf Monate gedauert hat, bis mich jemand angerufen hat.
TheSmurf