Jede Variable in einem C-Programm lebt irgendwo, und wo bestimmt zwei Dinge, die du danach nicht mehr ändern kannst: wie lange sie überlebt und wie viel davon du haben kannst. C gibt dir drei Speicherbereiche, und falsch zu wählen erzeugt entweder einen Absturz oder ein Leck. Diese Seite legt sie aus und zeigt den klassischen Bug, der aus falschen Lebensdauern entsteht.
Die drei Bereiche
hohe Adressen
+---------------------------+
| Stack | Lokale, Parameter, Rücksprungadressen
| wächst nach unten | | bei Rückkehr automatisch freigegeben
| v |
+---------------------------+
| (ungenutzte Lücke)|
+---------------------------+
| ^ |
| wächst nach oben | |
| Heap | malloc- / calloc- / realloc-Blöcke
+---------------------------+ nur durch free() freigegeben
| statische/globale Daten | Globale und Statics, ganzer Lauf
+---------------------------+
| Code (Text) | der Maschinencode, nur lesbar
+---------------------------+
niedrige Adressen
- Automatischer Speicher (der Stack) hält Funktionsparameter und nicht-statische Lokale. Ein Stück Stack wird beim Betreten einer Funktion beansprucht und bei ihrer Rückkehr freigegeben. Die Größe steht zur Übersetzungszeit fest.
- Dynamischer Speicher (der Heap) hält alles aus
malloc,callocundrealloc. Die Größe wird zur Laufzeit bestimmt; die Lebensdauer endet erst beifree. - Statischer Speicher hält globale Variablen und alles, was
staticdeklariert ist. Er existiert für den gesamten Lauf des Programms und wird vor dem Start vonmainmit null initialisiert.
Die Adressen im Diagramm sind die übliche Anordnung, keine Garantie - der Standard beschreibt Lebensdauern, kein Layout.
Automatischer Speicher in Aktion
Jeder Aufruf von demo bekommt ein frisches local und ein frisches table. Nichts wird von Hand freigegeben, nichts kann lecken, und die Belegung kostet eine einzige Instruktion, die den Stackzeiger verschiebt. Deshalb sollten gewöhnliche Lokale deine Voreinstellung sein: Sie sind der schnellste und sicherste Speicher, den C hat.
Der Haken ist die schließende Klammer. Sobald sie läuft, ist dieser Speicher fort.
Der baumelnde Zeiger
Hier ist der Bug, den jeder C-Programmierer einmal schreibt:
/* KAPUTT: liefert die Adresse von Speicher, den es nicht mehr gibt */
int *make_counter(void) {
int count = 0;
return &count; /* count stirbt an dieser Klammer */
}
int main(void) {
int *p = make_counter();
*p = 5; /* schreibt in einen toten Stackrahmen */
return 0;
}
&count war eine vollkommen gültige Adresse, solange make_counter lief. Bei der Rückkehr wird dieser Stackplatz an die nächste aufgerufene Funktion gereicht, p zeigt also nun in fremde lokale Variablen. Lesen liefert Müll; Schreiben beschädigt sie. GCC und Clang warnen vor genau dieser Form (-Wreturn-local-addr), kompiliere also mit eingeschalteten Warnungen.
Derselbe Bug tarnt sich bei Arrays, und dort greift die Warnung oft nicht:
Die kaputte Fassung dieser Funktion würde den Text in einem lokalen char buf[64] bauen und return buf; schreiben - und damit die Adresse eines Puffers zurückgeben, der im selben Augenblick aufhört zu existieren.
Drei Wege, es zu beheben
1. Der Aufrufer liefert den Puffer (oben gezeigt). Keine Belegung, keine Besitzfrage, und der verbreitetste Stil in C-Bibliotheken. Die Funktion nimmt die Größe entgegen, damit sie darin bleiben kann.
2. Gib Heap-Speicher zurück und sag, wer ihn freigibt.
Der Heap-Block überlebt die Funktion von Entwurfs wegen - das ist der ganze Sinn von dynamischem Speicher. Der Preis ist der Besitzkommentar und das free des Aufrufers.
3. Nimm statischen Speicher, wenn ein einziger gemeinsamer Puffer akzeptabel ist:
static innerhalb einer Funktion hält den Gültigkeitsbereich der Variablen lokal und gibt ihr zugleich die Lebensdauer des Programms, ihre Adresse zurückzugeben ist also erlaubt. Der Handel ist, dass es davon immer nur eine gibt: Jeder Aufrufer teilt sie sich, was dieses Muster in nebenläufigem Code unbrauchbar und selbst in einsträngigem Code überraschend macht, wenn zwei Aufrufer den Zeiger gleichzeitig halten.
Größe: wo dem Stack der Platz ausgeht
Stackplatz ist klein und fest. Der Hauptthread bekommt typischerweise 1 MB unter Windows und 8 MB unter Linux; ein erzeugter Thread oft weit weniger. Der Heap ist durch den verfügbaren Systemspeicher begrenzt.
void bad(void) {
int huge[1000000]; /* ~4 MB Stack - stürzt beim Betreten wahrscheinlich ab */
huge[0] = 1;
}
Es gibt keine Meldung und kein NULL zum Prüfen: Das Programm stirbt einfach, meist mit einem Segmentierungsfehler, bevor die erste Zeile des Rumpfes läuft. Die Heap-Fassung meldet den Fehlschlag ordentlich:
Tiefe Rekursion erschöpft den Stack auf dieselbe Weise, Rahmen für Rahmen - eine entgleiste rekursive Funktion ist in der Praxis die häufigste Ursache eines Stacküberlaufs.
Kosten und Lokalität
Stackbelegung ist eine Rechenoperation auf einem Register. Heapbelegung ist ein Bibliotheksaufruf, der einen passenden Block sucht, womöglich eine Sperre nimmt und gelegentlich das Betriebssystem um mehr Speicher bittet. In einer heißen Schleife ist dieser Unterschied messbar.
Stackdaten sind außerdem kompakt und kürzlich angefasst, liegen also meist im Cache. Heapblöcke können verstreut sein. Keine dieser Tatsachen sollte einen Entwurf allein bestimmen - die Korrektheit der Lebensdauer kommt zuerst -, aber zwischen zwei gleichermaßen korrekten Entwürfen ist der mit dem Stack meist der schnellere.
Die Bereiche sichtbar machen
Adressen auszugeben macht das Layout greifbar. Die genauen Werte unterscheiden sich in jedem Lauf (moderne Systeme würfeln sie), aber die Gruppierung ist sichtbar:
Die globale und die statische Variable liegen nebeneinander; der Heapblock liegt woanders; die Lokale liegt typischerweise weit von beiden entfernt. Caste für %p nach void * - das verlangt die Formatangabe.
Die Wahl
Nimm den Stack, wenn:
- die Größe zur Übersetzungszeit bekannt ist,
- die Daten nur innerhalb dieser Funktion und der von ihr aufgerufenen gebraucht werden,
- und sie klein sind - ein paar Kilobyte, keine Megabyte.
Nimm den Heap, wenn:
- die Größe von einer Eingabe, einer Datei oder einer Berechnung abhängt,
- die Daten die Funktion überleben müssen, die sie erzeugt hat,
- oder sie groß genug sind, um die Stackgrenze zu bedrohen.
Nimm statischen Speicher, wenn:
- genau eine Instanz für das ganze Programm existieren soll,
- und das Teilen zwischen allen Aufrufern wirklich korrekt ist.
Die Voreinstellung ist der Stack. Greife zum Heap, wenn einer seiner drei Gründe zutrifft, und folge dann den Besitzregeln aus Speicherlecks, damit der Block, für den du eine Lebensdauer gewonnen hast, auch wieder freigegeben wird.
Zwei spiegelbildliche Bugs
Sie lohnen sich gemeinsam zu benennen, denn sie sind dieselbe Lebensdauerfrage, zweifach beantwortet:
- Baumelnder Zeiger - der Speicher starb vor dem Zeiger.
&localzurückgeben oder einen Zeiger nachfreebenutzen. Das Programm liest oder schreibt Speicher, der nun etwas anderem gehört. - Speicherleck - der Zeiger starb vor dem Speicher. Die letzte Referenz auf einen
malloc-Block verlieren. Nichts geht sofort kaputt; der Prozess wächst einfach.
Beide entstehen aus einer Fehlpaarung zwischen der nötigen Lebensdauer der Daten und dem Bereich, in den du sie legst. Entscheide zuerst die Lebensdauer, und der Bereich ergibt sich.
Häufig gestellte Fragen
Was ist der Unterschied zwischen Stack und Heap in C?
Der Stack hält lokale Variablen: Der Compiler bemisst sie, sie entstehen beim Betreten einer Funktion und werden bei ihrer Rückkehr zerstört, und die Belegung kostet nichts. Der Heap hält malloc-Blöcke: Du wählst die Größe zur Laufzeit, der Block überlebt, bis du ihn mit free freigibst, und die Belegung hat echte Kosten.
Warum kann ich in C keinen Zeiger auf eine lokale Variable zurückgeben?
Weil der Speicher der Lokalen in dem Moment freigegeben wird, in dem die Funktion zurückkehrt. Der Zeiger hält weiterhin diese Adresse, aber der Speicher gehört nun dem nächsten Funktionsaufruf - ihn zu lesen liefert Müll, ihn zu schreiben beschädigt fremde Daten. Das ist ein baumelnder Zeiger. Gib einen malloc-Block zurück, oder lass den Aufrufer den Puffer liefern.
Wie groß ist der Stack in C?
Typischerweise 1 bis 8 MB für den Hauptthread und deutlich weniger für zusätzliche Threads - klein genug, dass int big[1000000]; als Lokale das Programm beim Betreten meist zum Absturz bringt. Der Heap ist durch den verfügbaren Systemspeicher begrenzt, große Daten oder Daten unbekannter Größe gehören also dorthin.
Wann sollte ich in C den Heap statt des Stacks benutzen?
Drei Fälle: Die Größe ist erst zur Laufzeit bekannt, die Daten müssen die Funktion überleben, die sie erzeugt hat, oder der Block ist zu groß für den Stack (grob alles über ein paar hundert Kilobyte). Alles andere sollte eine schlichte Lokale sein - das ist schneller und kann nicht lecken.