C Speicherverwaltung

Lesezeit: 11 Minuten

Benutzeravatar von The.Anti.9
Der.Anti.9

Ich habe immer gehört, dass man in C wirklich aufpassen muss, wie man Speicher verwaltet. Und ich fange immer noch an, C zu lernen, aber bisher musste ich überhaupt keine damit verbundenen Aktivitäten zur Speicherverwaltung durchführen. Ich habe mir immer vorgestellt, Variablen freigeben und alle möglichen hässlichen Dinge tun zu müssen. Aber dies scheint nicht der Fall zu sein.

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

  • Guter Ort zum Lernen G4G

    – EsmaeelE

    18. Dezember 2017 um 22:46 Uhr

Benutzeravatar von Euro Micelli
Euro Mizellen

Es gibt zwei Orte, an denen Variablen in den Speicher gestellt werden können. Wenn Sie eine Variable wie diese erstellen:

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

Die Variablen werden im “Stapel“. Stack-Variablen werden automatisch freigegeben, wenn sie den Gültigkeitsbereich verlassen (das heißt, wenn der Code sie nicht mehr erreichen kann). Sie werden sie vielleicht “automatische” Variablen nennen, aber das ist aus der Mode gekommen.

Viele Anfängerbeispiele verwenden nur Stack-Variablen.

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

Wenn Sie zur Kompilierzeit 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“. Sie können normalerweise Variablen so groß erstellen, wie es das Betriebssystem zulässt, aber Sie müssen es selbst tun. Frühere Beiträge haben Ihnen eine Möglichkeit gezeigt, wie Sie es tun können, obwohl es 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 manipuliert werden, sondern über Zeiger)

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 das “manuelle Freigeben” ins Spiel, auf das Sie sich bezogen haben. Ihr Code ist jetzt 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 einer “bösen Angelegenheit” 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, verbraucht Ihr Programm mehr Speicher als nötig. Diese Situation wird als “Leck” bezeichnet. Der “durchgesickerte” Speicher kann für nichts verwendet werden, bis Ihr Programm beendet ist und das Betriebssystem alle seine Ressourcen wiederhergestellt hat. Noch schlimmere Probleme sind möglich, wenn Sie versehentlich eine Heap-Variable freigeben Vor du bist eigentlich fertig damit.

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 namens “Garbage Collection” ist für den Entwickler viel einfacher, aber Sie zahlen einen Nachteil in Bezug auf Overhead und Leistung. Es ist ein Gleichgewicht.

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

  • Wenn Sie etwas auf den Stapel legen möchten, aber nicht wissen, wie groß es zur Kompilierzeit ist, kann alloca() den Stapelrahmen vergrößern, um Platz zu schaffen. Es gibt kein freea(), der gesamte Stackframe wird gepoppt, wenn die Funktion zurückkehrt. Die Verwendung von alloca() für große Allokationen ist voller Gefahren.

    – DGentry

    7. September 2008 um 2:17 Uhr

  • Vielleicht könnten Sie ein oder zwei Sätze über den Speicherort globaler Variablen hinzufügen

    – Michael Käfer

    18. Januar 2017 um 22:02 Uhr

Hier ist ein Beispiel. Angenommen, Sie haben eine strdup()-Funktion, die einen String 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 zugewiesen (über malloc), 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 Speichermenge, aber bedenken Sie den Fall:

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

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

Um das Problem zu beheben, müssen Sie free() für alles aufrufen, was mit malloc() erhalten wird, nachdem Sie es beendet haben:

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

Hoffe, dieses Beispiel hilft!

  • 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 Speicherfreigabefunktionen hinzufügt, sodass sie bei Verwendung von Variablen auch automatisch freigegeben werden?

    – Lorenzo

    17. August 2011 um 12:44 Uhr

  • Keine, die Teil des Standards sind. Wenn Sie in C++ einsteigen, erhalten Sie Zeichenfolgen und Container, die eine automatische Speicherverwaltung durchführen.

    – Mark Harrison

    18. August 2011 um 0:44 Uhr

  • Ich verstehe, also gibt es einige Bibliotheken von Drittanbietern? Könntest du sie bitte nennen?

    – Lorenzo

    18. August 2011 um 8:48 Uhr

