- Lernen
- Informatik & Informationswissenschaften
- Programmierung
- C-Programmierung – Zeiger, Speicherverwaltung und sicheres C
- Dynamische Speicherverwaltung in C
Dynamische Speicherverwaltung in C
AnfängerEinheit 5 von 8 in Kurs: C-Programmierung – Zeiger, Speicherverwaltung und sicheres C
Was du lernst
- Beschreibe Stack, Heap und statischen Speicher, wie lange Daten jeweils existieren und wann ein Programm Heap-Speicher benötigt.
- Stack, Heap und statischen Speicher vergleichenBeschreibe, wie sich automatischer, statischer und allokierter Speicher in Lebensdauer und Verwaltung unterscheiden und was bei Funktionsaufrufen mit ihnen geschieht.
- Entscheiden, wann Daten auf dem Heap gespeichert werden solltenWähle anhand von Größe, Lebensdauer und Wachstum der Daten zwischen lokalen Variablen und Speicherallokation auf dem Heap und vermeide es, die Adresse einer lokalen Variablen zurückzugeben.
- Stack, Heap und statischen Speicher vergleichen
- Reserviere mit malloc und calloc Blöcke in der richtigen Größe, prüfe, ob jede Anforderung erfolgreich war, und gib jeden Block unter einem klar festgelegten Besitzer genau einmal frei.
- Speicher mit malloc und sizeof anfordernBerechne die anzufordernde Bytezahl, rufe malloc auf, prüfe auf Fehlschlag und verwende den Block als Array.
- Mit calloc mit Nullen gefüllten Speicher anfordernVerwende calloc, um Arrays zu allokieren, deren Elemente anfangs null sind, und erkenne, wann es die bessere Wahl als malloc ist.
- Speicher mit free freigeben und den Besitz im Blick behaltenGib jeden Block zum richtigen Zeitpunkt genau einmal frei, entscheide, welcher Programmteil den jeweiligen Block besitzt, und räume bei einer fehlgeschlagenen Speicherallokation korrekt auf.
- Speicher mit malloc und sizeof anfordern
- Vergrößere und verkleinere Heap-Blöcke, ohne Daten oder Speicher zu verlieren, und implementiere Arrays, die beim Eintreffen von Daten wachsen.
- Einen Speicherblock mit realloc sicher vergrößern und verkleinernFange das Ergebnis von realloc in einem temporären Zeiger auf, bewahre bei einem Fehlschlag die Daten und den ursprünglichen Block und aktualisiere alle Zeiger, nachdem der Block verschoben wurde.
- Ein vergrößerbares Array implementierenSpeichere neben dem Zeiger eine Länge und eine Kapazität, vergrößere die Kapazität geometrisch und lies Eingaben unbekannter Länge in einen Puffer ein, der mitwächst.
- Einen Speicherblock mit realloc sicher vergrößern und verkleinern
- Erkenne Speicherlecks, hängende Zeiger, doppelte Speicherfreigaben, Zugriffe außerhalb der Speichergrenzen und Lesezugriffe auf nicht initialisierte Werte in C-Code, erkläre ihre Gefahren und verfolge eine Speicherzugriffsverletzung bis zu ihrer Ursache zurück.
- Speicherlecks finden und verhindernErkläre, was ein Speicherleck ist, erkenne Codeformen, die Speicherlecks verursachen, und wende Gewohnheiten an, die sie verhindern.
- Hängende Zeiger, Zugriffe nach der Speicherfreigabe und doppelte Speicherfreigaben vermeidenErkenne Zeiger auf Speicher, dessen Lebensdauer beendet ist, erkläre, warum ihre erneute Verwendung oder Freigabe undefiniertes Verhalten ist, und wende Gewohnheiten an, die dies verhindern.
- Innerhalb des Speicherblocks bleiben: Pufferüberläufe und Lesezugriffe auf nicht initialisierte WerteHalte jeden Zugriff innerhalb der Grenzen seines Blocks, bemesse Zeichenkettenpuffer korrekt und setze Speicherwerte vor dem Lesen.
- Speicherzugriffsverletzungen erklären und ihre Ursachen zurückverfolgenErkläre, was eine Speicherzugriffsverletzung ist, nenne ihre häufigen Ursachen und verfolge den Absturz bis zur fehlerhaften Zeile zurück.
- Speicherlecks finden und verhindern
- Führe Programme unter Valgrind Memcheck und AddressSanitizer aus, lies ihre Berichte und behebe damit Speicherlecks und ungültige Zugriffe, bis ein Programm fehlerfrei läuft.
- Ein Programm mit Valgrind Memcheck prüfenErstelle ein Programm für Valgrind, führe es mit Speicherleckprüfung aus und lies seine Fehlermeldungen, Heap-Übersicht und Speicherleckkategorien.
- AddressSanitizer und LeakSanitizer verwendenErstelle ein Programm mit AddressSanitizer, lies dessen Berichte und wähle zwischen Sanitizern und Valgrind.
- Speicherfehler anhand eines Prüfberichts behebenGehe nach einem wiederholbaren Ablauf vom Prüfbericht zur fehlerhaften Zeile, behebe die Ursache statt des Symptoms und bestätige die Korrektur mit einem fehlerfreien Lauf.
- Ein Programm mit Valgrind Memcheck prüfen
Dynamische Speicherverwaltung in C einfach erklärt
1Erklären, wo ein C-Programm seine Daten speichert
Jedes Objekt hat eine Lebensdauer, und ein Zeiger ist nur nützlich, solange das Objekt existiert, auf das er zeigt. Für lokale Variablen, statische Daten und allokierte Blöcke gelten unterschiedliche Regeln zur Lebensdauer. Verbindest du diese Regeln mit Stack und Heap, verstehst du, warum eine Funktion ein allokiertes Array, aber keine nutzbare Adresse ihres eigenen lokalen Arrays zurückgeben kann. Größe, Lebensdauer und späteres Wachstum bestimmen die Speicherwahl für die jeweilige Aufgabe.
1.1 Speicher und Lebensdauer
Die Adresse einer Variablen zeigt, wo ihre Daten liegen. Ihre Speicherdauer bestimmt, wie lange diese Daten existieren. Eine Adresse zu behalten, verlängert die Lebensdauer des Objekts nicht.
Drei Speicherdauern
| Speicherdauer | Typische Objekte | Ende der Lebensdauer |
|---|---|---|
| Automatisch | Gewöhnliche lokale Variablen und Parameter | Wenn ihr Block endet |
| Statisch | Globale Variablen und als static deklarierte lokale Variablen | Wenn das Programm endet |
| Allokiert | Von Speicherallokationsfunktionen erhaltene Blöcke | Wenn der Block freigegeben wird |
Auf gängigen Systemen liegen automatische Objekte auf dem Aufrufstapel, allokierte Blöcke auf dem Heap. C legt die Lebensdauern fest, schreibt diese physischen Bereiche aber nicht vor. Maschinencode und statische Daten belegen andere Bereiche, deren Anordnung vom System abhängt.
Ein Funktionsaufruf erzeugt gewöhnlich einen Stackframe mit seinen Parametern, lokalen Variablen und Rückkehrinformationen. Aufrufe werden verschachtelt: Der zuletzt angelegte Stackframe wird zuerst entfernt. Die Rückkehr gibt den automatischen Speicher dieses Aufrufs frei, sodass spätere Aufrufe den Platz wiederverwenden können. Der Allokator des Heaps verwaltet einen eigenen Speicherbereich. Auf Anforderung reserviert er einen zusammenhängenden Block und liefert dessen Anfangsadresse. Die Rückkehr aus einer Funktion gibt diesen Block nicht frei.
Zeiger und Block sind getrennte Objekte
Betrachte diese Funktion, für die <stdlib.h> verfügbar ist:
int *make_array(size_t n) {
if (n == 0 || n > SIZE_MAX / sizeof(int)) return NULL;
int *p = malloc(n * sizeof *p);
if (p == NULL) return NULL;
return p;
}
SIZE_MAX stammt aus <stdint.h>. Ruft main die Funktion mit int *nums = make_array(5); auf, gehören der Parameter n und die Zeigervariable p zum Aufruf. Das allokierte Array gehört nicht dazu. Wenn ein int 4 Byte belegt, umfasst der Block 20 Byte. Die Rückgabe kopiert seine Adresse nach nums, nicht das Array. Der Aufrufer prüft nums, verwendet die Elemente, nachdem er sie gesetzt hat, und gibt den Block schließlich frei.
Die lokale Variable p endet mit der Rückkehr der Funktion. Der Block bleibt bestehen, weil seine Speicherdauer allokiert und nicht automatisch ist. Die Unterscheidung gilt auch umgekehrt: Ein lokaler Zeiger kann auf ein globales Objekt zeigen und ein globaler Zeiger auf einen Heap-Block.
Statische Daten überdauern Aufrufe
int next_id(void) {
static int id = 0;
return ++id;
}
Aufeinanderfolgende Aufrufe geben 1, 2 und 3 zurück. Die Initialisierung erfolgt einmal, und id existiert während der gesamten Programmausführung. Ohne static entsteht bei jedem Aufruf eine neue automatische Variable id, die mit null initialisiert wird; dann gibt jeder Aufruf 1 zurück.
Stack-Speicher ist begrenzt
Sowohl ein großes lokales Array als auch tief verschachtelte Aufrufe verbrauchen Stack-Speicher. Das Überschreiten der Grenze ist ein Stapelüberlauf, der das Programm meist beendet. Viele Linux-Systeme erlauben einen 8 MiB großen Stack; Microsoft reserviert standardmäßig für den Hauptthread häufig 1 MiB. Das sind Systemeinstellungen, keine C-Regeln. Ein lokales Array aus 4.000.000 ints benötigt 16.000.000 Byte, wenn ein int 4 Byte belegt, also etwa 15,3 MiB.
Bei der Speicherallokation auf dem Heap entsteht beim Reservieren und Freigeben von Blöcken Verwaltungsaufwand. Das Lesen eines Elements ist nicht schon deshalb langsamer, weil sein Array auf dem Heap liegt. Außerdem hat der Heap-Speicherbereich nichts mit der Heap-Datenstruktur einer Prioritätswarteschlange zu tun.
Wichtige Erkenntnisse
- Die Speicherdauer eines Objekts bestimmt seine Lebensdauer.
- Automatische lokale Variablen enden mit ihrem Block; statische Objekte existieren während der Programmausführung.
- Ein lokaler Zeiger und der Heap-Block, auf den er zeigt, haben getrennte Lebensdauern.
- Große lokale Arrays und tiefe Aufrufverschachtelungen können den Stack erschöpfen.
- C legt Lebensdauern fest; Stack und Heap beschreiben gängige Implementierungen.
1.2 Den passenden Speicher für die Aufgabe wählen
Wähle den Speicher danach, wie viel Platz benötigt wird, wann die Größe bekannt wird und wie lange die Daten existieren müssen. Oft ist ein gewöhnliches lokales Array die einfachste geeignete Lösung.
Den Speicher an die Daten anpassen
| Bedarf | Geeignete Ausgangswahl |
|---|---|
| Ein kleines festes Array während eines Aufrufs | Lokales Array |
| Eine aus der Eingabe gelesene Anzahl | Geprüfte Speicherallokation auf dem Heap |
| Ein Ergebnis, das seine erzeugende Funktion überdauert | Speicherallokation auf dem Heap oder Puffer im Besitz des Aufrufers |
| Daten, die mit der Eingabe wachsen | Größenveränderbarer Heap-Puffer |
| Ein sehr großes Array | Speicherallokation auf dem Heap mit Prüfung auf Fehlschlag |
Die Tagestemperaturen einer Woche benötigen sieben Werte, daher ist double temps[7]; ein naheliegendes lokales Array. Für eine Note pro studierender Person wird die Größe erst zur Laufzeit ermittelt. Verwende bei n Studierenden nach einer Größenprüfung double *grades = malloc(n * sizeof *grades);. Bei 250 Studierenden und doubles zu 8 Byte fordert das 2.000 Byte an. Prüfe dann das Ergebnis, bevor du das Array füllst.
Warum die Rückgabe eines lokalen Puffers fehlschlägt
char *greeting(void) {
char buf[16] = "hello";
return buf; /* error: buf ends when this call returns */
}
Die zurückgegebene Adresse kann die Lebensdauer von buf nicht verlängern. Der Aufrufer erhält einen hängenden Zeiger, selbst wenn die alten Bytes unverändert erscheinen. GCC und Clang können vor diesem Muster warnen.
Eine Lösung besteht darin, den Aufrufer Speicher bereitstellen zu lassen:
#include <string.h>
void greeting(char out[16]) {
strcpy(out, "hello");
}
/* In the caller: */
char message[16];
greeting(message);
Hier existiert das Array des Aufrufers während des gesamten Aufrufs und danach weiter. Der Parameter verhält sich weiterhin wie ein Zeiger; der Aufrufer muss genügend Platz bereitstellen. Eine zweite Lösung allokiert ein Ergebnis auf dem Heap und macht den Aufrufer für dessen Freigabe verantwortlich. Eine dritte gibt einen statischen Puffer zurück. Dieser Puffer bleibt bestehen, wird aber von allen Aufrufern gemeinsam genutzt, und ein späterer Aufruf kann seinen Inhalt überschreiben. Wähle danach, ob die Aufrufer unabhängige Ergebnisse benötigen.
Eine Größe zur Laufzeit bedeutet nicht immer Heap
C99 führte Arrays variabler Länge wie int a[n]; ein. Sie haben automatische Speicherdauer und liegen gewöhnlich auf dem Stack. C11 machte ihre Unterstützung optional, und Microsofts Compiler unterstützt sie nicht. Eine große Eingabe kann den Stack erschöpfen, und die Deklaration des Arrays liefert kein Nullergebnis, das sich prüfen lässt. Die Speicherallokation auf dem Heap bietet einen prüfbaren Fehlerpfad und erlaubt spätere Größenänderungen.
Ein Zeiger kennzeichnet keinen Besitz
int value = 3;
int *p = &value;
p zeigt auf ein automatisches Objekt. Ihn an free zu übergeben ist ungültig. Dieselbe Regel schließt die Freigabe eines statischen Arrays oder eines Zeichenkettenliterals aus. Die Adresse muss zu einer noch bestehenden Speicherallokation gehören, die das Programm besitzt.
Ebenso zeigt char *s = "hello"; auf ein Zeichenkettenliteral; es erzeugt keine beschreibbare Kopie. Das Literal zu verändern ist undefiniertes Verhalten. Verwende char s[] = "hello"; für eine beschreibbare lokale Kopie oder allokiere Platz für eine separate Kopie auf dem Heap.
Wichtige Erkenntnisse
- Wähle den Speicher anhand von Datengröße, erforderlicher Lebensdauer und Wachstum.
- Eine von einer Funktion zurückgegebene Adresse ist nur nutzbar, wenn das Objekt, auf das sie zeigt, noch existiert.
- Bei einem vom Aufrufer bereitgestellten Puffer bleibt der Besitz beim Aufrufer.
- Ein statischer Ergebnispuffer wird von allen Aufrufen gemeinsam genutzt.
- Zeiger können auf lokale, statische oder allokierte Objekte zeigen; nur noch bestehende allokierte Blöcke können freigegeben werden.
- Zeichenkettenliterale sind keine beschreibbaren Kopien.
2Heap-Speicher reservieren und freigeben
Eine Speicheranforderung benötigt eine Bytezahl, einen Fehlerpfad und einen Besitzer. Ermittle aus der Elementanzahl den benötigten Speicher, entscheide, ob die Anfangswerte null sein sollen, und verwende jeden Block erst nach einer erfolgreichen Anforderung. Der Besitz verbindet dann die letzte Verwendung mit dem Aufräumen, auch bei verschachtelten Speicherallokationen und Funktionen, die nur einen Teil ihrer Arbeit erfolgreich abschließen.
2.1 Die richtige Bytezahl anfordern
malloc reserviert einen zusammenhängenden Block aus Bytes und gibt seine Anfangsadresse zurück. Binde für die Deklaration <stdlib.h> ein. Eine erfolgreiche Speicherallokation stellt Speicher bereit, füllt ihn aber nicht mit brauchbaren Werten.
Elemente zählen und ihre Größe bestimmen
int *nums = malloc(n * sizeof *nums);
n ist eine Elementanzahl; sizeof *nums ist die Bytezahl pro Element. Ihre Multiplikation ergibt die Bytezahl. Wenn ein int 4 Byte belegt, benötigen zehn ints 40 Byte. Zehn doubles zu je 8 Byte benötigen 80 Byte. Zehn Zeiger benötigen 80 Byte, wenn jeder Zeiger 8 Byte belegt. Ein char ist immer ein C-Byte groß.
Die Form sizeof *nums richtet sich nach dem Typ, auf den der Zeiger zeigt, auch wenn sich die Deklaration ändert. Bei diesem gewöhnlichen Typ ohne variable Länge wertet sizeof den Ausdruck *nums nicht aus und dereferenziert daher keinen nicht initialisierten Zeiger. In C wird der zurückgegebene void * ohne explizite Typumwandlung in den Objektzeigertyp umgewandelt.
malloc(n) allokiert n Byte, nicht Platz für n ints. sizeof nums misst die Zeigervariable, nicht ein Element oder den ganzen Block. Auf einem System mit 8 Byte großen Zeigern und 4 Byte großen ints fordert malloc(n * sizeof nums) doppelt so viel Speicher wie nötig an und verdeckt so den fehlerhaften Ausdruck. Bei anderen Typen kann zu wenig Speicher allokiert werden.
Größe und Fehlschlag vor der Verwendung prüfen
Dieses vollständige Programm weist eine leere Anforderung zurück, schützt die Multiplikation vor Überlauf, prüft die Speicherallokation und initialisiert jedes Element:
#include <stdint.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *nums;
if (n == 0 || n > SIZE_MAX / sizeof *nums) {
return EXIT_FAILURE;
}
nums = malloc(n * sizeof *nums);
if (nums == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < n; ++i) {
nums[i] = (int)(i * i);
}
free(nums);
return EXIT_SUCCESS;
}
SIZE_MAX aus <stdint.h> ist der größte size_t-Wert. Prüfe durch Division vor der Multiplikation. Ergibt das Produkt zuerst durch Wraparound einen kleineren Wert, lässt sich die beabsichtigte Größe durch dessen Prüfung nicht wiederherstellen. Eine Anforderung ungleich null kann auch dann fehlschlagen, wenn die Berechnung im Wertebereich liegt. Deshalb ist die Prüfung auf einen Nullzeiger ein eigener Schritt.
Die initialisierten Werte sind hier 0, 1, 4, 9 und 16. nums[i] greift auf dasselbe Element zu wie *(nums + i). Zeigerarithmetik bewegt sich in Elementen weiter, daher multipliziert der Code i nicht mit der Bytegröße.
Die Anzahl zum Zeiger speichern
In Standard-C gibt es keine Operation, die aus einem Zeiger die Elementanzahl einer Speicherallokation zurückgewinnt. Speichere n gesondert und übergib Zeiger und Anzahl gemeinsam an Funktionen. Die Anzahl steuert Schleifen und Indexprüfungen.
Eine kopierte Zeichenkette benötigt ihre Zeichen und das abschließende '\0'. Die Zeichenkette „Amsterdam“ hat neun Zeichen und benötigt zehn Byte. Fordere daher strlen(s) + 1 für ihre Kopie an. <string.h> stellt strlen bereit.
Eine malloc-Anforderung der Größe null kann einen Nullzeiger oder einen Zeiger zurückgeben, der freigegeben, aber nicht dereferenziert werden darf. Behandle die Anzahl null ausdrücklich als leeren Fall, statt einen solchen Zeiger als nutzbaren Arrayspeicher zu behandeln.
Wichtige Erkenntnisse
- Multipliziere für die Bytezahl eines Arrays die Elementanzahl mit
sizeof *pointer. - Prüfe vor dem Aufruf von
malloc, ob die Multiplikation überläuft. - Prüfe auf
NULL, bevor du das Ergebnis verwendest. - Initialisiere jedes mit
mallocallokierte Element vor dem Lesen. - Speichere die Elementanzahl gesondert;
sizeof pointerkann sie nicht ermitteln. - Berücksichtige bei der Speicherallokation für eine Zeichenkettenkopie das abschließende Nullzeichen.
- Behandle die Anzahl null ausdrücklich.
2.2 Ein Array mit null initialisieren
Ein Zähler benötigt vor der ersten Erhöhung einen definierten Anfangswert. calloc verbindet Speicherallokation und Nullsetzen, sodass ein Ganzzahl-Zählerarray nach der Prüfung auf Fehlschlag zum Zählen bereit ist.
Zwei Argumente beschreiben das Array
Binde <stdlib.h> ein und schreibe:
int *tally = calloc(26, sizeof *tally);
if (tally == NULL) return EXIT_FAILURE;
Das erste Argument ist die Elementanzahl, das zweite die Größe eines Elements. Wenn ein int 4 Byte belegt, werden damit 104 Byte reserviert. Jedes Byte beginnt bei null, sodass jeder Ganzzahlzähler bei null beginnt. Ein Aufruf von calloc mit einem zusammengefassten Argument lässt sich nicht kompilieren; die Schnittstelle verlangt zwei Argumente.
Erhöhe für das Wort „banana“ den Zähler des jeweiligen Buchstabens. Nach der Verarbeitung enthalten die Zähler für a, b und n die Werte 3, 1 und 2. Alle anderen bleiben null. Bei einer Zeichenkodierung mit aufeinanderfolgenden Kleinbuchstaben lautet der Indexausdruck c - 'a'. Die Eingabe muss ein Kleinbuchstabe sein, bevor dieser Ausdruck als Arrayindex verwendet wird.
#include <stdlib.h>
int main(void) {
int *counts = calloc(3, sizeof *counts);
if (counts == NULL) return EXIT_FAILURE;
counts[1]++;
counts[1]++;
/* The counters now hold 0, 2, 0. */
free(counts);
return EXIT_SUCCESS;
}
Der Zählcode hat nur den mittleren Zähler beschrieben. Seine beiden Nachbarn haben weiterhin brauchbare Werte, weil sie bei der Speicherallokation auf null gesetzt wurden.
Was „mit Nullen gefüllt“ bedeutet
calloc setzt jedes Bit auf null. Dadurch erhalten Ganzzahl- und char-Elemente den Wert null. Ein auf null gesetzter Zeichenpuffer mit positiver Größe beginnt daher als leere Zeichenkette: Sein erstes Zeichen ist '\0'.
Auf gängigen Plattformen stehen ausschließlich auf null gesetzte Bits auch für Gleitkomma-Nullwerte und Nullzeiger. Portables C garantiert diese Darstellungen für Gleitkomma- und Zeigertypen nicht. Benötigt Code solche Werte auf allen C-Implementierungen, weise den Elementen ausdrücklich 0.0 oder NULL zu.
Zwischen den Speicherallokationsfunktionen wählen
| Erste Verwendung | Naheliegende Wahl |
|---|---|
| Ganzzahlzähler erhöhen | calloc |
| Ganzzahlmerker nach und nach aus einem Zustand setzen, in dem alle null sind | calloc |
| Jedes Element sofort überschreiben | malloc |
Ein erfolgreiches malloc mit anschließendem memset kann einen Block auf null setzen, aber die Größenmultiplikation in diesem Code benötigt weiterhin eine Überlaufprüfung. Gängige calloc-Implementierungen weisen ein für size_t zu großes Produkt zurück; C23 schreibt dies ausdrücklich vor. Das Ergebnis muss trotzdem auf einen Nullzeiger geprüft werden, weil auch eine korrekt bemessene Anforderung fehlschlagen kann.
Das Nullsetzen gilt für diese Speicherallokation. Vergrößert realloc den Block später, sind die hinzugefügten Bytes nicht initialisiert. Setze diese Elemente, bevor du sie zum Zählen verwendest oder liest. Gib den Block schließlich mit free frei, genau wie einen mit malloc erhaltenen Block.
Wichtige Erkenntnisse
- Übergib
calloceine Elementanzahl und eine Elementgröße. - Mit Nullen gefüllte Ganzzahlzähler können unmittelbar nach der Prüfung der Speicherallokation erhöht werden.
- Ausschließlich auf null gesetzte Bytes sind kein portabler Ersatz für die Zuweisung von Gleitkomma-Nullwerten oder Nullzeigern.
- Beim Vergrößern mit
reallocwerden die hinzugefügten Bytes nicht auf null gesetzt.
2.3 Ein Besitzer und eine abschließende Freigabe
Für einen allokierten Block braucht die Frage „Wer gibt ihn frei?“ eine klare Antwort. Diese Verantwortung ist der Besitz. Anderer Code kann die Adresse ausleihen, ohne die Erlaubnis zur Freigabe zu erhalten.
Nach der letzten Verwendung freigeben
free, deklariert in <stdlib.h>, stellt einen noch bestehenden allokierten Block zur Wiederverwendung bereit. Übergib seine Anfangsadresse genau einmal. free(NULL) bewirkt nichts. Die Übergabe einer lokalen Adresse, eines Zeichenkettenliterals, einer Adresse im Blockinneren wie p + 1 oder eines bereits freigegebenen Blocks führt zu undefiniertem Verhalten.
free(p) weist der Variablen p nichts zu und verspricht nicht, die Bytes zu löschen. Nach der Freigabe ist der alte Zeigerwert ungültig. Bleibt die Variable in Verwendung, weise p = NULL; zu, ohne den alten Wert zu untersuchen. Das ändert nur p; über Aliase darf weiterhin nicht auf den freigegebenen Block zugegriffen werden.
Eine Funktion, die neu allokierten Speicher zurückgibt, überträgt den Besitz an ihren Aufrufer:
#include <stdlib.h>
#include <string.h>
char *copy_string(const char *s) {
char *copy = malloc(strlen(s) + 1);
if (copy == NULL) return NULL;
strcpy(copy, s);
return copy;
}
Der Aufrufer prüft das Ergebnis, verwendet die Kopie und gibt sie frei. Die eingegebene Zeichenkette ist geliehen; diese Funktion gibt sie nicht frei. POSIX und C23 bieten strdup für dasselbe Besitzmuster an; die obige Funktion funktioniert auch in C17.
Verschachtelte Speicherallokationen von innen nach außen freigeben
Eine Tabelle aus einem Array von Zeilenzeigern besteht aus getrennten Blöcken. Drei Zeilenzeiger und drei Zeilen aus je vier ints benötigen vier Speicherallokationen. Bei Zeigern zu 8 Byte und ints zu 4 Byte belegt der äußere Block 24 Byte und die Zeilen belegen 48 Byte, insgesamt 72 Byte.
Gib jede Zeile frei, solange ihre Adresse noch verfügbar ist, und gib dann das Zeigerarray frei. Die Freigabe des äußeren Blocks gibt seine Zeilen nicht rekursiv frei. Ein Zugriff auf grid[i] nach der Freigabe von grid verwendet freigegebenen Speicher.
Nach teilweisem Erfolg aufräumen
Dieses vollständige Beispiel hält fest, wie viele Zeilen tatsächlich existieren:
#include <stdlib.h>
int main(void) {
size_t rows = 3, made = 0;
int **grid = malloc(rows * sizeof *grid);
if (grid == NULL) return EXIT_FAILURE;
for (; made < rows; ++made) {
grid[made] = malloc(4 * sizeof *grid[made]);
if (grid[made] == NULL) break;
}
int status = made == rows ? EXIT_SUCCESS : EXIT_FAILURE;
/* Use the grid here only if every row was allocated. */
for (size_t i = 0; i < made; ++i) free(grid[i]);
free(grid);
return status;
}
Schlägt die Allokation der zweiten Zeile fehl, ist made gleich 1, sodass nur Zeile null freigegeben wird. Dabei wird kein nicht initialisierter Zeilenzeiger gelesen. Eine weitere nützliche Lösung setzt unabhängige Zeiger anfangs auf NULL und leitet jeden Fehlschlag zu einem gemeinsamen Aufräumabschnitt. Dank free(NULL) ist dieser Abschnitt auch nach teilweisem Erfolg sicher.
Zähle Besitzzeiträume, nicht Allokationsaufrufe. Ein mit malloc erzeugter und zweimal erfolgreich in der Größe geänderter Puffer benötigt ein abschließendes free für das letzte Ergebnis; ein erfolgreiches realloc beendet bereits die Lebensdauer des vorherigen Blocks. Der Allokator kann freigegebenen Platz für spätere Speicherallokationen behalten, sodass ein Systemmonitor keinen sofortigen Rückgang des Prozessspeichers zeigen muss.
Wichtige Erkenntnisse
- Der Besitzer gibt jeden Block nach dessen letzter Verwendung frei.
- Nur die Anfangsadresse einer noch bestehenden Speicherallokation oder
NULList ein gültiges Argument fürfree. - Das Zurücksetzen eines Zeigers korrigiert keine Aliase.
- Gib innere Blöcke vor dem äußeren Block frei, der ihre Adressen enthält.
- Fehlerpfade müssen alle zuvor erhaltenen Blöcke freigeben.
- Nach erfolgreicher Größenänderung muss der letzte Block schließlich einmal mit
freefreigegeben werden.
3Die Größe von Puffern mit realloc ändern
Bei eintreffenden Eingaben ist die endgültige Größe nicht immer bekannt. Ein größenveränderbarer Puffer erlaubt einem Programm, mehr Platz zu reservieren, sobald es den Bedarf kennt. Sicheres Wachstum erhält sowohl vorhandene Werte als auch die Möglichkeit, einen Fehlschlag zu behandeln. Mit einem Zeiger, einer Länge und einer Kapazität lassen sich diese Regeln konkret umsetzen; geometrisches Wachstum begrenzt dabei das wiederholte Kopieren.
3.1 Die Größe ändern, ohne den alten Block zu verlieren
realloc ändert die Größe eines allokierten Blocks. Die Funktion kann den Speicher vergrößern oder verkleinern, und die zurückgegebene Adresse kann sich in beiden Fällen ändern. Binde <stdlib.h> ein und behandle Erfolg und Fehlschlag als unterschiedliche Ereignisse für die Lebensdauer.
Was erhalten bleibt und was sich ändert
Wächst ein Block, bleibt der vorhandene Inhalt bis zur alten Größe erhalten. Hinzugefügte Bytes sind nicht initialisiert. Schrumpft er, bleibt nur der Anfangsteil erhalten, der in die neue Größe passt. Ein Array mit 3, 1, 4 und 1 behält diese vier Werte, wenn es auf sechs ints vergrößert wird; den letzten beiden Elementen müssen vor der Verwendung Werte zugewiesen werden. Bei ints zu 4 Byte ändert sich die Anforderung damit von 16 auf 24 Byte.
Der Allokator kann den Block an Ort und Stelle erweitern oder anderswo Speicher allokieren, die erhaltenen Bytes kopieren und den alten Block freigeben. Dein Code muss mit beiden Ergebnissen funktionieren.
Das Ergebnis in einem temporären Zeiger auffangen
int *tmp = realloc(nums, 6 * sizeof *nums);
if (tmp == NULL) {
/* nums still owns its original block. */
} else {
nums = tmp;
nums[4] = 5;
nums[5] = 9;
}
Bei dieser Anforderung einer positiven Größe bleibt der ursprüngliche Block im Fehlerfall allokiert und unverändert. Das Programm kann ihn weiterverwenden oder vor der Fehlermeldung freigeben. Bei Erfolg tritt der zurückgegebene Block an die Stelle des bisherigen Blocks im Besitz des Programms. Weise das neue Ergebnis zu, bevor du auf das Array mit geänderter Größe zugreifst.
Die direkte Zuweisung nums = realloc(nums, bytes); bietet diesen Schutz nicht. Schlägt der Aufruf fehl, überschreibt er die einzige gespeicherte Adresse mit NULL. Der alte Speicherblock bleibt reserviert, aber das Programm kann ihn nicht mehr freigeben.
Gespeicherte Elementzeiger müssen neu berechnet werden
Angenommen, der Code speichert int *selected = &nums[2];. Eine erfolgreiche Größenänderung beendet die Lebensdauer des alten Objekts, selbst wenn die zurückgegebene Adresse zufällig übereinstimmt. Danach ist selected ungültig. Speichere über die Größenänderung hinweg den Index 2 und berechne anschließend selected = &nums[2]; anhand des neuen Zeigers, falls dieser Index noch existiert.
Vergleiche den alten Zeiger nicht mit dem Ergebnis, um zu entscheiden, ob Aliase aktualisiert werden müssen. Gehe bei jedem Erfolg vom neuen Ergebnis aus. Wurde das ausgewählte Element beim Verkleinern entfernt, gibt es keinen Elementzeiger, der neu berechnet werden könnte.
Ein vollständiges Beispiel zum Vergrößern und Freigeben
#include <stdlib.h>
int main(void) {
int *nums = malloc(4 * sizeof *nums);
if (nums == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < 4; ++i) nums[i] = (int)i;
int *tmp = realloc(nums, 6 * sizeof *nums);
if (tmp == NULL) {
free(nums);
return EXIT_FAILURE;
}
nums = tmp;
nums[4] = 4;
nums[5] = 5;
free(nums);
return EXIT_SUCCESS;
}
Prüfe bei einer variablen Anzahl vor diesem Aufruf die Multiplikation zur Berechnung der Bytezahl, genau wie bei malloc. Fordere Bytes an, keine Elemente: realloc(nums, 6) fordert sechs Byte an.
realloc(NULL, positive_size) verhält sich wie malloc, sodass ein leerer Puffer mit einem Nullzeiger beginnen kann. Um einen Puffer freizugeben, rufe ausdrücklich free auf. Vermeide Größenänderungen auf null: Das frühere Verhalten der Bibliotheken unterscheidet sich, und die Größenänderung eines bestehenden Blocks auf null ist in C23 undefiniertes Verhalten. Weder lokale Arrays noch zuvor freigegebene Blöcke dürfen in ihrer Größe geändert werden.
Wichtige Erkenntnisse
- Verwende einen temporären Zeiger, damit bei einer fehlgeschlagenen Größenänderung auf eine positive Größe die Adresse des Besitzers erhalten bleibt.
- Beim Vergrößern bleibt der alte Inhalt erhalten, hinzugefügte Bytes sind aber nicht initialisiert.
- Beim Verkleinern bleibt nur der Teil erhalten, der noch hineinpasst.
- Verwende nach einem Erfolg den zurückgegebenen Zeiger und berechne gespeicherte Elementzeiger neu.
- Gib Speicher mit
freefrei und behandle die Größe null ausdrücklich.
3.2 Ein Array, das mit seiner Eingabe wächst
Ein vergrößerbares Array benötigt drei Zustandsangaben: seinen Zeiger, seine Länge und seine Kapazität. Die Länge zählt die initialisierten, belegten Elemente. Die Kapazität zählt die allokierten Elementplätze. Halte die Länge kleiner oder gleich der Kapazität.
In einer sicheren Reihenfolge anhängen
Beginne mit int *data = NULL; size_t len = 0, cap = 0;. Prüfe vor dem Anhängen, ob len == cap gilt. Ein volles Array muss wachsen, bevor nach data[len] geschrieben wird. Erst nach einer erfolgreichen Größenänderung dürfen sich Zeiger und Kapazität ändern. Speichere den neuen Wert und erhöhe dann die Länge.
Das folgende vollständige Beispiel hängt zehn Werte mit geometrischem Wachstum an:
#include <stdint.h>
#include <stdlib.h>
int main(void) {
int *data = NULL;
size_t len = 0, cap = 0;
for (int value = 0; value < 10; ++value) {
if (len == cap) {
size_t next;
if (cap == 0) {
next = 4;
} else {
if (cap > SIZE_MAX / 2 / sizeof *data) {
free(data);
return EXIT_FAILURE;
}
next = cap * 2;
}
int *tmp = realloc(data, next * sizeof *data);
if (tmp == NULL) {
free(data);
return EXIT_FAILURE;
}
data = tmp;
cap = next;
}
data[len++] = value;
}
free(data);
return EXIT_SUCCESS;
}
Die Überlaufprüfung prüft sowohl die Verdopplung als auch die Umrechnung in Bytes, bevor eines der Produkte durch Wraparound einen kleineren Wert ergeben kann. Auch die Anfangskapazität von vier passt bei diesem Elementtyp. Bei einem Fehlschlag wird der noch im Besitz des Programms befindliche alte Block freigegeben und das Programm beendet; die Kapazität wird nicht fälschlich aktualisiert.
Länge und Kapazität getrennt verfolgen
Ausgehend von Kapazität 4 nutzen die ersten vier Anhängevorgänge den vorhandenen Platz. Vor dem fünften verdoppelt sich die Kapazität auf 8. Vor dem neunten verdoppelt sie sich auf 16. Nach zehn Anhängevorgängen betragen Länge und Kapazität 10 und 16. Werden bei beiden Vergrößerungen Daten verschoben, werden höchstens vier und acht Elemente kopiert, insgesamt zwölf.
Die freie Kapazität ist beabsichtigt. Eine Vergrößerung um ein Element bei jedem Anhängen verursacht wiederholtes Kopieren. Ausgehend von Kapazität 1 können bis zum Erreichen von 1.000 Elementen 999 Größenänderungen und 499.500 kopierte Elemente nötig sein. Durch Verdoppeln wird nach zehn Größenänderungen und höchstens 1.023 Kopien die Kapazität 1.024 erreicht. Eine einzelne Größenänderung kann aufwendig sein, doch bei geometrischem Wachstum bleibt der durchschnittliche Aufwand pro Anhängen konstant.
Ein Faktor wie 1,5 lässt weniger Platz ungenutzt, führt aber zu häufigerem Wachstum. Ist die Eingabe beendet, kann das Programm auf die belegte Länge verkleinern und einen Fehlschlag mit demselben Muster eines temporären Zeigers behandeln. Ein leeres Array wird mit free freigegeben, nicht durch eine Größenänderung auf null.
Zeichenpuffer benötigen einen zusätzlichen Platz
Reserviere bei einer wachsenden Zeichenkette Platz für '\0'. Vergrößere vor dem Hinzufügen eines Zeichens den Puffer, falls len + 1 >= cap gilt. Bei Kapazität 8 passen sieben Zeichen hinein, während der Platz für das abschließende Nullzeichen frei bleibt; „hello world“ löst eine Vergrößerung auf 16 aus und benötigt einschließlich des Nullzeichens zwölf Byte.
Ein gespeicherter Elementzeiger wird nach erfolgreichem Wachstum ungültig. Behalte einen Index und greife über die neue Anfangsadresse erneut auf das Element zu. Wenn du ein Array für die weitere Verwendung zurücksetzt, gib seinen Block frei und setze data = NULL; len = cap = 0;.
Wichtige Erkenntnisse
- Die Länge zählt belegte Elemente; die Kapazität zählt verfügbare Plätze.
- Vergrößere das Array vor dem Anhängen, wenn Länge und Kapazität gleich sind.
- Aktualisiere Zeiger und Kapazität erst nach erfolgreicher Größenänderung.
- Geometrisches Wachstum begrenzt wiederholtes Kopieren, benötigt dafür aber freien Speicherplatz.
- Prüfe die Berechnung für das Wachstum vor der Multiplikation.
- Reserviere ein zusätzliches Byte für das abschließende Nullzeichen einer Zeichenkette.
- Behalte Indizes über das Wachstum hinweg und berechne Elementzeiger danach neu.
4Speicherfehler erkennen und verhindern
Speicherfehler entstehen durch verletzte Regeln zu Besitz, Lebensdauer, Speichergrenzen oder Initialisierung. Jeder hinterlässt andere Spuren: eine verlorene Adresse, einen Zugriff nach der Freigabe, einen Index außerhalb des Blocks oder einen zu früh gelesenen Wert. Manche Fehler führen sofort zum Absturz, andere beschädigen Daten und treten erst später zutage. Wenn du den Code im Hinblick auf Blöcke und ihre Geschichte liest, kannst du die Ursache hinter dem Symptom erkennen.
4.1 Den Pfad finden, auf dem Speicher verloren geht
Ein Speicherleck ist allokierter Speicher, den ein Programm nicht mehr benötigt, aber nie freigibt. Oft geht der letzte Zeiger verloren, sodass ein späteres free unmöglich wird. Ein Speicherleck kann unbemerkt bleiben, während der Speicherverbrauch wächst.
Adressen verfolgen, nicht Variablennamen
char *p = malloc(100);
if (p == NULL) return EXIT_FAILURE;
p = malloc(200); /* loses the only address of the first block */
if (p == NULL) return EXIT_FAILURE;
free(p);
Das abschließende free gibt nur den zweiten Block frei. Der Name p wurde wiederverwendet, der erste allokierte Block jedoch nicht. Schlägt die zweite Anforderung fehl, ist der 100 Byte große Block trotzdem verloren. Gib entweder den ersten Block frei, bevor du ihn ersetzt, sobald du ihn nicht mehr benötigst, oder bewahre seine Adresse in einer anderen besitzenden Variablen auf.
Den besitzenden Zeiger vor der Freigabe auf NULL zu setzen, führt zum selben Verlust. Die Zuweisung ändert eine Zeigervariable; sie weist den Allokator nicht an, etwas freizugeben. Automatische lokale Variablen verschwinden am Ende eines Aufrufs, die allokierten Blöcke, auf die sie zeigten, bleiben jedoch bestehen.
Jeden Weg aus einer Funktion prüfen
Eine vorzeitige Rückkehr kann das Aufräumen überspringen:
char *line = malloc(100);
if (line == NULL) return EXIT_FAILURE;
if (input_is_bad) {
free(line);
return EXIT_FAILURE;
}
/* Use line on the successful path. */
free(line);
Beide Pfade geben den Block frei. In einer Funktion mit mehr Ressourcen verringert ein gemeinsamer Aufräumabschnitt die Anzahl der zu prüfenden Pfade. Eine Funktion, die ein allokiertes Ergebnis zurückgibt, überträgt die Verantwortung für dessen Besitz; eine Funktion, die einen lokalen besitzenden Zeiger einfach aufgibt, verursacht ein Speicherleck.
Die direkte Zuweisung eines fehlgeschlagenen realloc-Ergebnisses ist ein weiterer Verlustpfad. Bei verschachtelten Speicherallokationen entstehen Speicherlecks, wenn das äußere Zeigerarray freigegeben wird, bevor die inneren Adressen zum Aufräumen verwendet wurden.
Durch Wiederholung werden kleine Speicherlecks teuer
Angenommen, eine Schleife allokiert pro Durchlauf 100 Byte und gibt sie nie frei. Nach 1.000 Durchläufen hat sie 100.000 Byte zurückgelassen. Gib jeden Puffer am Ende seines Durchlaufs frei oder allokiere einmal vor der Schleife, verwende den Block erneut und gib ihn danach frei. Die Wiederverwendung vermeidet auch wiederholte Arbeit des Allokators, wenn dieselbe Kapazität ausreicht.
Ein Dienst, der pro Anfrage 64 Byte verliert, hat nach 10.000 Anfragen 640.000 Byte verloren, genau 625 KiB. Nach einer Million Anfragen sind es 64.000.000 Byte, etwa 61 MiB. Lange laufende Programme können schließlich keinen Speicher mehr allokieren oder das System verlangsamen. Das Betriebssystem holt sich Prozessspeicher beim Beenden gewöhnlich zurück, doch während der Dienst läuft, hilft ihm das nicht.
Verloren und noch erreichbar sind unterschiedliche Meldungen
Enthält ein globaler Zeiger beim Beenden noch die Anfangsadresse einer Speicherallokation, bezeichnet Valgrind den Block als „still reachable“. Die Adresse ist nicht verloren gegangen; das Programm hat den Block lediglich nicht freigegeben. „Definitely lost“ bedeutet, dass kein nutzbarer Zeiger mehr vorhanden ist. Räume gezielt auf, damit diese Berichte unerklärte Speicherallokationen deutlich zeigen.
Wichtige Erkenntnisse
- Das Überschreiben oder Zurücksetzen des letzten besitzenden Zeigers kann zu einem Speicherleck führen.
- Normale Pfade, vorzeitige Rückkehr und Fehlerpfade müssen alle aufräumen.
- Wiederholte kleine Speicherlecks summieren sich während der Programmausführung.
- Die Wiederverwendung eines geeigneten Puffers kann wiederholte Allokationsarbeit und Speicherlecks pro Iteration vermeiden.
4.2 Wenn ein Zeiger das Objekt überdauert, auf das er zeigt
Ein Zeiger wird hängend, wenn die Lebensdauer des Objekts endet, auf das er zeigt. Die Variable kann weiterhin existieren, aber über diesen Zeiger auf das alte Objekt zuzugreifen ist ungültig. Sowohl die Rückgabe einer lokalen Adresse als auch die Freigabe eines allokierten Blocks kann dieses Problem erzeugen.
Die letzte Verwendung vor der Freigabe finden
int *scores = malloc(3 * sizeof *scores);
if (scores == NULL) return EXIT_FAILURE;
scores[0] = 90;
scores[1] = 75;
scores[2] = 82;
free(scores);
printf("%d\n", scores[0]); /* use-after-free */
Die Zuweisung an scores[0] erfolgte während der Lebensdauer des Blocks. Der abschließende Lesezugriff erfolgte danach. Verschiebe free(scores) hinter die Ausgabe, sobald alle Verwendungen abgeschlossen sind. Lediglich scores = NULL vor der Ausgabe zuzuweisen, macht aus dem ungültigen Zugriff eine Nullzeigerdereferenzierung; die fehlenden Daten werden dadurch nicht bereitgestellt.
Das ist undefiniertes Verhalten: C stellt keine Anforderungen an das Ergebnis. Das Programm kann einen alten Wert anzeigen, abstürzen oder unbemerkt Daten beschädigen. Freigegebene Bytes bleiben oft an Ort und Stelle, bis der Allokator sie wiederverwendet. Ein plausibles Ergebnis kann einen Lesezugriff nach der Freigabe daher nicht als korrekt bestätigen.
Aliase teilen das Lebensdauerproblem
int *a = malloc(3 * sizeof *a);
if (a == NULL) return EXIT_FAILURE;
int *b = a;
free(a);
a = NULL;
b[0] = 1; /* b also lost its live target */
Es gab einen Block, dessen Adresse in zwei Zeigervariablen stand. Die Freigabe über a beendet seine Lebensdauer für beide. Das Setzen von a auf null ändert nur a; b wird dadurch nicht aktualisiert. Code, der geliehene Aliase verwendet, muss seine Arbeit beenden, bevor der Besitzer den Block freigibt. Danach dürfen diese Aliase nicht mehr verwendet werden.
Eine doppelte Speicherfreigabe nutzt ebenfalls eine beendete Lebensdauer
free zweimal für denselben Block aufzurufen ist eine doppelte Speicherfreigabe, auch wenn der zweite Aufruf über einen Alias erfolgt. Sie kann die Verwaltungsdaten des Allokators beschädigen oder eine spätere Speicherallokation fehlschlagen lassen. Manche Bibliotheken erkennen sie sofort und beenden das Programm, aber eine sofortige Erkennung ist nicht garantiert.
Die besitzende Variable nach der Freigabe auf NULL zu setzen, hilft bei einem wiederholten Aufräumaufruf, weil free(NULL) gültig ist. Es macht free(b) nicht sicher, wenn b noch auf den früheren Block verweist. Genau ein Besitzer sollte entscheiden, wann der Block freigegeben wird.
Auch eine Größenänderung beendet die alte Lebensdauer
Ein erfolgreiches realloc ersetzt das alte Objekt, selbst wenn der zurückgegebene numerische Adresswert unverändert ist. Gespeicherte Zeiger in den alten Block müssen anhand des zurückgegebenen Zeigers neu berechnet werden. Speichere Positionen als Indizes, solange Wachstum möglich ist. Ein fehlgeschlagenes realloc mit positiver Größe lässt den alten Block dagegen bestehen.
Eine Funktion, die die Adresse einer automatischen lokalen Variablen zurückgibt, erzeugt einen ähnlichen Lebensdauerfehler ohne jeden free-Aufruf: Die Lebensdauer des Objekts endet bei der Rückkehr der Funktion. Verwende Speicher im Besitz des Aufrufers oder ein Heap-Ergebnis mit klar geregeltem Besitz. Zugriffe nach der Speicherfreigabe und doppelte Speicherfreigaben können auch zu Sicherheitslücken werden, wenn ein Angreifer den wiederverwendeten Speicher beeinflussen kann.
Wichtige Erkenntnisse
- Ein hängender Zeiger zeigt auf ein Objekt, dessen Lebensdauer beendet ist.
- Schließe jeden Lese- und Schreibzugriff ab, bevor du den Block freigibst.
- Eine Freigabe macht alle Aliase auf das alte Objekt ungültig.
- Den besitzenden Zeiger auf null zu setzen hilft beim wiederholten Aufräumen, korrigiert aber keine geliehenen Aliase.
- Eine doppelte Speicherfreigabe kann die Verwaltungsdaten des Allokators beschädigen.
- Ein erfolgreiches
reallocmacht gespeicherte Zeiger in den alten Block ungültig.
4.3 Gültige Grenzen und initialisierte Werte
Ein gültiger Elementzugriff benötigt sowohl genügend Speicher als auch einen Wert, der vor dem Lesen gesetzt wurde. Ein Block kann die richtige Größe haben und dennoch nicht initialisierte Daten enthalten oder brauchbare Werte enthalten und dennoch außerhalb seiner Grenzen angesprochen werden.
Den Indexbereich prüfen
Für n Elemente reichen die gültigen Indizes von null bis n - 1. Eine Schleife mit i <= n führt einen zusätzlichen Zugriff aus. Bei zehn ints zu 4 Byte umfasst die Speicherallokation 40 Byte; p[10] beginnt unmittelbar hinter diesen Bytes. Verwende i < n, um die zehn Elemente zu durchlaufen.
Auch ein Index aus einer Eingabe benötigt vor der Verwendung eine Wertebereichsprüfung. Eine vorzeichenbehaftete Eingabe muss nicht negativ und kleiner als die Elementanzahl sein. Speichere die Anzahl zum Zeiger und übergib beides an Funktionen. Ein Zeiger allein kann den erlaubten Bereich nicht beschreiben.
Zeichenketten mit Platz für das abschließende Nullzeichen bemessen
char *copy = malloc(strlen(s));
strcpy(copy, s); /* writes the terminator beyond the allocation */
Dieses fehlerhafte Fragment benötigt außerdem eine Prüfung auf Fehlschlag. Die Größenkorrektur lautet malloc(strlen(s) + 1), gefolgt von der Prüfung und dann dem Kopieren. „Amsterdam“ hat neun Zeichen, benötigt mit '\0' aber zehn Byte. Das abschließende Nullzeichen ist ein tatsächlich gespeichertes Byte, auch wenn es nicht als Teil des Textes ausgegeben wird.
Ein Zeigerarray ist kein Zeichenkettenspeicher
char **names = malloc(n * sizeof *names);
Dies reserviert n Zeigerplätze. Es erzeugt keine Zeichenpuffer, auf die diese Zeiger zeigen. Bevor du names[i] als Zeichenkette verwendest, weise dem Element eine gültige Zeichenkettenadresse zu. Es kann ein vorhandenes Array oder ein Literal mit ausreichender Lebensdauer ausleihen oder eine separat allokierte Zeichenkettenkopie besitzen. Beim verschachtelten Aufräumen werden nur die Zeichenketten im Besitz des Arrays freigegeben.
Dieselbe Überlegung zur Größe gilt für numerische Arrays: malloc(n) zählt Bytes und sizeof p misst einen Zeiger. Verwende n * sizeof *p mit Überlaufprüfung und Prüfung auf einen Nullzeiger, um eine Elementanzahl korrekt auszudrücken.
Vor dem Lesen schreiben
int *t = malloc(3 * sizeof *t);
if (t == NULL) return EXIT_FAILURE;
t[0] = 5;
t[1] = 7;
printf("%d\n", t[0] + t[1] + t[2]); /* t[2] was never set */
free(t);
t[2] liegt innerhalb des allokierten Blocks, sein Wert ist aber nicht initialisiert. Weise ihm vor der Summe einen Wert zu oder verwende calloc, wenn null der beabsichtigte Anfangswert ist. Die beim Vergrößern mit realloc hinzugefügten Bytes benötigen dieselbe Aufmerksamkeit. Innerhalb der Grenzen zu liegen initialisiert den Speicher nicht.
Warum der Absturz später auftreten kann
Ein Schreibzugriff außerhalb der Speichergrenzen kann benachbarte Daten oder die Verwaltungsdaten des Allokators beschädigen. Ein Lesezugriff außerhalb der Speichergrenzen kann fremde Daten offenlegen. Beides ist undefiniertes Verhalten. Ein kleiner Überlauf kann zugeordneten Speicher berühren und unbemerkt bleiben; ein späteres free kann der erste sichtbare Fehlschlag sein.
Prüfe die Allokationsgröße, den Indexbereich und die Initialisierungsgeschichte unabhängig voneinander. Eine größere Speicherallokation kann einem nicht initialisierten Element keinen aussagekräftigen Wert geben, und das Nullsetzen kann einen Index außerhalb des Bereichs nicht gültig machen.
Wichtige Erkenntnisse
- Gültige Indizes für
nElemente reichen von null bisn - 1. - Der Speicher für eine Zeichenkette umfasst ein Byte für das abschließende Nullzeichen.
- Ein Array von Zeichenkettenzeigern allokiert die Zeichenketten selbst nicht.
- Initialisiere alle Werte, die eine Berechnung liest, einschließlich neu hinzugefügter Elemente.
- Ein ungültiger Schreibzugriff kann Daten lange vor einem sichtbaren Absturz beschädigen.
4.4 Einen Absturz bis zu seiner Ursache zurückverfolgen
Eine Speicherzugriffsverletzung ist eine Reaktion des Betriebssystems auf einen unerlaubten Speicherzugriff. Häufige Ursachen sind die Dereferenzierung eines Nullzeigers oder nicht initialisierten Zeigers, das Schreiben in ein Zeichenkettenliteral, Zugriffe weit außerhalb eines Arrays, die Verwendung freigegebenen Speichers und die Erschöpfung des Stacks.
Was die Meldung aussagt
Unter Linux beendet SIGSEGV gewöhnlich den Prozess, und eine Shell kann „Segmentation fault (core dumped)“ ausgeben. Ein häufiger Rückgabestatus der Shell ist 139, nach der Signalkonvention 128 plus Signal 11. macOS kann für ungültige Zugriffe SIGSEGV oder SIGBUS melden. Windows meldet den vergleichbaren Fehler als Zugriffsverletzung, häufig mit dem Ausnahmecode 0xC0000005.
Diese Meldungen zeigen einen fehlgeschlagenen Zugriff, aber nicht seine vollständige Vorgeschichte. Ein kleiner Überlauf kann früh einen Zeiger beschädigen und viel später einen Absturz verursachen. Viele ungültige Zugriffe bleiben innerhalb des Speichers, den der Prozess adressieren kann, und beschädigen daher unbemerkt Daten, statt einen Fehler auszulösen.
Mit Zeileninformationen beginnen
Erstelle eine ausführbare Debug-Datei mit Warnungen:
cc -std=c17 -g -O0 -Wall -Wextra program.c -o program
Führe sie dann unter einem Debugger oder Speicherprüfwerkzeug aus. Eine Aufrufrückverfolgung listet die beim Absturz aktiven Funktionsaufrufe auf. Finde den obersten Stackframe, der zu deinem eigenen Code gehört. Bibliotheksfunktionen wie free können die Stelle sein, an der ein fehlerhafter Zustand sichtbar wird, während ein früherer Aufruf in deinem Programm ihn erzeugt hat.
Ermittle in der gemeldeten Zeile jeden dereferenzierten Zeiger. Verfolge für jeden seinen Wert bis zur Quelle zurück. Wurde die Speicherallokation geprüft? Existiert das Objekt, auf das er zeigt, noch? Passt der Index in den Block? Wurde dem Zeiger überhaupt eine gültige Adresse zugewiesen?
Ein Nullergebnis untersuchen
int *p = NULL;
printf("%d\n", *p); /* invalid dereference */
Die Ausgabe benötigt einen Lesezugriff über p, aber an dieser Adresse existiert kein Ganzzahlobjekt. Stammt p aus einer Speicherallokationsfunktion, prüfe das Ergebnis vor dieser Zeile und wähle bei einem Fehlschlag einen Fehlerpfad. Wurde der Zeiger nach der Freigabe zurückgesetzt, verschiebe die benötigte Verwendung vor die Freigabe oder ändere, wo die Daten aufbewahrt werden.
Einem nicht initialisierten Zeiger wie in int *p; *p = 5; wurde kein gültiges Objekt zugewiesen, auf das er zeigt. Vor der Dereferenzierung muss eine Speicherallokation, die Adresse eines noch bestehenden Objekts oder ein geprüfter geliehener Zeiger dieses Ziel festlegen.
Der Ursache folgen, wenn sich Symptome ändern
Eine endlos rekursive Funktion kann in einer nicht optimierten Version den Stack durch ihre Stackframes erschöpfen. Eine Abbruchbedingung behebt den logischen Fehler. Ein optimierender Compiler kann stattdessen manche rekursiven Aufrufe in eine Schleife umwandeln. Dass kein Stapelüberlauf auftritt, macht die unbegrenzte Berechnung daher nicht nützlich.
Ausgegebene Fortschrittsmeldungen sind ebenfalls kein vollkommener Beleg. stdout wird gepuffert: Bei einem Prozessabsturz kann Ausgabe ungeschrieben bleiben, besonders bei umgeleiteter Ausgabe. Verwende für eine Diagnosemeldung stderr oder rufe nach einer Fortschrittsausgabe fflush(stdout) auf. Das Hinzufügen einer Ausgabe kann die Speicheranordnung verändern und einen Fehler verbergen; den ungültigen Zugriff hat es nicht behoben. Führe das Programm erneut mit einem Prüfwerkzeug aus, um die fehlerhafte Operation selbst zu finden.
Wichtige Erkenntnisse
- Eine Speicherzugriffsverletzung meldet einen unerlaubten Zugriff; die ursprüngliche Beschädigung kann früher entstanden sein.
- Erstelle das Programm mit Debuginformationen und finde den ersten relevanten Stackframe in deinem eigenen Code.
- Verfolge bei jedem Zeiger seinen Ursprung, die Lebensdauer des Zielobjekts und den Indexbereich.
- Gepufferte Ausgabe kann eine Ausgabe verbergen, die vor einem Absturz ausgeführt wurde.
- Ändert sich das Symptom nach dem Hinzufügen einer Ausgabe, muss es weiterhin untersucht werden.
5Speicherfehler mit Valgrind und Sanitizern finden
Speicherprüfwerkzeuge machen verborgene Fehler sichtbar, indem sie den Zugriff und die Geschichte des Blocks melden. Valgrind beobachtet eine ausführbare Debug-Datei; AddressSanitizer fügt beim Erstellen Prüfungen hinzu. Lerne, ihre Meldungen mit Elementgrenzen, Allokationsstellen und Freigabestellen zu verbinden und dann eine Korrektur vorzunehmen, die den Besitz korrekt regelt. Auf einen fehlerfreien erneuten Lauf folgen neue Eingaben, die andere Pfade durchlaufen.
5.1 Einen Valgrind-Bericht lesen
Valgrinds Standardwerkzeug Memcheck beobachtet ein kompiliertes Programm während seiner Ausführung. Es verfolgt Heap-Grenzen, die Lebensdauer von Speicherallokationen und ob Werte initialisiert wurden. Es benötigt keine mit einem Sanitizer erstellte Programmversion, aber Debuginformationen machen seine Berichte wesentlich leichter nutzbar.
Erstellen und ausführen
cc -std=c17 -g -O0 -Wall -Wextra program.c -o program
valgrind --leak-check=full ./program
Setze die Argumente des Programms hinter ./program. Ergänze --show-leak-kinds=all, um alle Speicherleckkategorien aufzulisten, und --track-origins=yes, wenn der Ursprung eines nicht initialisierten Werts unklar ist.
Valgrind läuft unter Linux und einigen anderen Unix-artigen Systemen, nicht nativ unter Windows oder auf Macs mit Apple-Chips. Memcheck verlangsamt die Ausführung häufig ungefähr um den Faktor 20 bis 30. Verwende zuerst eine kleine fehlerauslösende Eingabe und führe dann weitere Tests aus. Ein fehlerfreier Test deckt nur die Pfade ab, die er tatsächlich ausführt.
Zugriffsgröße und Blockgröße getrennt lesen
Ein Bericht mit der Überschrift „Invalid write of size 4“ beschreibt einen Schreibzugriff über vier Byte. Eine Adresse „0 bytes after a block of size 40 alloc'd“ beginnt unmittelbar hinter einer Speicherallokation von 40 Byte. Enthält dieser Block zehn ints zu je 4 Byte, beginnt der fehlerhafte Zugriff bei Element 10; das letzte gültige Element ist 9.
Der Bericht nennt auch die Zugriffszeile und den Aufrufstapel der Speicherallokation. Nutze beides zusammen: Die Speicherallokation zeigt die vorgesehenen Grenzen, der Zugriff zeigt, welcher Index sie verletzt hat. „Size 4“ bedeutet nicht, dass das Programm um vier Elemente über die Grenze hinausgegangen ist.
„Invalid read of size N“ beschreibt entsprechend einen ungültigen Lesezugriff. Eine Adresse innerhalb eines als „free'd“ beschriebenen Blocks weist auf ein Lebensdauerproblem hin; vergleiche die Stellen von Allokation, Freigabe und Zugriff. „Invalid free() / delete / delete[] / realloc()“ kann auf eine doppelte Freigabe oder eine Adresse hinweisen, die nie der gültige Anfang einer Speicherallokation war.
Für eine Bedingung, die nicht gesetzte Daten verwendet, kann Memcheck „Conditional jump or move depends on uninitialised value(s)“ melden. Diese Schreibweise gehört zur Meldung des Werkzeugs. Die gemeldete Bedingung hat den Wert verwendet; --track-origins=yes hilft dabei, den Ursprung seines nicht initialisierten Speichers zu finden.
Die Speicherleckkategorie lesen
| Berichtskategorie | Bedeutung |
|---|---|
| Definitely lost | Kein Zeiger auf den Block ist mehr vorhanden |
| Indirectly lost | Der Block ist nur über einen anderen verlorenen Block erreichbar |
| Possibly lost | Es wurde nur ein Zeiger ins Objektinnere gefunden |
| Still reachable | Beim Beenden ist noch ein Zeiger vorhanden, der Block wurde aber nicht freigegeben |
Angenommen, eine Hilfsfunktion allokiert zehn ints zu je 4 Byte und speichert deren Adresse in einem lokalen Zeiger. Dann kehrt sie zurück, ohne den Speicher freizugeben oder die Adresse zurückzugeben. Ihr 40 Byte großer Block ist definitiv verloren. Ein globaler Zeiger, der die Anfangsadresse behält, würde einen nicht freigegebenen Block dagegen noch erreichbar machen. Die Gesamtzahlen der Allokationen und Freigaben in der Heap-Übersicht beschreiben Ereignisse, einschließlich Größenänderungen; sie ersetzen nicht die Überlegungen zum Besitz.
Ein fehlerfreies Ende kann „All heap blocks were freed -- no leaks are possible“ und „ERROR SUMMARY: 0 errors from 0 contexts“ melden. Memcheck erkennt im Allgemeinen nicht jeden Überlauf eines lokalen oder globalen Arrays, der innerhalb des ansonsten gültigen umgebenden Speichers bleibt. Behebe den ersten gemeldeten Fehler, führe das Programm erneut aus und teste weitere Eingaben, darunter eine leere Eingabe und Pfade mit vorzeitiger Rückkehr.
Wichtige Erkenntnisse
- Verwende Debuginformationen, damit Memcheck deine Quellcodezeilen bestimmen kann.
- Betrachte Zugriffsbreite, Blockgrenzen und Allokationsgeschichte zusammen.
- Verwende
--track-origins=yes, um nicht initialisierte Werte zurückzuverfolgen. - Unterscheide verlorene Blöcke von Blöcken, die beim Beenden noch erreichbar sind.
- Behebe den ersten Fehler, bevor du spätere Folgen deutest.
- Führe mehrere Eingaben aus, weil ein Prüfwerkzeug nur ausgeführte Pfade beobachtet.
5.2 Prüfungen in das Programm einbauen
AddressSanitizer, kurz ASan, fügt beim Kompilieren Prüfungen von Speicherzugriffen hinzu. Die entstehende ausführbare Datei prüft sich während der Ausführung selbst. Das ist für regelmäßige Entwicklungstests nützlich, weil die typische Verlangsamung etwa bei Faktor zwei liegt und damit deutlich geringer als bei Memcheck ist.
Mit dem Sanitizer kompilieren und linken
cc -std=c17 -g -fsanitize=address -fno-omit-frame-pointer \
program.c -o program-asan
./program-asan
Verwende bei getrennten Erstellungsschritten die Sanitizer-Option sowohl beim Kompilieren als auch beim abschließenden Linken. -g liefert Zeileninformationen, und -fno-omit-frame-pointer hilft, brauchbare Aufrufstapel zu erhalten. Microsofts Compiler verwendet /fsanitize=address.
ASan erkennt Überläufe auf dem Heap, auf dem Stack und in globalen Arrays, Zugriffe nach der Speicherfreigabe sowie ungültige und doppelte Freigaben. Prüfungen der Lebensdauer innerhalb des Gültigkeitsbereichs und optionale Prüfungen auf Zugriffe nach der Rückkehr können auch Zeiger auf ehemalige lokale Variablen erkennen. ASan benötigt eine Neuerstellung und zusätzlichen Speicher. Belasse diese Instrumentierung in Entwicklungs- und Testversionen.
Die drei Abläufe lesen
Ein Bericht über einen Zugriff nach der Speicherfreigabe kann mit „ERROR: AddressSanitizer: heap-use-after-free“ beginnen. Ein nachfolgendes „READ of size 4“ beschreibt die ungültige Operation. Der Bericht zeigt dann, wo der Speicher „freed by thread T0 here:“ und „previously allocated by thread T0 here:“ wurde.
Beginne in jedem Aufrufstapel mit der ersten relevanten Zeile in deinem Code. Die Speicherallokation erzeugt den Block; die Freigabe beendet seine Lebensdauer; der gemeldete Lesezugriff erfolgt danach. Entscheide, ob du die Freigabe hinter den Lesezugriff verschiebst oder nach der Freigabe nicht mehr auf den Block zugreifst. Der Bericht liefert die Vorgeschichte, während der vorgesehene Besitz die Korrektur bestimmt.
Bei „heap-buffer-overflow“ beginnt eine Adresse „located 0 bytes after 20-byte region“ unmittelbar hinter dem Bereich. Rechne Bytes erst in Elemente um, nachdem du die vom Programm verwendete Typgröße geprüft hast. ASan hält gewöhnlich beim ersten Fehler an, sodass dessen Behebung beim nächsten Lauf einen weiteren Fehler sichtbar machen kann.
Speicherleckprüfungen und nicht initialisierte Werte sind verschieden
LeakSanitizer prüft beim Beenden auf nicht freigegebene Speicherallokationen. Unter Linux ist es mit ASan normalerweise aktiviert. Bei unterstützten macOS-Werkzeugketten lässt es sich mit ASAN_OPTIONS=detect_leaks=1 aktivieren; manche Mac-Werkzeugketten stellen es nicht bereit. Apples separates Werkzeug leaks ist dort eine weitere Möglichkeit zur Speicherleckprüfung.
ASan verfolgt nicht, ob ein Wert innerhalb der Speichergrenzen initialisiert wurde. Verwende für dieses Problem Memcheck oder Clangs MemorySanitizer auf einem unterstützten Linux-System. UndefinedBehaviorSanitizer prüft andere Arten undefinierten Verhaltens, etwa Überläufe bei vorzeichenbehafteten Ganzzahlen, und lässt sich mit -fsanitize=address,undefined mit ASan kombinieren.
Für jedes Prüfwerkzeug einen eigenen Lauf wählen
| Bedarf | Nützliches Werkzeug |
|---|---|
| Schnelle Prüfungen während der Entwicklung, einschließlich der Grenzen lokaler Arrays | ASan-Version |
| Verfolgung nicht initialisierter Werte ohne Sanitizer-Instrumentierung | Memcheck auf einem unterstützten System |
| Speicherlecks beim Beenden | LeakSanitizer oder Memcheck |
Führe keine ausführbare ASan-Datei unter Valgrind aus. Erstelle für Memcheck eine separate gewöhnliche ausführbare Debug-Datei und kombiniere ASan und MemorySanitizer nicht in derselben Erstellung. Variiere nach einem fehlerfreien Lauf die Eingabe, damit andere Pfade für Allokation, Wachstum und Aufräumen ausgeführt werden.
Wichtige Erkenntnisse
- ASan benötigt beim Kompilieren und Linken seine Instrumentierungsoption.
- Lies die Aufrufstapel des fehlerhaften Zugriffs, der Freigabe und der Speicherallokation, um einen Lebensdauerfehler zu rekonstruieren.
- LeakSanitizer prüft beim Beenden auf Speicherlecks, wo die Plattform dies unterstützt.
- ASan erkennt gewöhnliche Lesezugriffe auf nicht initialisierte Werte nicht.
- Verwende für ASan und Memcheck getrennte Ausführungen und geeignete Programmversionen.
5.3 Einen Bericht in eine geprüfte Korrektur umsetzen
Ein Speicherprüfbericht benennt einen beobachteten Fehler und Teile seiner Vorgeschichte. Eine nützliche Korrektur stellt Größe, Grenzen, Initialisierung und Besitz richtig und prüft dann das korrigierte Programm erneut.
Eingabe verkleinern und Fehler reproduzieren
Finde die kleinste Eingabe, die den Fehler noch erzeugt. Erstelle das Programm mit -g, Warnungen und geringer Optimierung und verwende dann entweder Memcheck oder eine ASan-Version. Lies den ersten Bericht, bevor du spätere untersuchst; ein beschädigter Zustand kann eine Kette irreführender Fehler erzeugen.
Angenommen, ein Programm enthält:
size_t n = 5;
int *v = malloc(n * sizeof *v);
for (size_t i = 0; i <= n; ++i) v[i] = (int)(i * i);
printf("%d\n", v[2]);
return 0;
Auf einem System mit ints zu 4 Byte meldet ASan einen Schreibzugriff über vier Byte am ersten Byte hinter einer Speicherallokation von 20 Byte. Der Schreibzugriff liegt in der Schleife. Memcheck kann ebenfalls den Überlauf und später eine nicht freigegebene Speicherallokation melden; ob der Block verloren oder noch erreichbar ist, hängt davon ab, welche Zeiger beim Beenden übrig sind.
Den Bericht dem Code zuordnen
Fünf ints belegen die Indizes null bis vier. Die Bedingung i <= n erreicht Index fünf. Die Allokationsgröße ist korrekt, die Schleifengrenze nicht. Ein größerer Block würde das Symptom verbergen, während der Durchlauf fehlerhaft bliebe.
Außerdem fehlt eine Prüfung der Speicherallokation, und der Block im Besitz des Programms wird nicht freigegeben. Diese Fehler benötigen eigene Korrekturen, selbst wenn der erste Bericht das Programm angehalten hat, bevor es sie erreichte.
#include <stdio.h>
#include <stdlib.h>
int main(void) {
size_t n = 5;
int *v = malloc(n * sizeof *v);
if (v == NULL) return EXIT_FAILURE;
for (size_t i = 0; i < n; ++i) v[i] = (int)(i * i);
printf("%d\n", v[2]);
free(v);
return EXIT_SUCCESS;
}
Das korrigierte Programm initialisiert fünf gültige Elemente, gibt 4 aus, während der Block existiert, und gibt ihn frei. Die feste kleine Anzahl passt; bei einer aus der Eingabe erhaltenen Anzahl ist vor der Speicherallokation außerdem die Prüfung auf einen Überlauf bei der Größenberechnung nötig.
Die Vorgeschichte im Bericht für die passende Korrektur nutzen
Vergleiche bei einem Zugriff nach der Speicherfreigabe die Aufrufstapel der Freigabe und des Zugriffs, um die letzte zulässige Verwendung zu finden. Verschiebe die Freigabe dahinter oder entferne den späteren ungültigen Zugriff. Einen Zeiger auf NULL zu setzen, kann seine früheren Daten nicht wieder verfügbar machen.
Bei einem Speicherleck zeigt der Allokationsstapel, wo der Block entstand. Er zeigt nicht die richtige Freigabezeile. Verfolge den Besitz durch erfolgreiche Rückgaben und Fehlschläge, um das Ende der letzten Verwendung zu finden. Entferne bei einer doppelten Speicherfreigabe die doppelte Freigabe oder korrigiere widersprüchliche Besitzverhältnisse. Das Entfernen der einzigen gültigen Freigabe erzeugt ein Speicherleck.
Ergänze bei nicht initialisierten Daten Memchecks --track-origins=yes. Die Zeile, die den Wert verwendet, kann weit von der Speicherallokation entfernt sein, die nicht gesetzte Bytes bereitgestellt hat. Initialisiere jeden Wert, den die Berechnung liest, oder verwende calloc, wenn null der richtige Anfangszustand ist.
Die Korrektur auf mehreren Pfaden prüfen
Erstelle das Programm neu und führe die ursprüngliche fehlerauslösende Eingabe erneut aus. Fahre fort, bis der Bericht fehlerfrei ist, und versuche dann eine leere Eingabe, Wachstumsgrenzen, gewöhnliche Eingaben und Fehlerrückgaben. Compilerwarnungen erkennen manche Probleme vor der Ausführung; Laufzeitwerkzeuge prüfen die Pfade, die diese Tests erreichen.
Wichtige Erkenntnisse
- Reproduziere den Fehler mit einer kleinen Eingabe und lies den ersten Fehler.
- Ermittle anhand von Blockgröße und Zugriffsposition den tatsächlichen Grenzfehler.
- Prüfe auf einen Fehlschlag der Speicherallokation und räume auf, auch wenn der erste Zugriffsfehler die Ausführung anhält.
- Speicherleckberichte zeigen Allokationsstellen; der Besitz bestimmt die Freigabestellen.
- Erstelle das Programm nach der Korrektur neu, führe das reproduzierende Beispiel erneut aus und teste zusätzliche Pfade.
Dynamische Speicherverwaltung in C: Beispiele
1Ein Objekt durch Funktionsaufrufe verfolgen
9 Schritte
- 1EinführungEine Adresse benötigt ein bestehendes Objekt
Ein Zeiger ermöglicht es einem Programmteil, anderswo erzeugte Daten zu finden. Sein Nutzen hängt davon ab, ob diese Daten noch existieren, wenn der Zeiger verwendet wird.
Verfolge eine Hilfsfunktion, die ein Ganzzahlarray erzeugt, seine Adresse zurückgibt und ihren Aufrufer das Ergebnis verwenden lässt. Betrachte beim Nachvollziehen des Aufrufs den lokalen Zeiger und das Array getrennt: Sie müssen nicht dieselbe Lebensdauer haben.
- 2ErklärungDie drei Speicherdauern benennen
Eine gewöhnliche lokale Variable hat automatische Speicherdauer und endet mit ihrem Block. Eine globale oder lokale
static-Variable besteht während der Programmausführung. Ein allokierter Block besteht bis zu seiner Freigabe.Auf gängigen Systemen liegen automatische Objekte auf dem Aufrufstapel und Speicherallokationen auf dem Heap. Diese Namen beschreiben gängige Implementierungen; die Regeln von C legen die Lebensdauern unabhängig von der Speicheranordnung fest.
- 3BeispielDen lokalen Zeiger und den Block erzeugen
Innerhalb von
make_arrayerzeugtint *p = malloc(5 * sizeof *p);zwei getrennte Objekte: den lokalen Zeiger und bei Erfolg einen allokierten Block für fünf ints.Wenn ein
int4 Byte belegt, umfasst der Block 20 Byte. Prüfepvor der Verwendung. Die Zeigervariable gehört zum Funktionsaufruf, der allokierte Block ist aber kein Teil des automatischen Speichers dieses Aufrufs. - 4BeispielDie Adresse zurückgeben
Die Hilfsfunktion gibt
pzurück, und ihr Aufrufer speichert das Ergebnis inint *nums. Die Adresse wird kopiert, das Array nicht. Danach endet die Lebensdauer der lokalen Variablen der Hilfsfunktion.Bei Erfolg verweist
numsweiterhin auf das allokierte Array. Der Aufrufer prüft das Ergebnis, initialisiert und verwendet die Elemente und gibt den Block schließlich frei. Die Rückkehr hat den Block nicht freigegeben, weil seine Speicherdauer allokiert ist. - 5HerausforderungWas überdauert die Rückkehr?
Eine Hilfsfunktion erzeugt ein automatisches
int countund ein getrenntes Heap-Array. Sie gibt die Adresse des Arrays zurück. Welches dieser beiden Objekte kann der Aufrufer weiterhin verwenden?---reveal---
Das Heap-Array bleibt bis zu seiner Freigabe bestehen. Die Lebensdauer der automatischen Variablen
countder Hilfsfunktion endete bei der Rückkehr. Die kopierte Adresse verweist nur auf das Array; hätte die Hilfsfunktion stattdessen&countzurückgegeben, würde das Aufbewahren der Adressecountnicht erhalten. - 6BeispielVersuche, ein lokales Array zurückzugeben
Ändere die Hilfsfunktion nun so, dass sie
int values[5];verwendet undvalueszurückgibt. Dieses Array ist automatisch. Seine Lebensdauer endet, wenn die Hilfsfunktion zurückkehrt, sodass der Aufrufer kein noch bestehendes Objekt erhält, auf das der Zeiger zeigt.Die Größe beträgt weiterhin fünf ints, aber die Lebensdauer ist anders. Ein nutzbares Ergebnis kann stattdessen in einem vom Aufrufer bereitgestellten Puffer abgelegt oder allokiert und mit klarer Verantwortung für die Freigabe an den Aufrufer übertragen werden.
- 7VerbindungEine Vereinbarung für den Puffer wählen
Ein statischer Ergebnispuffer überdauert die Rückkehr, wird aber von jedem Aufruf gemeinsam genutzt. Ein späterer Aufruf kann ein früheres Ergebnis überschreiben. Ein Heap-Ergebnis kann jedem Aufruf eigenen, unabhängigen Speicher geben, für dessen Freigabe der Aufrufer verantwortlich ist.
Für vorübergehende Aufgaben ist ein kleines lokales Array fester Größe einfacher. Arrays mit eingabeabhängiger Größe sowie sehr große oder vergrößerbare Arrays benötigen eine geprüfte Speicherallokation, damit das Programm Fehlschläge behandeln und die Lebensdauer steuern kann.
- 8HerausforderungZwei Ergebnisse müssen unabhängig bleiben
Ein Aufrufer benötigt zwei beschreibbare Ergebnisse aus aufeinanderfolgenden Aufrufen und muss beide behalten. Er stellt keinen Puffer bereit. Würde ein einziges statisches Ergebnisarray diesen Bedarf erfüllen? Welche Lösung passt?
---reveal---
Ein einziges statisches Array würde gemeinsam genutzt und überschrieben. Gib bei jedem Aufruf ein separat allokiertes Ergebnis zurück, prüfe jedes Ergebnis und lass den Aufrufer beide nach ihrer letzten Verwendung freigeben. Die Lebensdauer auf dem Heap sichert den Fortbestand; getrennte Blöcke sichern die Unabhängigkeit.
- 9ZusammenfassungAdresse und Lebensdauer getrennt betrachten
Verfolge das Objekt, auf das ein Zeiger zeigt: Automatische lokale Variablen enden mit ihrem Block, statische Objekte bestehen während der Ausführung und allokierte Blöcke müssen gezielt freigegeben werden.
Frag dich bei einem zurückgegebenen Ergebnis, ob der Speicher bestehen bleibt und ob Aufrufer unabhängige Kopien benötigen. Frag dich bei vorübergehenden Aufgaben, ob bereits ein kleines lokales Array fester Größe ausreicht. Die benötigte Größe und Lebensdauer bestimmen die Lösung, bevor ein Allokationsaufruf geschrieben wird.
2Eine Tabelle allokieren und auf jedem Pfad aufräumen
13 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungErfolg und Fehlschlag einplanen
- 2ErklärungAnzahlen in Bytes umrechnen
- 3TippVor dem Multiplizieren prüfen
- 4HerausforderungAnzahl oder Zeigergröße?
- 5ErklärungZählern einen Anfangswert geben
- 6BeispielDie auf null gesetzte Zeile verwenden
- 7HerausforderungWas geschieht bei späterem Wachstum?
- 8ErklärungBlöcke zählen, nicht Zeigernamen
- 9BeispielDie Erstellung bei einem teilweisen Fehlschlag anhalten
- 10HerausforderungDie Reihenfolge beim Aufräumen wählen
- 11KernaussageEin Besitzer kann Zeiger verleihen
- 12HerausforderungEin Pfad endet vorzeitig
- 13ZusammenfassungDas Aufräumen zusammen mit der Erstellung planen
3Werte anhängen, ohne Daten zu verlieren
12 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungVor dem nächsten Speichern vergrößern
- 2ErklärungEine Größenänderung kann das Objekt ersetzen
- 3BeispielFehlschlag und Erfolg unterscheiden
- 4HerausforderungWas bleibt nach dem Verkleinern erhalten?
- 5ErklärungLänge und Kapazität beantworten unterschiedliche Fragen
- 6BeispielDie erste Wachstumsgrenze überschreiten
- 7BeispielDie nächste Grenze überschreiten
- 8KernaussageFreier Platz ermöglicht weniger Kopien
- 9HerausforderungWelcher Zustand bleibt nach einem fehlgeschlagenen Anhängen?
- 10VerbindungEine Zeichenkette reserviert einen Platz für das abschließende Nullzeichen
- 11HerausforderungEine geliehene Auswahl übersteht eine Vergrößerung
- 12ZusammenfassungDen Zustand erst nach Erfolg übernehmen
4Die vollständige Geschichte eines Blocks untersuchen
15 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungVier Fragen zu jedem Zugriff
- 2BeispielEine Speicherallokation durch Ersetzen ihrer Adresse verlieren
- 3HerausforderungRäumt das Zurücksetzen des Zeigers auf?
- 4TippAuch Ausgänge und Schleifen untersuchen
- 5BeispielEinmal freigeben, alle Aliase ungültig machen
- 6HerausforderungDie letzte Verwendung korrigieren
- 7KernaussageErwartete Ausgabe kann aus ungültigem Code stammen
- 8BeispielDas abschließende Nullzeichen mitzählen
- 9BeispielAllokiert bedeutet nicht initialisiert
- 10HerausforderungZwei getrennte Grenzen finden
- 11ErklärungEine Speicherzugriffsverletzung ist ein Symptom
- 12BeispielDen abstürzenden Zugriff lesen
- 13HerausforderungEine Ausgabe lässt den Absturz verschwinden
- 14HerausforderungBesitz und Grenzen zusammen betrachten
- 15ZusammenfassungDie verletzte Regel erkennen
5Einen Bericht lesen und die Korrektur prüfen
14 Schritte
Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.
- 1EinführungAus einer Meldung eine Blockgeschichte ableiten
- 2ErklärungEine gewöhnliche Debug-Version für Memcheck verwenden
- 3BeispielDie Adressbeschreibung deuten
- 4HerausforderungBytes als Bytes lesen
- 5TippEine Berichtsoption für das Problem wählen
- 6ErklärungEine ausführbare ASan-Datei separat erstellen
- 7BeispielEinen Zugriff nach der Speicherfreigabe anhand dreier Aufrufstapel verfolgen
- 8HerausforderungEin fehlerfreier ASan-Lauf übersieht einen Wertefehler
- 9BeispielDen Bericht der Schleife zuordnen
- 10BeispielDie Arbeit innerhalb der gültigen Lebensdauer ausführen
- 11HerausforderungWo sollte ein Speicherleck behoben werden?
- 12VerbindungPrüfung beim Beenden und Plattformunterstützung beachten
- 13HerausforderungDen Korrekturablauf abschließen
- 14ZusammenfassungHinweise nutzen, korrigieren und erneut ausführen
Dynamische Speicherverwaltung in C: Übungsaufgaben
Set 1: Lebensdauern, Bytezahlen und erste Prüfungen10 Übungsfragen
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Frage 2 von 10
Antwort zeigenAntwort ausblenden
Frage 3 von 10
sizeof(int) gleich 4. Wie viele Byte sollte die Speicherallokation anfordern?Antwort zeigenAntwort ausblenden
Frage 4 von 10
hits[4] nach einem erfolgreichen int *hits = calloc(6, sizeof *hits); vor späteren Zuweisungen?Antwort zeigenAntwort ausblenden
calloc setzt jedes Byte auf null, wodurch Ganzzahlelemente den Wert null erhalten. Index 4 liegt innerhalb des Arrays mit sechs Elementen und kann nach der Prüfung der Speicherallokation als null gelesen werden. Frisch mit malloc allokierter Speicher wäre nicht initialisiert; diese Regel gilt nicht für diese erfolgreiche calloc-Allokation.Frage 5 von 10
Antwort zeigenAntwort ausblenden
Frage 6 von 10
int *tmp = realloc(data, bytes); und gibt NULL zurück. Der ursprüngliche Block war gültig. Was kann der Besitzer nun tun?Antwort zeigenAntwort ausblenden
realloc mit positiver Größe lässt die ursprüngliche Speicherallokation und ihren Inhalt unverändert. Da das Ergebnis in tmp aufgefangen wurde, enthält data weiterhin die gültige Adresse. Die Freigabe des temporären Nullzeigers bewirkt nichts; sie kann den noch im Besitz befindlichen Block, auf den data zeigt, nicht freigeben.Frage 7 von 10
Antwort zeigenAntwort ausblenden
Frage 8 von 10
int *view = owner; free(owner); owner = NULL; aus. Was geschieht, wenn er danach view[0] liest?Antwort zeigenAntwort ausblenden
view war ein Alias auf dieselbe Speicherallokation, deren Lebensdauer bei free(owner) endete. Die Zuweisung von NULL an owner aktualisiert nur diese Zeigervariable. Sie ändert weder view noch stellt sie den alten Block wieder her. Der spätere Lesezugriff ist daher ein Zugriff nach der Speicherfreigabe und somit undefiniertes Verhalten.Frage 9 von 10
strcpy in einen neuen Heap-Puffer. Wie groß muss die Speicherallokation mindestens in C-Bytes sein?Antwort zeigenAntwort ausblenden
strcpy speichert zusätzlich das abschließende Nullzeichen. Der kleinste Puffer umfasst daher acht Byte. Sieben Byte würden die sichtbaren Buchstaben aufnehmen, aber das abschließende Nullzeichen läge außerhalb der Speicherallokation. Das verursacht einen Schreibzugriff außerhalb der Speichergrenzen, selbst wenn die Ausgabe zu funktionieren scheint.Frage 10 von 10
Ein globales int *saved ist anfangs gleich NULL. Eine Hilfsfunktion führt diesen Code aus und kehrt zurück. Ints belegen 4 Byte. Memcheck meldet einen ungültigen Schreibzugriff unmittelbar hinter der Speicherallokation. Welcher Fehler beim Durchlaufen und welche Speicherleckkategorie beim Beenden ergeben sich?
int *p = malloc(7 * sizeof *p);
if (p == NULL) return;
for (size_t i = 0; i <= 7; ++i) p[i] = (int)i;
saved = p;
Antwort zeigenAntwort ausblenden
Set 2: Allokations- und Aufräumpfade verfolgen10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
p zurück. Der Aufrufer speichert das Ergebnis in values. Was existiert unmittelbar nach der Rückkehr der Hilfsfunktion noch?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
n ungleich null vor der Speicherallokation, dass n * sizeof *p nicht in size_t passt?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
a, prüft ihn und versucht dann, b zu allokieren. Die zweite Speicherallokation schlägt fehl. Sie besitzt a und wird sofort einen Fehler zurückgeben. Welches Aufräumen ist nötig?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
static int visits = 0; und führt return ++visits; aus. Welchen Wert gibt ihr dritter Aufruf zurück?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
free ab, nachdem eine Schleife ein Heap-Array gefüllt hat. Was sollte die Untersuchung prüfen, bevor der Freigabeaufruf geändert wird?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Ein Programm beginnt mit diesem geprüften Block und verzweigt später anhand von p[5]. Eine Größenänderung auf eine positive Größe war erfolgreich. ASan meldet nichts, während Memcheck an der Verzweigung einen nicht gesetzten Wert meldet. Welche Korrektur passt zur vollständigen Vorgeschichte?
int *p = calloc(4, sizeof *p);
if (p == NULL) return EXIT_FAILURE;
int *tmp = realloc(p, 6 * sizeof *p);
if (tmp == NULL) { free(p); return EXIT_FAILURE; }
p = tmp;
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 3: Wachstum, Lebensdauern und Hinweise aus Berichten10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
int *p = malloc(11 * sizeof p);. Wie viele Byte fordert er an, und was ist an dem Ausdruck falsch?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
calloc mit positiver Größe?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
p besessen. Code speichert Index 4 und einen Zeiger auf dieses Element. Ein Versuch, über einen temporären Zeiger auf neun ints zu vergrößern, schlägt fehl. Eine spätere Verkleinerung auf drei gelingt, und der besitzende Zeiger wird aktualisiert. Kann die Auswahl danach verwendet werden?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
old, setzt cap = 16 und weist data = realloc(data, cap * sizeof *data) zu. Diese Anforderung positiver Größe schlägt fehl, bevor ein weiterer Wert gespeichert wird. Welcher Zustand stellt das unveränderte Array wieder her?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
buffer. Nach dessen letzter Verwendung führt sie buffer = NULL; free(buffer); aus. Was geschieht mit dem ursprünglichen Block?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
p frei und ruft später free(alias) auf, wobei alias vor der Freigabe aus p kopiert wurde. Welcher Fehler ist das?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
stdout hat keinen Zeilenumbruch, danach stürzt das Programm ab. Ihr Text erscheint nie. Warum lässt sich daraus nicht zuverlässig ablesen, wie weit die Ausführung gekommen ist?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
program-asan mit AddressSanitizer kompiliert und möchte einen unabhängigen Memcheck-Lauf durchführen. Welche ausführbare Datei sollte Memcheck erhalten?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
items[n] = 0; unmittelbar hinter einer Speicherallokation für n ints an. Die Schleife verwendet i <= n, das Ergebnis wird nicht geprüft und die Freigabe fehlt. Welche Korrektur behebt die Ursache und die verbleibenden Besitzfehler?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 4: Besitz bei veränderten Bedingungen korrigieren10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
calloc allokiert und den Block später erfolgreich auf sieben Zähler vergrößert. Was muss geschehen, bevor Zähler 6 zum ersten Mal erhöht wird?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
p besessen. Ein Versuch, ihn über tmp auf sieben ints zu vergrößern, schlägt fehl und lässt p unverändert. Der Code führt dann p = realloc(p, 9 * sizeof *p) aus, und auch diese Anforderung schlägt fehl. Es gibt keine Aliase. Welcher allokierte Block ist nun verloren?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
char **labels = malloc(4 * sizeof *labels);, prüft das Ergebnis und führt dann sofort strcpy(labels[0], "east"); aus. Welche Lösung korrigiert den ersten Eintrag und regelt zugleich seinen Besitz eindeutig?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Set 5: Vollständige Speichergeschichten verfolgen10 Übungsfragen
Speichere diese Einheit, um jede Antwort zu sehen und jedes Set zu üben.
Frage 1 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 2 von 10
sizeof samples bei einem gewöhnlichen double *samples?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 3 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 4 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 5 von 10
X erfolgreich ohne Vergrößerung an und aktualisiert dabei das Nullzeichen. Das nächste Zeichen Y löst eine Vergrößerung über einen temporären Zeiger aus, aber die Anforderung schlägt fehl. Die Schnittstelle verspricht, die Zeichenkette bei einem Fehlschlag zu erhalten. Welcher Zustand bleibt bestehen?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 6 von 10
a und 40 Byte in die lokale Variable b. Auf dem Fehlerpfad gibt er a frei, setzt b = NULL und kehrt zurück. Es bleiben keine Aliase. Welche Gesamtgröße an Speicher geht verloren, und welche Änderung verhindert das?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 7 von 10
Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 8 von 10
int *p = malloc(6 * sizeof p);, prüft den Erfolg, initialisiert die Indizes 0 bis 5 und summiert dann die Indizes 0 bis 11. Welche Diagnose passt zur gesamten Summe?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 9 von 10
p[index] liest. Was sollte bei diesem Zugriff geprüft werden?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Frage 10 von 10
display, eine frühere Freigabe in reset und eine Speicherallokation in load. Der Aufrufer ruft load, reset und dann display für dasselbe Ergebnis auf. Er benötigt diese Ausgabe. Welche Korrektur folgt aus der Blockgeschichte?Antwort zeigenAntwort ausblenden
Speichere diese Einheit, um diese Antwort zu sehen.
Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe
Schlüsselbegriffe
A
abschließendes Nullzeichen
AddressSanitizer
Alias
NULL gesetzt, bleiben gespeicherte Aliase für spätere Lesezugriffe unbrauchbar.Allokationsstelle
Array variabler Länge
int a[n] bietet keine Prüfung auf einen Nullzeiger als Fehlerergebnis, wie sie bei einer Speicherallokation auf dem Heap möglich ist.Aufräumpfad
Aufrufrückverfolgung
Aufrufstapel
Ausgabepufferung
fflush(stdout) nach einer Fortschrittsausgabe stellt ihren gepufferten Text vor der nächsten Operation bereit.automatischer Speicher
B
Besitz
C
calloc
calloc reserviert ein Array anhand einer Anzahl und einer Elementgröße, setzt seine Bytes auf null und gibt bei einem Fehlschlag der Speicherallokation einen Nullzeiger zurück.calloc-Anforderung beginnen vor der ersten Erhöhung bei null.D
definitiv verloren
doppelte Speicherfreigabe
free für denselben zuvor bestehenden Block aufrufen.dynamisches Array
E
erster gemeldeter Fehler
F
fehlerauslösende Eingabe
free
free gibt einen noch bestehenden allokierten Block zur Wiederverwendung frei, wenn ihr dessen Anfangsadresse übergeben wird, und bewirkt nichts, wenn ihr NULL übergeben wird.G
geliehener Zeiger
gemeinsam genutzter statischer Puffer
geometrisches Wachstum
H
hängender Zeiger
Heap
Heap-Pufferüberlauf
Heap-Übersicht
I
indirekt verloren
Initialisierung
Instrumentierung
-fsanitize=address beim Kompilieren und Linken erzeugt eine mit ASan instrumentierte ausführbare Datei.K
Kapazität
L
Länge
LeakSanitizer
Lesezugriff auf einen nicht initialisierten Wert
M
malloc
malloc reserviert eine angeforderte Anzahl von Bytes und gibt eine Blockadresse oder einen Nullzeiger zurück, ohne die gespeicherten Werte zu initialisieren.malloc erhaltenes Ganzzahlarray benötigt Zuweisungen, bevor seine Elemente addiert oder verglichen werden.Memcheck
MemorySanitizer
mit Nullen gefüllter Speicher
calloc beginnt mit einem Nullzeichen und enthält eine leere Zeichenkette.möglicherweise verloren
N
noch erreichbar
Nullzeiger
O
Objektlebensdauer
realloc beendet die Lebensdauer des alten Objekts, selbst wenn die zurückgegebene Adresse unverändert aussieht.Off-by-one-Fehler
i <= n zu i < n lässt einen bei null beginnenden Durchlauf genau die n vorgesehenen Elemente besuchen.P
Prüflauf
R
realloc
realloc ersetzt einen allokierten Block durch ein Objekt der angeforderten Größe, erhält den passenden Anfangsteil und ändert möglicherweise dessen Adresse.S
size_t
size_t ist der vorzeichenlose Ganzzahltyp für Objektgrößen und Bytezahlen bei Speicherallokationen, einschließlich des Ergebnisses von sizeof.size_t-Variablen für Länge und Kapazität, damit diese Objektgrößen beschreiben können.sizeof
sizeof gibt die Größe eines Typs oder Objekts in C-Bytes an; bei einem Zeiger misst er den Zeiger statt des allokierten Speichers, auf den er zeigt.n * sizeof *p richtet sich nach dem Elementtyp, während sizeof p die Länge des Arrays nicht ermitteln kann.Speicherallokation
Speicherdauer
Speicherleck
Speicherzugriffsverletzung
Stapelüberlauf
statischer Speicher
static deklarierte lokale Variablen verwendet.T
temporärer Zeiger
tmp = realloc(p, bytes) bleibt bei einem Nullergebnis p für die weitere Verwendung oder das Aufräumen verfügbar.Ü
Überlauf bei der Größenberechnung
size_t überschreitet und durch Wraparound einen kleineren darstellbaren Wert ergibt.SIZE_MAX / sizeof *p, bevor du sie mit der Elementgröße multiplizierst.U
UndefinedBehaviorSanitizer
-fsanitize=address,undefined verwenden, um die beiden kompatiblen Prüfungssammlungen zu aktivieren.undefiniertes Verhalten
Ursprungsverfolgung
--track-origins=yes kann erklären, warum eine spätere Verzweigung von nicht gesetzten Heap-Daten abhängt.V
verlorener Speicherblock
NULL gesetzt, ohne vorher free aufzurufen, geht die Adresse des allokierten Blocks verloren.verschachtelte Speicherallokation
vom Aufrufer bereitgestellter Puffer
W
Wiederverwendung von Puffern
Z
Zeichenkettenliteral
char word[] = "hello";, wenn eine lokale beschreibbare Kopie statt eines Zeigers auf das Literal benötigt wird.Zeiger ins Objektinnere
Zugriff nach der Speicherfreigabe
Über diese Einheit
Erstelle deine eigenen Lernmaterialien
Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.