Sie müssen “Speicherverwaltung” durchführen, wenn Sie Speicher auf dem Heap statt auf dem Stack verwenden möchten. Wenn Sie bis zur Laufzeit nicht wissen, wie groß ein Array werden soll, müssen Sie den Heap verwenden. Beispielsweise möchten Sie möglicherweise etwas in einem String speichern, wissen aber nicht, wie groß sein Inhalt sein wird, bis das Programm ausgeführt wird. In diesem Fall würdest du 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

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

In C liegt die Verantwortung dafür, dass Ihre Zeiger auf Speicher zeigen, den Sie besitzen, bei Ihnen und nur bei Ihnen. Dies erfordert einen organisierten und disziplinierten Ansatz, es sei denn, Sie verzichten auf Zeiger, was es schwierig macht, effektives C zu schreiben.

Die bisher geposteten Antworten konzentrieren sich auf automatische (Stapel-) und Heap-Variablenzuweisungen. Die Verwendung der Stack-Zuordnung sorgt für automatisch verwalteten und bequemen Speicher, aber unter bestimmten Umständen (große Puffer, rekursive Algorithmen) kann dies zu dem schrecklichen Problem des Stack-Überlaufs führen. Genau zu wissen, wie viel Speicher Sie dem Stapel zuweisen können, hängt stark vom System ab. In einigen Embedded-Szenarien könnten ein paar Dutzend Bytes Ihr Limit sein, in einigen Desktop-Szenarien können Sie Megabyte sicher verwenden.

Die Heap-Zuweisung ist der Sprache weniger eigen. Es handelt sich im Grunde genommen um eine Reihe von Bibliotheksaufrufen, die Ihnen das Eigentum an einem Speicherblock einer bestimmten Größe gewähren, bis Sie bereit sind, ihn zurückzugeben („frei“). Es klingt einfach, ist aber mit unsagbarem Programmiererschmerz verbunden. Die Probleme sind einfach (denselben Speicher zweimal oder gar nicht freizugeben [memory leaks]nicht genügend Speicher zuweisen [buffer overflow], etc), aber schwer zu vermeiden und zu debuggen. Ein diszipliniertes Vorgehen ist in der Praxis zwingend erforderlich, aber die Sprache schreibt es natürlich nicht vor.

Ich möchte eine andere Art der Speicherzuweisung erwähnen, die von anderen Beiträgen ignoriert wurde. Es ist möglich, Variablen statisch zuzuweisen, indem man sie außerhalb jeder Funktion deklariert. Ich denke, im Allgemeinen bekommt diese Art der Zuordnung einen schlechten Ruf, weil sie von globalen Variablen verwendet wird. Es gibt jedoch nichts, was besagt, dass der einzige Weg, den auf diese Weise zugewiesenen Speicher zu verwenden, darin besteht, eine undisziplinierte globale Variable in einem Durcheinander von Spaghetti-Code zu verwenden. Die statische Zuweisungsmethode kann einfach verwendet werden, um einige der Fallstricke der Heap- und automatischen Zuweisungsmethoden zu vermeiden. Einige C-Programmierer sind überrascht zu erfahren, dass große und ausgeklügelte C-Embedded- und Spieleprogramme ohne jegliche Verwendung von Heap-Allokation erstellt wurden.

Hier finden Sie einige großartige Antworten zum Zuweisen und Freigeben von Speicher, und meiner Meinung nach besteht die größere Herausforderung bei der Verwendung von C darin, sicherzustellen, dass der einzige Speicher, den Sie verwenden, der Speicher ist, den Sie zugewiesen haben – wenn dies nicht richtig gemacht wird, was Sie beenden up with ist der Cousin dieser Site – ein Pufferüberlauf – und Sie überschreiben möglicherweise 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";
}

An diesem Punkt haben Sie 5 Bytes für myString zugewiesen und mit “abcd\0” gefüllt (Strings enden mit einer 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 nachgestellte Nullzeichen würde am Ende davon stehen – ein Teil des Speichers, der nicht für Ihre Verwendung zugewiesen wurde und sein könnte frei, könnte 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 haben wird.

  • Hier weisen 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

    23. September 2008 um 0:15 Uhr

  • Es hängt jedoch wirklich von dem Malloc ab, das Sie verwenden. Viele Malloc-Operatoren sind auf 8 Byte ausgerichtet. Wenn also dieser 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 3 zusätzliche Bytes für die Ausrichtung geben. Ich sage nicht, dass es eine gute Idee ist, dies zu verwenden, da es nur Systeme unterstützt, deren Malloc so funktioniert, aber es ist zumindest wichtig zu wissen, warum etwas falsch gemacht werden könnte.

    – Kodai

    15. Dezember 2009 um 1:11 Uhr

  • Loki: Ich habe die zu verwendende Antwort bearbeitet strcpy() Anstatt von =; Ich nehme an, das war die Absicht von Chris BC.

    – Echristopherson

    31. August 2013 um 19:08 Uhr

  • Ich glaube, dass der Hardware-Speicherschutz moderner Plattformen verhindert, dass Userspace-Prozesse die Adressräume anderer Prozesse überschreiben. Sie würden stattdessen einen Segmentierungsfehler erhalten. Aber das ist nicht Teil von C per se.

    – Echristopherson

    31. August 2013 um 19:23 Uhr

Benutzeravatar von Hernán
Hernán

Eine Sache, an die man sich erinnern sollte, ist stets Initialisieren Sie Ihre Zeiger auf NULL, da ein nicht initialisierter Zeiger eine pseudozufällige gültige Speicheradresse enthalten kann, die dazu führen kann, dass Zeigerfehler stillschweigend ausgeführt werden. Indem Sie erzwingen, dass ein Zeiger mit NULL initialisiert wird, können Sie immer abfangen, wenn Sie diesen Zeiger verwenden, ohne ihn zu initialisieren. Der Grund dafür ist, dass Betriebssysteme die virtuelle Adresse 0x00000000 mit allgemeinen Schutzausnahmen „verdrahten“, um die Verwendung von Nullzeigern abzufangen.

  • Hier weisen 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

    23. September 2008 um 0:15 Uhr

  • Es hängt jedoch wirklich von dem Malloc ab, das Sie verwenden. Viele Malloc-Operatoren sind auf 8 Byte ausgerichtet. Wenn also dieser 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 3 zusätzliche Bytes für die Ausrichtung geben. Ich sage nicht, dass es eine gute Idee ist, dies zu verwenden, da es nur Systeme unterstützt, deren Malloc so funktioniert, aber es ist zumindest wichtig zu wissen, warum etwas falsch gemacht werden könnte.

    – Kodai

    15. Dezember 2009 um 1:11 Uhr

  • Loki: Ich habe die zu verwendende Antwort bearbeitet strcpy() Anstatt von =; Ich nehme an, das war die Absicht von Chris BC.

    – Echristopherson

    31. August 2013 um 19:08 Uhr

  • Ich glaube, dass der Hardware-Speicherschutz moderner Plattformen verhindert, dass Userspace-Prozesse die Adressräume anderer Prozesse überschreiben. Sie würden stattdessen einen Segmentierungsfehler erhalten. Aber das ist nicht Teil von C per se.

    – Echristopherson

    31. August 2013 um 19:23 Uhr

Benutzeravatar von Serge
Serge

Außerdem möchten Sie möglicherweise die dynamische Speicherzuweisung verwenden, wenn Sie ein riesiges Array definieren müssen, z. B. int[10000]. Sie können es nicht einfach in den Stapel legen, weil Sie dann, hm … einen Stapelüberlauf bekommen.

Ein weiteres gutes Beispiel wäre eine Implementierung einer Datenstruktur, z. B. einer verketteten Liste oder eines binären Baums. Ich habe keinen Beispielcode zum Einfügen hier, aber Sie können ihn einfach googeln.

1420860cookie-checkC Speicherverwaltung

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy