Dynamische Speicherverwaltung in C

Lerne, wie ein C-Programm während der Ausführung Speicher erhält und wieder freigibt. Vergleiche Stack und Heap, reserviere Blöcke mit malloc und calloc, ändere ihre Größe mit realloc und gib sie mit free frei. Erkenne Speicherlecks, hängende Zeiger, doppelte Speicherfreigaben und Zugriffe außerhalb der Speichergrenzen, verstehe, warum sie Abstürze wie Speicherzugriffsverletzungen verursachen, und finde und behebe sie mit Valgrind und AddressSanitizer.

AnfängerEinheit 5 von 8 in Kurs: C-Programmierung – Zeiger, Speicherverwaltung und sicheres C

Was du lernst

  1. Beschreibe Stack, Heap und statischen Speicher, wie lange Daten jeweils existieren und wann ein Programm Heap-Speicher benötigt.
    • Stack, Heap und statischen Speicher vergleichen
      Beschreibe, 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 sollten
      Wä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.
  2. 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 anfordern
      Berechne die anzufordernde Bytezahl, rufe malloc auf, prüfe auf Fehlschlag und verwende den Block als Array.
    • Mit calloc mit Nullen gefüllten Speicher anfordern
      Verwende 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 behalten
      Gib jeden Block zum richtigen Zeitpunkt genau einmal frei, entscheide, welcher Programmteil den jeweiligen Block besitzt, und räume bei einer fehlgeschlagenen Speicherallokation korrekt auf.
  3. 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 verkleinern
      Fange 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 implementieren
      Speichere 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.
  4. 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 verhindern
      Erklä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 vermeiden
      Erkenne 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 Werte
      Halte jeden Zugriff innerhalb der Grenzen seines Blocks, bemesse Zeichenkettenpuffer korrekt und setze Speicherwerte vor dem Lesen.
    • Speicherzugriffsverletzungen erklären und ihre Ursachen zurückverfolgen
      Erkläre, was eine Speicherzugriffsverletzung ist, nenne ihre häufigen Ursachen und verfolge den Absturz bis zur fehlerhaften Zeile zurück.
  5. 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üfen
      Erstelle ein Programm für Valgrind, führe es mit Speicherleckprüfung aus und lies seine Fehlermeldungen, Heap-Übersicht und Speicherleckkategorien.
    • AddressSanitizer und LeakSanitizer verwenden
      Erstelle ein Programm mit AddressSanitizer, lies dessen Berichte und wähle zwischen Sanitizern und Valgrind.
    • Speicherfehler anhand eines Prüfberichts beheben
      Gehe 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.

Dynamische Speicherverwaltung in C einfach erklärt

5 Kapitel

1

Erklä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
SpeicherdauerTypische ObjekteEnde der Lebensdauer
AutomatischGewöhnliche lokale Variablen und ParameterWenn ihr Block endet
StatischGlobale Variablen und als static deklarierte lokale VariablenWenn das Programm endet
AllokiertVon Speicherallokationsfunktionen erhaltene BlöckeWenn 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
BedarfGeeignete Ausgangswahl
Ein kleines festes Array während eines AufrufsLokales Array
Eine aus der Eingabe gelesene AnzahlGeprüfte Speicherallokation auf dem Heap
Ein Ergebnis, das seine erzeugende Funktion überdauertSpeicherallokation auf dem Heap oder Puffer im Besitz des Aufrufers
Daten, die mit der Eingabe wachsenGrößenveränderbarer Heap-Puffer
Ein sehr großes ArraySpeicherallokation 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.
2

Heap-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 malloc allokierte Element vor dem Lesen.
  • Speichere die Elementanzahl gesondert; sizeof pointer kann 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 VerwendungNaheliegende Wahl
Ganzzahlzähler erhöhencalloc
Ganzzahlmerker nach und nach aus einem Zustand setzen, in dem alle null sindcalloc
Jedes Element sofort überschreibenmalloc

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 calloc eine 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 realloc werden 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 NULL ist ein gültiges Argument für free.
  • 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 free freigegeben werden.
3

Die 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 free frei 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.
4

Speicherfehler 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 realloc macht 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 n Elemente reichen von null bis n - 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.
5

Speicherfehler 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
BerichtskategorieBedeutung
Definitely lostKein Zeiger auf den Block ist mehr vorhanden
Indirectly lostDer Block ist nur über einen anderen verlorenen Block erreichbar
Possibly lostEs wurde nur ein Zeiger ins Objektinnere gefunden
Still reachableBeim 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
BedarfNützliches Werkzeug
Schnelle Prüfungen während der Entwicklung, einschließlich der Grenzen lokaler ArraysASan-Version
Verfolgung nicht initialisierter Werte ohne Sanitizer-InstrumentierungMemcheck auf einem unterstützten System
Speicherlecks beim BeendenLeakSanitizer 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

5 Interaktive Lektionen

1

Ein Objekt durch Funktionsaufrufe verfolgen

9 Schritte
Verfolge lokale, statische und allokierte Daten und wähle dann Speicher für ein zurückgegebenes Ergebnis.
  1. 1
    Einfü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.

  2. 2
    Erklä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.

  3. 3
    BeispielDen lokalen Zeiger und den Block erzeugen

    Innerhalb von make_array erzeugt int *p = malloc(5 * sizeof *p); zwei getrennte Objekte: den lokalen Zeiger und bei Erfolg einen allokierten Block für fünf ints.

    Wenn ein int 4 Byte belegt, umfasst der Block 20 Byte. Prüfe p vor der Verwendung. Die Zeigervariable gehört zum Funktionsaufruf, der allokierte Block ist aber kein Teil des automatischen Speichers dieses Aufrufs.

  4. 4
    BeispielDie Adresse zurückgeben

    Die Hilfsfunktion gibt p zurück, und ihr Aufrufer speichert das Ergebnis in int *nums. Die Adresse wird kopiert, das Array nicht. Danach endet die Lebensdauer der lokalen Variablen der Hilfsfunktion.

    Bei Erfolg verweist nums weiterhin 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.

  5. 5
    HerausforderungWas überdauert die Rückkehr?

    Eine Hilfsfunktion erzeugt ein automatisches int count und 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 count der Hilfsfunktion endete bei der Rückkehr. Die kopierte Adresse verweist nur auf das Array; hätte die Hilfsfunktion stattdessen &count zurückgegeben, würde das Aufbewahren der Adresse count nicht erhalten.

  6. 6
    BeispielVersuche, ein lokales Array zurückzugeben

    Ändere die Hilfsfunktion nun so, dass sie int values[5]; verwendet und values zurü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.

  7. 7
    VerbindungEine 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.

  8. 8
    HerausforderungZwei 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.

  9. 9
    ZusammenfassungAdresse 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.

2

Eine Tabelle allokieren und auf jedem Pfad aufräumen

13 Schritte
Gehe von Elementgrößen zu geprüften Anforderungen, initialisierten Werten und dem Aufräumen nach teilweisem Erfolg.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungErfolg und Fehlschlag einplanen
  2. 2
    ErklärungAnzahlen in Bytes umrechnen
  3. 3
    TippVor dem Multiplizieren prüfen
  4. 4
    HerausforderungAnzahl oder Zeigergröße?
  5. 5
    ErklärungZählern einen Anfangswert geben
  6. 6
    BeispielDie auf null gesetzte Zeile verwenden
  7. 7
    HerausforderungWas geschieht bei späterem Wachstum?
  8. 8
    ErklärungBlöcke zählen, nicht Zeigernamen
  9. 9
    BeispielDie Erstellung bei einem teilweisen Fehlschlag anhalten
  10. 10
    HerausforderungDie Reihenfolge beim Aufräumen wählen
  11. 11
    KernaussageEin Besitzer kann Zeiger verleihen
  12. 12
    HerausforderungEin Pfad endet vorzeitig
  13. 13
    ZusammenfassungDas Aufräumen zusammen mit der Erstellung planen
3

Werte anhängen, ohne Daten zu verlieren

12 Schritte
Verfolge Länge, Kapazität und fehlgeschlagene Größenänderungen eines dynamischen Arrays, auch bei einer Zeichenkette mit abschließendem Nullzeichen.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungVor dem nächsten Speichern vergrößern
  2. 2
    ErklärungEine Größenänderung kann das Objekt ersetzen
  3. 3
    BeispielFehlschlag und Erfolg unterscheiden
  4. 4
    HerausforderungWas bleibt nach dem Verkleinern erhalten?
  5. 5
    ErklärungLänge und Kapazität beantworten unterschiedliche Fragen
  6. 6
    BeispielDie erste Wachstumsgrenze überschreiten
  7. 7
    BeispielDie nächste Grenze überschreiten
  8. 8
    KernaussageFreier Platz ermöglicht weniger Kopien
  9. 9
    HerausforderungWelcher Zustand bleibt nach einem fehlgeschlagenen Anhängen?
  10. 10
    VerbindungEine Zeichenkette reserviert einen Platz für das abschließende Nullzeichen
  11. 11
    HerausforderungEine geliehene Auswahl übersteht eine Vergrößerung
  12. 12
    ZusammenfassungDen Zustand erst nach Erfolg übernehmen
4

Die vollständige Geschichte eines Blocks untersuchen

15 Schritte
Unterscheide Speicherlecks, ungültig gewordene Aliase, Grenzfehler und nicht gesetzte Werte und verfolge dann einen Absturz bis zu seiner Ursache zurück.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungVier Fragen zu jedem Zugriff
  2. 2
    BeispielEine Speicherallokation durch Ersetzen ihrer Adresse verlieren
  3. 3
    HerausforderungRäumt das Zurücksetzen des Zeigers auf?
  4. 4
    TippAuch Ausgänge und Schleifen untersuchen
  5. 5
    BeispielEinmal freigeben, alle Aliase ungültig machen
  6. 6
    HerausforderungDie letzte Verwendung korrigieren
  7. 7
    KernaussageErwartete Ausgabe kann aus ungültigem Code stammen
  8. 8
    BeispielDas abschließende Nullzeichen mitzählen
  9. 9
    BeispielAllokiert bedeutet nicht initialisiert
  10. 10
    HerausforderungZwei getrennte Grenzen finden
  11. 11
    ErklärungEine Speicherzugriffsverletzung ist ein Symptom
  12. 12
    BeispielDen abstürzenden Zugriff lesen
  13. 13
    HerausforderungEine Ausgabe lässt den Absturz verschwinden
  14. 14
    HerausforderungBesitz und Grenzen zusammen betrachten
  15. 15
    ZusammenfassungDie verletzte Regel erkennen
5

Einen Bericht lesen und die Korrektur prüfen

14 Schritte
Wähle ein Prüfwerkzeug, deute Bytes und Aufrufstapel und behebe einen Überlauf sowie fehlendes Aufräumen.

Speichere diese Einheit, um jede Interaktive Lektion Schritt für Schritt durchzugehen.

  1. 1
    EinführungAus einer Meldung eine Blockgeschichte ableiten
  2. 2
    ErklärungEine gewöhnliche Debug-Version für Memcheck verwenden
  3. 3
    BeispielDie Adressbeschreibung deuten
  4. 4
    HerausforderungBytes als Bytes lesen
  5. 5
    TippEine Berichtsoption für das Problem wählen
  6. 6
    ErklärungEine ausführbare ASan-Datei separat erstellen
  7. 7
    BeispielEinen Zugriff nach der Speicherfreigabe anhand dreier Aufrufstapel verfolgen
  8. 8
    HerausforderungEin fehlerfreier ASan-Lauf übersieht einen Wertefehler
  9. 9
    BeispielDen Bericht der Schleife zuordnen
  10. 10
    BeispielDie Arbeit innerhalb der gültigen Lebensdauer ausführen
  11. 11
    HerausforderungWo sollte ein Speicherleck behoben werden?
  12. 12
    VerbindungPrüfung beim Beenden und Plattformunterstützung beachten
  13. 13
    HerausforderungDen Korrekturablauf abschließen
  14. 14
    ZusammenfassungHinweise nutzen, korrigieren und erneut ausführen

Dynamische Speicherverwaltung in C: Übungsaufgaben

50 Übungsfragen in 5 Sets

Set 1: Lebensdauern, Bytezahlen und erste Prüfungen10 Übungsfragen

Frage 1 von 10

Ein Puffer enthält sechs initialisierte ints. Code speichert den Index 4, verkleinert den Block erfolgreich auf drei ints und benötigt dann das zuvor ausgewählte Element. Was ist ein gültiger nächster Schritt?
  1. Den ausgewählten Zeiger als &data[4] neu berechnen.
  2. Die Auswahl verwerfen, weil Index 4 entfernt wurde.
  3. Den alten ausgewählten Zeiger vor einer weiteren Allokation lesen.
  4. Index 4 lesen, wenn die Adresse nach der Größenänderung gleich blieb.
Antwort zeigenAntwort ausblenden
Antwort: B
Beim Verkleinern bleiben nur die ersten drei Elemente an den Indizes null bis zwei erhalten. Index 4 bezeichnet kein Element im Block mit geänderter Größe mehr und kann daher nicht durch Neuberechnung eines Zeigers wiederhergestellt werden. Bei Erfolg endete auch die Lebensdauer des alten Objekts, selbst wenn die Adresse gleich blieb; gespeicherte Zeiger können verworfene Daten nicht retten.

Frage 2 von 10

Eine Funktion muss ein beschreibbares Textergebnis erzeugen, das ihre Rückkehr überdauert. Der Aufrufer stellt keinen Puffer bereit, und verschiedene Aufrufe müssen unabhängige Ergebnisse behalten. Welche Speicherlösung passt?
  1. Ein lokales Array zurückgeben; der Aufrufer behält seine Adresse.
  2. Einen statischen Puffer zurückgeben; der Aufrufer gibt ihn frei.
  3. Das Literal zurückgeben; der Aufrufer ändert seine Buchstaben.
  4. Eine allokierte Kopie zurückgeben; der Aufrufer gibt sie frei.
Antwort zeigenAntwort ausblenden
Antwort: D
Eine allokierte Kopie überdauert den erzeugenden Aufruf und gibt jedem Ergebnis unabhängigen Speicher. Ein statischer Puffer bleibt ebenfalls bestehen, teilt aber ein Ergebnis zwischen Aufrufen und kann vom Aufrufer nicht freigegeben werden. Die Lebensdauer eines lokalen Arrays endet bei der Rückkehr, und ein Zeichenkettenliteral bietet keinen beschreibbaren Speicher.

Frage 3 von 10

Ein Array benötigt 13 ints. Auf diesem System ist sizeof(int) gleich 4. Wie viele Byte sollte die Speicherallokation anfordern?
  1. 52 Byte
  2. 13 Byte
  3. 17 Byte
  4. 104 Byte
Antwort zeigenAntwort ausblenden
Antwort: A
Die Anforderung benötigt 13 Elemente zu je 4 Byte, also 52 Byte. Eine Anforderung von 13 Byte verwechselt die Elementanzahl mit der Bytezahl; sie bietet auf diesem System keinen Platz für dreizehn ints.

Frage 4 von 10

Welchen Wert hat hits[4] nach einem erfolgreichen int *hits = calloc(6, sizeof *hits); vor späteren Zuweisungen?
  1. Einen nicht initialisierten Wert
  2. Die Ganzzahl 6
  3. 0
  4. Die Ganzzahl 4
Antwort zeigenAntwort ausblenden
Antwort: C
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

Eine Funktion allokiert ein Zeigerarray und vier separate Zeilenblöcke. Alle fünf Speicherallokationen gelingen, und sie besitzt alle Blöcke. Wie sollte sie die Tabelle freigeben?
  1. Das Zeigerarray freigeben und dann seine Zeilenzeiger lesen.
  2. Nur das Zeigerarray freigeben; es besitzt auch die Zeilen.
  3. Alle vier Zeilen und dann das Zeigerarray freigeben.
  4. Jede Zeile zweimal und dann das Zeigerarray freigeben.
Antwort zeigenAntwort ausblenden
Antwort: C
Die Zeilen sind vier separate Speicherallokationen, und ihre Adressen stehen im äußeren Array. Gib sie frei, solange diese Adressen noch verfügbar sind, und gib dann den äußeren Block frei. Wird nur das Zeigerarray freigegeben, bleiben die Zeilen unfreigegeben; wird es zuerst freigegeben, sind spätere Lesezugriffe auf seine Einträge außerdem ungültig.

Frage 6 von 10

Eine Größenänderung auf eine positive Größe verwendet int *tmp = realloc(data, bytes); und gibt NULL zurück. Der ursprüngliche Block war gültig. Was kann der Besitzer nun tun?
  1. Den unveränderten ursprünglichen Block weiterverwenden oder freigeben.
  2. Die neuen Bytes über tmp verwenden, nachdem der Zeiger zurückgesetzt wurde.
  3. tmp freigeben, um den ursprünglichen Block sofort freizugeben.
  4. data erst verwenden, nachdem erneut Speicher dafür allokiert wurde.
Antwort zeigenAntwort ausblenden
Antwort: A
Ein fehlgeschlagenes 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

Ein dynamisches Array beginnt mit Länge 0 und Kapazität 3. Es verdoppelt seine Kapazität nur, wenn es voll ist. Wie groß sind Länge und Kapazität nach acht erfolgreichen Anhängevorgängen?
  1. Länge 8, Kapazität 8
  2. Länge 12, Kapazität 12
  3. Länge 8, Kapazität 9
  4. Länge 8, Kapazität 12
Antwort zeigenAntwort ausblenden
Antwort: D
Beim vierten Anhängen wächst die Kapazität von 3 auf 6 und beim siebten von 6 auf 12. Acht Werte sind nun belegt, daher ist die Länge 8. Kapazität 8 würde ein Wachstum auf genau die benötigte Größe beschreiben, nicht die angegebene Verdopplungsregel; freie Kapazität erhöht die Anzahl gespeicherter Werte nicht.

Frage 8 von 10

Nach einer erfolgreichen Speicherallokation führt Code int *view = owner; free(owner); owner = NULL; aus. Was geschieht, wenn er danach view[0] liest?
  1. Er liest gültigen Speicher, weil view kein Nullzeiger ist.
  2. Er greift auf ein Objekt zu, dessen Lebensdauer beendet ist.
  3. Er liest null, weil owner auf null zurückgesetzt wurde.
  4. Er erzeugt eine neue Speicherallokation an der früheren Adresse.
Antwort zeigenAntwort ausblenden
Antwort: B
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

Ein Programm kopiert die Zeichenkette „network“ mit strcpy in einen neuen Heap-Puffer. Wie groß muss die Speicherallokation mindestens in C-Bytes sein?
  1. 7 Byte
  2. 9 Byte
  3. 16 Byte
  4. 8 Byte
Antwort zeigenAntwort ausblenden
Antwort: D
Die sieben Buchstaben benötigen sieben Byte, und 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;
  1. Ein int zu viel wird geschrieben; der Block ist noch erreichbar.
  2. Vier ints zu viel werden geschrieben; der Block ist definitiv verloren.
  3. Ein int zu viel wird geschrieben; der Block ist definitiv verloren.
  4. Vier ints zu viel werden geschrieben; der Block ist noch erreichbar.
Antwort zeigenAntwort ausblenden
Antwort: A
Der Block mit sieben ints erlaubt die Indizes null bis sechs, aber die Schleife schreibt auch an Index sieben. Die Zugriffsbreite beträgt ein int zu vier Byte. Die globale Variable behält die Anfangsadresse nach der Rückkehr der Hilfsfunktion, sodass der nicht freigegebene Block noch erreichbar und nicht definitiv verloren ist.
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

Eine Hilfsfunktion gibt ein erfolgreich allokiertes Array über einen lokalen Zeiger p zurück. Der Aufrufer speichert das Ergebnis in values. Was existiert unmittelbar nach der Rückkehr der Hilfsfunktion noch?
  1. Die Variable p der Hilfsfunktion und das allokierte Array.
  2. Die Variable p der Hilfsfunktion und alle ihre Parameter.
  3. Das allokierte Array und die Variable values des Aufrufers.
  4. Die Variable values des Aufrufers, aber kein allokiertes Array.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Welche Bedingung erkennt bei einer Anzahl n ungleich null vor der Speicherallokation, dass n * sizeof *p nicht in size_t passt?
  1. n > SIZE_MAX / sizeof *p
  2. n * sizeof *p > SIZE_MAX
  3. sizeof p > n / SIZE_MAX
  4. n > SIZE_MAX * sizeof *p
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Eine Funktion allokiert den Puffer 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?
  1. b freigeben und zurückkehren, ohne a anzufassen.
  2. a auf null setzen und dann einen Fehler zurückgeben.
  3. Einen Fehler zurückgeben; das Aufräumen lokaler Zeiger gibt a frei.
  4. a freigeben, bevor der Fehler zurückgegeben wird.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Eine Funktion deklariert static int visits = 0; und führt return ++visits; aus. Welchen Wert gibt ihr dritter Aufruf zurück?
  1. 1
  2. 3
  3. 0
  4. 2
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein Zeichenkettenpuffer hat eine Kapazität von 12 Byte und enthält 11 Zeichen sowie sein abschließendes Nullzeichen. Was muss vor dem Anhängen eines weiteren Zeichens geschehen?
  1. Das abschließende Nullzeichen überschreiben und die Größe unverändert lassen.
  2. Die Länge zweimal erhöhen und nach dem nächsten Anhängen vergrößern.
  3. An Index 12 schreiben und das alte Nullzeichen an seinem Platz lassen.
  4. Den Puffer vergrößern und dann das Zeichen und das Nullzeichen schreiben.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Eine Schleife allokiert bei jeder von 750 Iterationen erfolgreich 48 Byte und verliert den einzigen Zeiger auf jeden Block, ohne ihn freizugeben. Wie viel Speicher geht verloren?
  1. 36.000 Byte
  2. 750 Byte
  3. 48.000 Byte
  4. 36.048 Byte
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Ein erfolgreich allokiertes Array enthält neun Elemente. Was ist sein größter gültiger Elementindex?
  1. 9
  2. 8
  3. 10
  4. 7
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Ein Programm stürzt innerhalb von free ab, nachdem eine Schleife ein Heap-Array gefüllt hat. Was sollte die Untersuchung prüfen, bevor der Freigabeaufruf geändert wird?
  1. Frühere Schreibzugriffe, die die Arraygrenzen überschritten haben könnten.
  2. Ob das Entfernen des gesamten Aufräumcodes das Programm beendet.
  3. Ob eine Ausgabe die gemeldete Freigabeadresse verändert.
  4. Ob die lokale Allokation desselben Arrays den Absturz verbirgt.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Die Anfangsadresse einer nicht freigegebenen Speicherallokation ist beim Beenden noch in einem globalen Zeiger gespeichert. Welche Valgrind-Speicherleckkategorie beschreibt sie?
  1. Definitiv verloren
  2. Indirekt verloren
  3. Noch erreichbar
  4. Möglicherweise verloren
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;
  1. Die Elemente 0 bis 3 vor der Verzweigung erneut initialisieren.
  2. Element 4 initialisieren, aber Element 5 unverändert lassen.
  3. Die Elemente 4 und 6 vor der Verzweigung initialisieren.
  4. Die Elemente 4 und 5 initialisieren, bevor ihre Werte verwendet werden.
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

Eine Hilfsfunktion hat einen festen lokalen Arbeitsbereich von 20 ints, der nur während des Aufrufs verwendet wird und dessen Größe nicht geändert werden muss. Welche Speicherwahl vermeidet einen unnötigen Besitzer und Aufräumpfad?
  1. Ein Heap-Block, dessen Adresse verloren geht.
  2. Ein zurückgegebener Zeiger auf ein lokales Array.
  3. Ein Heap-Block hinter einer besitzerlosen globalen Variablen.
  4. Ein gewöhnliches lokales Array aus 20 ints.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Auf einem System mit Zeigern zu 8 Byte und ints zu 4 Byte verwendet Code int *p = malloc(11 * sizeof p);. Wie viele Byte fordert er an, und was ist an dem Ausdruck falsch?
  1. 44 Byte; er bestimmt die Größe eines Elements statt des Zeigers.
  2. 88 Byte; er bestimmt die Größe des Zeigers statt eines Elements.
  3. 11 Byte; sizeof entfernt die Multiplikation vollständig.
  4. 88 Byte; er reserviert elf unabhängig besessene Blöcke.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Welche C-Zeichenkette enthält ein Zeichenpuffer unmittelbar nach einem erfolgreichen calloc mit positiver Größe?
  1. Eine leere Zeichenkette
  2. Eine Zeichenkette aus Leerzeichen
  3. Eine Zeichenkette aus „0“
  4. Eine unbekannte Zeichenkette
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Ein Block mit sechs ints wird über 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?
  1. Ja; das fehlgeschlagene Wachstum erhielt Auswahl und Lebensdauer.
  2. Ja; Index 4 anhand des letzten Zeigers auf drei ints neu berechnen.
  3. Nein; der alte Zeiger ist ungültig und Index 4 passt nicht mehr.
  4. Nein; das fehlgeschlagene Wachstum hatte den Block mit sechs ints bereits freigegeben.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein Array hat Länge 6 und Kapazität 8. Zwei Werte werden erfolgreich angehängt. Der Code speichert dann den besitzenden Zeiger in 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?
  1. data = old wiederherstellen, Länge 9 und Kapazität 16.
  2. data = old wiederherstellen, Länge 8 und Kapazität 8.
  3. data = NULL beibehalten, Länge 8 und Kapazität 8.
  4. data = old wiederherstellen, Länge 8 und Kapazität 16.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Eine Funktion hält ihren einzigen allokierten Block in buffer. Nach dessen letzter Verwendung führt sie buffer = NULL; free(buffer); aus. Was geschieht mit dem ursprünglichen Block?
  1. Er wird freigegeben, weil die Variable einst seine Adresse enthielt.
  2. Er wird freigegeben und durch die Zuweisung mit Nullbytes gefüllt.
  3. Er bleibt allokiert und seine Adresse geht verloren.
  4. Er bleibt allokiert, ist aber noch über buffer erreichbar.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Nach einer geprüften Speicherallokation gibt eine Funktion ihren Block über p frei und ruft später free(alias) auf, wobei alias vor der Freigabe aus p kopiert wurde. Welcher Fehler ist das?
  1. Eine doppelte Speicherfreigabe über zwei Aliase
  2. Ein Speicherleck von zwei getrennten bestehenden Blöcken
  3. Ein gültiges Aufräumen von zwei Zeigern
  4. Ein Lesezugriff auf einen nicht initialisierten Wert in neuem Speicher
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine Diagnoseausgabe auf 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?
  1. Der fehlende Text beweist, dass der Ausgabeaufruf nie ausgeführt wurde.
  2. Die Ausgabe hat den Speicherfehler selbst zwangsläufig verhindert.
  3. Der fehlende Zeilenumbruch hat die Speicherzugriffsverletzung zwangsläufig verursacht.
  4. Der Text kann sich noch im Ausgabepuffer befunden haben.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Ein Entwickler hat program-asan mit AddressSanitizer kompiliert und möchte einen unabhängigen Memcheck-Lauf durchführen. Welche ausführbare Datei sollte Memcheck erhalten?
  1. Dieselbe ASan-Version mit ausgeschalteter Speicherleckerkennung.
  2. Eine separate Debug-Version ohne AddressSanitizer.
  3. Dieselbe ASan-Version ohne Frame-Pointer.
  4. Eine direkt übergebene Quelldatei statt einer ausführbaren Datei.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

ASan hält bei 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?
  1. Allokation prüfen, mit i < n durchlaufen und nach der Verwendung freigeben.
  2. n + 1 ints allokieren, die Grenze beibehalten und die Freigabe weglassen.
  3. Allokation prüfen, i <= n beibehalten und vor der Schleife freigeben.
  4. Größe und Grenze beibehalten, den Block auf null setzen und danach freigeben.
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

Welche Speicherdauer hat ein gewöhnliches globales Array mit Dateigültigkeitsbereich?
  1. Statische Speicherdauer
  2. Automatische Speicherdauer
  3. Allokierte Speicherdauer
  4. Eine neue Speicherdauer pro Aufruf
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Eine Bibliothek hat vier Ganzzahlzähler mit 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?
  1. Ihn direkt erhöhen, weil jedes Wachstum das Nullsetzen erhält.
  2. Alle sieben Zähler freigeben und neu erzeugen, um alte Werte zu erhalten.
  3. Ihm den vorgesehenen Anfangswert zuweisen, bevor er erhöht wird.
  4. Seine Adresse auf NULL setzen, bevor der größere Block verwendet wird.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Das äußere Array einer Tabelle enthält Zeiger auf separat besessene Zeilen. Ihr Ersteller hat die Zeilen 0 und 1 angelegt. Eine Lesefunktion leiht Zeile 0 bis zu ihrer Rückkehr. Das Erstellen von Zeile 2 schlägt fehl, während die Lesefunktion aktiv ist. Spätere Einträge wurden nie gesetzt. Welches Aufräumen im Fehlerfall erhält alle erforderlichen Lebensdauern?
  1. Zeilen 0 und 1 freigeben, die Lesefunktion beenden lassen, dann das äußere Array freigeben.
  2. Die Lesefunktion beenden lassen, das äußere Array freigeben, dann seine Zeilen lesen und freigeben.
  3. Die Lesefunktion beenden lassen, jede geplante Zeile und dann das äußere Array freigeben.
  4. Die Lesefunktion beenden lassen, Zeilen 0 und 1 und dann das äußere Array freigeben.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Ein allokierter Block mit vier ints wird über 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?
  1. Der zuerst angeforderte Block für sieben ints bleibt allokiert, ist aber verloren.
  2. Der ursprüngliche Block mit vier ints bleibt allokiert, ist aber verloren.
  3. Der danach angeforderte Block für neun ints bleibt allokiert, ist aber verloren.
  4. Kein Block bleibt allokiert, weil beide Größenänderungen fehlgeschlagen sind.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Die Kapazität eines dynamischen Arrays verdoppelt sich ausgehend von 5, sobald es voll ist. Es beginnt leer mit dieser Kapazität und erhält 18 Werte. Welche Kapazität und wie viele Elementkopien insgesamt ergeben sich höchstens durch das Wachstum, wenn bei jeder Vergrößerung der Block verschoben wird?
  1. Kapazität 20; höchstens 15 kopierte Elemente.
  2. Kapazität 20; höchstens 18 kopierte Elemente.
  3. Kapazität 18; höchstens 23 kopierte Elemente.
  4. Kapazität 40; höchstens 35 kopierte Elemente.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Eine Funktion allokiert ein Ergebnis, füllt es, gibt es frei und gibt dann den Zeiger an ihren Aufrufer zurück. Der Aufrufer muss dieses Ergebnis lesen. Welche Änderung des Besitzes passt?
  1. Das freigegebene Ergebnis zurückgeben und den Aufrufer auf einen Nullzeiger prüfen lassen.
  2. Den Zeiger vor der Rückkehr zurücksetzen und dieselbe Schnittstelle beibehalten.
  3. Das Ergebnis zweimal freigeben, damit kein ungültiger Block zurückbleibt.
  4. Das noch bestehende Ergebnis zurückgeben und den Aufrufer es später freigeben lassen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Code allokiert 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?
  1. Fünf Zeigerplätze allokieren und dann wie angegeben über Eintrag 0 kopieren.
  2. Eintrag 0 auf das Literal „east“ setzen und dieses Literal dann mit strcpy überschreiben.
  3. Fünf chars für Eintrag 0 allokieren, prüfen, dann kopieren und später freigeben.
  4. Eintrag 0 auf NULL setzen und dann fünf chars über diesen Eintrag kopieren.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Eine erfolgreiche Größenänderung verkleinert einen Block von neun initialisierten ints auf vier. Welcher Inhalt bleibt garantiert erhalten?
  1. Die ursprünglichen Werte an den Indizes 5 bis 8.
  2. Die ursprünglichen Werte an den Indizes 0 bis 3.
  3. Alle neun Werte, solange keine neue Speicherallokation erfolgt.
  4. Vier neue Nullwerte, die den ursprünglichen Anfangsteil ersetzen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Ein Test enthält einen Überlauf um ein Element bei einem lokalen Array, der innerhalb von ansonsten gültigem Stack-Speicher bleibt. Ein Memcheck-Lauf übersieht ihn. Welche nächste Prüfung passt am besten zu diesem Befund?
  1. Memcheck mit angezeigten Speicherleckkategorien wiederholen.
  2. Mit ASan neu erstellen und dieselbe Eingabe erneut ausführen.
  3. Den Zugriff nach einem fehlerfreien Lauf als gültig behandeln.
  4. Das Array durch einen größeren ungeprüften Heap-Block ersetzen.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Ein Prüfwerkzeug meldet eine doppelte Speicherfreigabe. Das Entfernen beider Freigabeaufrufe verhindert den Absturz, lässt aber eine Speicherallokation unfreigegeben. Welche Korrektur sollte diese Änderung ersetzen?
  1. Keine Freigabe beibehalten und beim Beenden auf das Betriebssystem vertrauen.
  2. Beide Freigaben beibehalten, aber einen geliehenen Alias auf null setzen.
  3. Beide Freigaben vor die letzte Verwendung verschieben, um die Lebensdauer zu verkürzen.
  4. Eine Freigabe nach der letzten Verwendung unter einem Besitzer beibehalten.
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

Eine Funktion gibt die Adresse eines lokalen Zeichenarrays zurück. Der Aufrufer gibt manchmal den erwarteten Text aus und stürzt manchmal ab. Was erklärt beide Ergebnisse?
  1. Das lokale Array wurde statisch, weil seine Adresse weitergegeben wurde.
  2. Die Rückgabe übertrug den Besitz einer gültigen Heap-Allokation.
  3. Der erwartete Text beweist, dass das Array bei diesem Lauf existierte.
  4. Die Lebensdauer des Arrays endete bei der Rückkehr; der Zugriff ist ungültig.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 2 von 10

Was misst sizeof samples bei einem gewöhnlichen double *samples?
  1. Die Größe eines double, auf das der Zeiger zeigt.
  2. Die Bytezahl der gesamten Speicherallokation.
  3. Die Größe der Zeigervariablen.
  4. Die Anzahl initialisierter Arrayelemente.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 3 von 10

Eine Funktion gibt eine neu allokierte Zeichenkette zurück. Der Aufrufer übergibt sie an eine Ausgabefunktion, die sie nur ausleiht; die Ausgabefunktion gibt sie irrtümlich frei. Danach liest der Aufrufer sie und gibt sie frei. Welche Änderung des Besitzes korrigiert den gesamten Pfad?
  1. Die Freigabe des Aufrufers entfernen; seinen Lesezugriff danach beibehalten.
  2. Die Freigabe der Ausgabefunktion entfernen; die letzte Freigabe des Aufrufers beibehalten.
  3. Beide Freigaben beibehalten; nur den lokalen Zeiger der Ausgabefunktion auf null setzen.
  4. Die Freigabe des Aufrufers vor die Ausgabefunktion verschieben, um den Besitz früher zu beenden.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 4 von 10

Memcheck meldet, dass ein bedingter Sprung von einem nicht initialisierten Wert abhängt. Welche Option hilft dabei, den Ursprung dieses Werts zu finden?
  1. --track-origins=yes
  2. --leak-check=no
  3. --show-leak-kinds=all
  4. --error-limit=no
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 5 von 10

Ein Textpuffer beginnt mit Kapazität 6, Länge 4 und einem abschließenden Nullzeichen an Index 4. Er hängt 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?
  1. Länge 4, Kapazität 6; sowohl X als auch Y werden verworfen.
  2. Länge 5, Kapazität 6; X bleibt erhalten und das Nullzeichen liegt an Index 5.
  3. Länge 6, Kapazität 6; Y ersetzt das Nullzeichen an Index 5.
  4. Länge 5, Kapazität 12; X bleibt erhalten und das Nullzeichen liegt an Index 5.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 6 von 10

Ein Dienst bearbeitet drei Aufträge. Jeder Auftrag allokiert erfolgreich 24 Byte in die lokale Variable 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?
  1. 192 Byte; beide Zeigervariablen nach ihrer Rückkehr freigeben.
  2. 72 Byte; auf dem Fehlerpfad a nicht mehr freigeben.
  3. 40 Byte; nach der Schleife nur b des letzten Auftrags freigeben.
  4. 120 Byte; jeden Block von b vor dem Zurücksetzen seines Zeigers freigeben.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 7 von 10

Für ein Array existiert ein geliehener Zeiger auf Element 2. Code vergrößert das Array erfolgreich über einen temporären Zeiger und aktualisiert den besitzenden Zeiger. Danach schreibt er über den alten geliehenen Zeiger. Die Adresse hat sich nicht geändert. Welche Regel macht diesen Zugriff trotzdem ungültig?
  1. Jede Größenänderung verwirft den ursprünglichen Anfangsinhalt.
  2. Der besitzende und der geliehene Zeiger müssen verschiedene Adressen haben.
  3. Die erfolgreiche Größenänderung beendete die Lebensdauer des alten Objekts.
  4. Beim Vergrößern bleiben nur über Index null erreichte Werte erhalten.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 8 von 10

Ints belegen 4 Byte und Zeiger 8 Byte. Code verwendet 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?
  1. Die letzten sechs Lesezugriffe liegen innerhalb der Grenzen, lesen aber nicht initialisierte Werte.
  2. Die letzten sechs Lesezugriffe liegen außerhalb einer 24 Byte großen Speicherallokation.
  3. Alle zwölf Lesezugriffe sind gültig, weil die Speicherallokation sie initialisiert.
  4. Nur der letzte Lesezugriff überschreitet das Ende der zu großen Speicherallokation.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 9 von 10

Ein Programm hält mit einer Speicherzugriffsverletzung in einer Zeile an, die p[index] liest. Was sollte bei diesem Zugriff geprüft werden?
  1. Der Ursprung des Zeigers, die Lebensdauer des Zielobjekts und der Indexbereich.
  2. Nur der Index, weil jeder Zeiger, der nicht null ist, auf ein bestehendes Objekt zeigt.
  3. Nur die Freigabezeile, weil alle Speicherzugriffsverletzungen doppelte Freigaben sind.
  4. Nur die Ausgabepufferung, weil ein Array-Lesezugriff keinen Fehler auslösen kann.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Frage 10 von 10

Ein Prüfbericht zeigt einen Lesezugriff in 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?
  1. Die Größe in load erhöhen und die Aufrufreihenfolge unverändert lassen.
  2. Nur den lokalen Zeiger von reset zurücksetzen und den späteren Lesezugriff beibehalten.
  3. Das Ergebnis bis nach display bestehen lassen und es dann freigeben.
  4. reset ignorieren, weil der gemeldete Fehler in display auftrat.
Antwort zeigenAntwort ausblenden

Speichere diese Einheit, um diese Antwort zu sehen.

Hängst du bei deiner eigenen Aufgabe fest? Hausaufgabenhilfe

Schlüsselbegriffe

63 Schlüsselbegriffe, jeweils mit Definition

A

abschließendes Nullzeichen
Ein abschließendes Nullzeichen ist das Zeichen mit dem Wert null, das das Ende einer C-Zeichenkette markiert und ein Byte hinter ihren sichtbaren Zeichen belegt.
Die vier Zeichen lange Zeichenkette „west“ benötigt beim Kopieren zusammen mit ihrem abschließenden Nullzeichen fünf Byte.
AddressSanitizer
AddressSanitizer ist eine vom Compiler hinzugefügte Instrumentierung zur Erkennung ungültiger Speicherzugriffe und Freigaben während der Ausführung einer instrumentierten Datei.
Eine ASan-Version kann einen Überlauf eines lokalen Arrays erkennen, der innerhalb ansonsten gültigen Stack-Speichers bleibt.
Alias
Ein Alias ist ein weiterer Zeiger auf dasselbe Objekt. Er teilt dessen Lebensdauer, wird aber nicht aktualisiert, wenn sich eine andere Zeigervariable ändert.
Wird der besitzende Zeiger nach der Freigabe auf NULL gesetzt, bleiben gespeicherte Aliase für spätere Lesezugriffe unbrauchbar.
Allokationsstelle
Eine Allokationsstelle ist die Stelle im Programm, die einen Block anfordert. Prüfberichte zeigen sie an, um den Ursprung dieses Speichers festzuhalten.
Die Allokationsstelle eines Speicherleckberichts kann innerhalb einer Hilfsfunktion liegen, während die korrekte Freigabe in ihren Aufrufer gehört.
Array variabler Länge
Ein Array variabler Länge ist ein Array, dessen Größe während der Ausführung festgelegt wird und das eine automatische Lebensdauer hat; es belegt gewöhnlich Stack-Speicher.
Die Deklaration 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
Ein Aufräumpfad ist Code, der erhaltene Ressourcen vor dem Verlassen einer Funktion freigibt, auch wenn die Arbeit nach teilweisem Erfolg abgebrochen wird.
Schlägt eine zweite Speicherallokation fehl, gibt der Fehlerpfad den ersten Block frei, bevor er den Fehler zurückgibt.
Aufrufrückverfolgung
Eine Aufrufrückverfolgung listet die bei einem Absturz oder Fehler aktiven Funktionsaufrufe auf und hilft, den relevanten Zugriff und den Pfad dorthin zu finden.
Beginne beim ersten relevanten Stackframe in deinem eigenen Code und untersuche dann, wie dessen Zeiger seinen Wert erhalten hat.
Aufrufstapel
Der Aufrufstapel ist der Bereich, der üblicherweise verwendet wird, um aktive Funktionsaufrufe, ihre lokalen Objekte, Parameter und Rückkehrinformationen zu verwalten.
Eine nicht optimierte rekursive Funktion kann immer weitere Stackframes hinzufügen, bis sie den verfügbaren Stack-Speicher erschöpft.
Ausgabepufferung
Die Ausgabepufferung hält ausgegebenen Text vor dem Schreiben vorübergehend im Speicher, sodass ein Absturz verhindern kann, dass kürzlich ausgegebene Fortschrittsmeldungen erscheinen.
Ein Aufruf von fflush(stdout) nach einer Fortschrittsausgabe stellt ihren gepufferten Text vor der nächsten Operation bereit.
automatischer Speicher
Automatischer Speicher gehört zu gewöhnlichen lokalen Objekten und Parametern, deren Lebensdauer endet, wenn die Ausführung ihren Block verlässt.
Die Lebensdauer des lokalen Arrays einer Hilfsfunktion endet, wenn die Hilfsfunktion zurückkehrt, selbst wenn der Aufrufer die Adresse des Arrays gespeichert hat.

B

Besitz
Besitz ist die Verantwortung, einen allokierten Block nach seiner letzten Verwendung freizugeben oder diese Pflicht gezielt an einen anderen Programmteil zu übertragen.
Eine Funktion, die eine Kopie erzeugt, überträgt den Besitz an ihren Aufrufer, wenn sie ein neu allokiertes Ergebnis zurückgibt.

C

calloc
Die Funktion 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.
Ganzzahlzähler aus einer geprüften calloc-Anforderung beginnen vor der ersten Erhöhung bei null.

D

definitiv verloren
Eine Speicherallokation ist in Memcheck definitiv verloren, wenn kein Zeiger mehr vorhanden ist und ihr reservierter Speicher daher über keine gespeicherte Adresse freigegeben werden kann.
Eine Hilfsfunktion, die ihren einzigen besitzenden Zeiger zurücklässt, kann im Bericht beim Beenden einen definitiv verlorenen Block hinterlassen.
doppelte Speicherfreigabe
Eine doppelte Speicherfreigabe versucht, einen Block freizugeben, dessen Lebensdauer durch eine frühere Freigabe bereits beendet wurde, auch über einen anderen Alias.
Ein Besitzer und ein Nutzer eines geliehenen Zeigers können nicht beide free für denselben zuvor bestehenden Block aufrufen.
dynamisches Array
Ein dynamisches Array ist allokierter Elementspeicher, der mit einer Länge und einer Kapazität verwaltet wird, sodass er beim Eintreffen weiterer Werte wachsen kann.
Ein dynamisches Array kann zehn Werte auf sechzehn Plätzen enthalten und so freie Kapazität für späteres Anhängen lassen.

E

erster gemeldeter Fehler
Der erste gemeldete Fehler ist der früheste Befund eines Prüfwerkzeugs in einem Lauf und oft der beste Ausgangspunkt, weil spätere Fehler aus seinem Schaden folgen können.
Behebe den ersten Überlauf, bevor du jeden späteren Allokatorfehler im selben Bericht untersuchst.

F

fehlerauslösende Eingabe
Eine fehlerauslösende Eingabe ist ein Testfall, der das beobachtete Problem zuverlässig erreicht und damit einen konkreten Ausgangspunkt für Fehlersuche und Überprüfung bietet.
Verkleinere eine fehlerhafte Eingabe so, dass ihr Fehler erhalten bleibt und der erste relevante Bericht leichter zu deuten ist.
free
Die Funktion free gibt einen noch bestehenden allokierten Block zur Wiederverwendung frei, wenn ihr dessen Anfangsadresse übergeben wird, und bewirkt nichts, wenn ihr NULL übergeben wird.
Der Aufrufer gibt eine zurückgegebene Zeichenkettenkopie erst frei, nachdem alle Nutzer geliehener Zeiger sie fertig gelesen haben.

G

geliehener Zeiger
Ein geliehener Zeiger gibt Zugriff auf ein noch bestehendes Objekt, während ein anderer Programmteil dafür verantwortlich bleibt, über dessen Freigabe zu entscheiden.
Eine Ausgabefunktion kann während ihres Aufrufs eine Zeichenkette ausleihen, ohne die Erlaubnis zu erhalten, den Block des Aufrufers freizugeben.
gemeinsam genutzter statischer Puffer
Ein gemeinsam genutzter statischer Puffer ist der dauerhaft bestehende Ergebnisspeicher einer Funktion, den ihre Aufrufe wiederverwenden, sodass ein späterer Aufruf ein früheres Ergebnis überschreiben kann.
Zwei von aufeinanderfolgenden Aufrufen zurückgegebene Zeiger können auf dasselbe statische Array statt auf unabhängige Texte verweisen.
geometrisches Wachstum
Geometrisches Wachstum erhöht die Kapazität um einen multiplikativen Faktor, etwa durch Verdoppeln, um wiederholtes Kopieren über viele Anhängevorgänge hinweg zu begrenzen.
Ein volles Array mit Kapazität vier wächst auf acht und dann auf sechzehn, wenn seine nächste Wachstumsgrenze erreicht wird.

H

hängender Zeiger
Ein hängender Zeiger verweist auf ein Objekt, dessen Lebensdauer beendet ist, sodass für einen späteren Zugriff über diesen Zeiger kein gültiges altes Ziel mehr besteht.
Ein Zeiger auf das ehemalige lokale Array einer Hilfsfunktion ist unmittelbar nach deren Rückkehr ein hängender Zeiger.
Heap
Der Heap ist der Speicherbereich, aus dem der Allokator Blöcke bereitstellt, deren Lebensdauer durch Allokation und Freigabe gesteuert wird.
Wird die Adresse eines allokierten Arrays zurückgegeben, bleibt der Block auf dem Heap, damit der Aufrufer ihn verwenden und freigeben kann.
Heap-Pufferüberlauf
Ein Heap-Pufferüberlauf liest oder schreibt vor oder hinter den Grenzen eines allokierten Blocks, selbst wenn die umgebende Adresse zufällig zugänglich ist.
Eine Schleife, die bei einer Speicherallokation für zehn Elemente Index zehn erreicht, überschreitet den gültigen Elementbereich des Blocks.
Heap-Übersicht
Eine Heap-Übersicht berichtet über Allokationsvorgänge und beim Beenden verbleibenden Heap-Speicher und unterstützt so die Untersuchung nicht freigegebener Blöcke.
Lies die Gesamtzahlen der verbleibenden Blöcke zusammen mit den Speicherleckkategorien, statt jeden Block als verloren zu behandeln.

I

indirekt verloren
Eine Speicherallokation ist laut Memcheck indirekt verloren, wenn sie nur über einen anderen Block erreichbar ist, der selbst verloren gegangen ist.
Geht ein äußeres Array mit den Adressen besessener Zeilen verloren, können auch die allokierten Zeilen indirekt verloren sein.
Initialisierung
Die Initialisierung legt einen Wert fest, bevor er gelesen wird, durch eine erste Zuweisung oder eine geeignete Speicherallokation mit Nullen.
Ein Zähler muss einen definierten Anfangswert haben, weil das Erhöhen zuerst seinen vorherigen Wert liest.
Instrumentierung
Instrumentierung fügt beim Erstellen Prüfcode hinzu, damit das entstehende Programm ausgewählte Fehler erkennen kann, während seine getesteten Pfade ausgeführt werden.
Das Hinzufügen von -fsanitize=address beim Kompilieren und Linken erzeugt eine mit ASan instrumentierte ausführbare Datei.

K

Kapazität
Die Kapazität eines Arrays ist die Anzahl der in seinem Block reservierten Elementplätze, einschließlich ungenutzter Plätze, die noch nicht zu seinem logischen Inhalt gehören.
Eine fehlgeschlagene Wachstumsanforderung muss die Kapazität unverändert lassen, weil keine zusätzlichen Plätze erhalten wurden.

L

Länge
Die Länge eines Arrays ist die Anzahl seiner initialisierten, aktuell belegten Elemente. Sie bestimmt den Bereich für Berechnungen über seine gespeicherten Werte.
Nach dem Anhängen eines fünften Werts in verfügbaren Speicher wird die Länge fünf, selbst wenn die Kapazität acht bleibt.
LeakSanitizer
LeakSanitizer prüft beim Beenden auf verlorene Speicherallokationen und kann ASan ergänzen; Verfügbarkeit und Standardeinstellungen hängen von der Plattform ab.
Linux-ASan-Versionen aktivieren normalerweise Speicherleckprüfungen; manche macOS-Werkzeugketten können sie nicht bereitstellen.
Lesezugriff auf einen nicht initialisierten Wert
Ein Lesezugriff auf einen nicht initialisierten Wert verwendet einen Wert, der nie gesetzt wurde, etwa ein Element innerhalb der Grenzen von frisch allokiertem oder neu hinzugefügtem Speicher.
Werden drei allokierte ints summiert, nachdem nur zweien ein Wert zugewiesen wurde, wird weiterhin ein nicht gesetzter dritter Wert gelesen.

M

malloc
Die Funktion malloc reserviert eine angeforderte Anzahl von Bytes und gibt eine Blockadresse oder einen Nullzeiger zurück, ohne die gespeicherten Werte zu initialisieren.
Ein mit malloc erhaltenes Ganzzahlarray benötigt Zuweisungen, bevor seine Elemente addiert oder verglichen werden.
Memcheck
Memcheck ist Valgrinds Standardwerkzeug zur Speicherprüfung. Es verfolgt während eines Laufs ungültige Zugriffe, Lebensdauern von Speicherallokationen, nicht gesetzte Werte und nicht freigegebene Blöcke.
Führe eine gewöhnliche ausführbare Debug-Datei unter Memcheck aus, um neben den Speichervorgeschichten Quellcodezeilen zu erhalten.
MemorySanitizer
MemorySanitizer ist Clangs Prüfwerkzeug für die Verwendung nicht initialisierter Werte auf unterstützten Systemen und behandelt damit eine Fehlerart, die gewöhnliches ASan nicht verfolgt.
Verwende es in einer separaten unterstützten Programmversion, statt seine Instrumentierung mit AddressSanitizer zu kombinieren.
mit Nullen gefüllter Speicher
Bei mit Nullen gefülltem Speicher ist jedes Bit auf null gesetzt. Dies ergibt den Ganzzahlwert null, garantiert aber portabel weder Gleitkomma-Nullwerte noch Nullzeiger.
Ein Zeichenpuffer positiver Größe aus calloc beginnt mit einem Nullzeichen und enthält eine leere Zeichenkette.
möglicherweise verloren
Eine Speicherallokation ist in Memcheck möglicherweise verloren, wenn nur ein Zeiger ins Objektinnere gefunden wurde und daher unklar ist, was mit der Anfangsadresse geschehen ist.
Untersuche, ob ein wandernder Zeiger den besitzenden Zeiger ersetzt hat, statt einen separaten Zeiger auf den Anfang zu erhalten.

N

noch erreichbar
Eine Speicherallokation ist in Valgrind noch erreichbar, wenn beim Beenden ein Zeiger vorhanden ist, obwohl das Programm den Block nicht freigegeben hat.
Ein globaler Zeiger, der die Anfangsadresse einer nicht freigegebenen Speicherallokation behält, macht den Block für die Prüfung beim Beenden erreichbar.
Nullzeiger
Ein Nullzeiger ist ein Zeigerwert, der kein Objekt bezeichnet. Speicherallokationsfunktionen geben ihn zurück, wenn sie einen angeforderten Block ungleich null nicht bereitstellen können.
Eine Prüfung auf einen Nullzeiger leitet die Ausführung auf einen Fehlerpfad, bevor der Code versucht, auf eine fehlgeschlagene Speicherallokation zuzugreifen.

O

Objektlebensdauer
Die Lebensdauer eines Objekts ist der Zeitraum, in dem es in reserviertem Speicher existiert und über gültige Zeiger darauf zugegriffen werden kann.
Ein erfolgreiches realloc beendet die Lebensdauer des alten Objekts, selbst wenn die zurückgegebene Adresse unverändert aussieht.
Off-by-one-Fehler
Ein Off-by-one-Fehler setzt eine Grenze um eine Position zu hoch oder zu niedrig und schließt häufig die Elementanzahl als ungültigen letzten Index ein.
Die Änderung von i <= n zu i < n lässt einen bei null beginnenden Durchlauf genau die n vorgesehenen Elemente besuchen.

P

Prüflauf
Ein Prüflauf führt das korrigierte Programm mit einem Prüfwerkzeug aus, um den getesteten Pfad zu bestätigen. Danach folgen Eingaben, die andere Pfade durchlaufen.
Teste nach einem fehlerfreien reproduzierenden Beispiel eine leere Eingabe, Wachstumsgrenzen und vorzeitige Ausstiege auf fehlendes Aufräumen.

R

realloc
Die Funktion realloc ersetzt einen allokierten Block durch ein Objekt der angeforderten Größe, erhält den passenden Anfangsteil und ändert möglicherweise dessen Adresse.
Wird ein Block mit vier ints auf sechs vergrößert, bleiben die ersten vier Werte erhalten, aber zwei neue Elemente sind nicht gesetzt.

S

size_t
Der Typ size_t ist der vorzeichenlose Ganzzahltyp für Objektgrößen und Bytezahlen bei Speicherallokationen, einschließlich des Ergebnisses von sizeof.
Ein dynamisches Array verwendet size_t-Variablen für Länge und Kapazität, damit diese Objektgrößen beschreiben können.
sizeof
Der Operator 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.
Die Anforderung n * sizeof *p richtet sich nach dem Elementtyp, während sizeof p die Länge des Arrays nicht ermitteln kann.
Speicherallokation
Eine Speicherallokation ist die Anforderung eines reservierten Speicherblocks; bei Erfolg liefert die Anforderung dessen Anfangsadresse für die spätere Verwendung und Freigabe.
Ein Programm prüft, ob seine Speicherallokation erfolgreich war, bevor es das erste Element des angeforderten Arrays speichert.
Speicherdauer
Die Speicherdauer eines Objekts bestimmt, wie lange sein Speicher reserviert ist, unter anderem ob dies mit einem Block, einer Freigabe oder dem Programmende endet.
Ein lokaler Zeiger und sein allokiertes Array können unterschiedliche Speicherdauern haben, obwohl sie dieselbe Adresse teilen.
Speicherleck
Ein Speicherleck ist eine Speicherallokation, die nicht mehr benötigt wird, aber unfreigegeben bleibt, oft weil das Programm seinen letzten Zeiger darauf verloren hat.
Wird der einzige besitzende Zeiger mit einer neuen Speicherallokation überschrieben, kann der alte Block dauerhaft zurückgelassen werden.
Speicherzugriffsverletzung
Eine Speicherzugriffsverletzung ist ein Fehler des Betriebssystems infolge eines unerlaubten Speicherzugriffs, etwa der häufigen Dereferenzierung eines Nullzeigers oder eines Schreibzugriffs auf schreibgeschützten Speicher.
Ein fehlerhafter Schreibzugriff kann zuerst Daten beschädigen und später in einer anderen Funktion eine Speicherzugriffsverletzung verursachen.
Stapelüberlauf
Ein Stapelüberlauf erschöpft den für aktive Aufrufe und automatische Objekte verfügbaren Speicherplatz, häufig durch tiefe Rekursion oder ein riesiges lokales Array.
Eine nicht optimierte rekursive Funktion ohne Abbruchbedingung kann immer weitere Stackframes hinzufügen, bis sie fehlschlägt.
statischer Speicher
Statischer Speicher besteht während der Programmausführung und wird für globale Objekte und mit static deklarierte lokale Variablen verwendet.
Ein statischer Zähler innerhalb einer Funktion behält seinen aktualisierten Wert, wenn der nächste Aufruf beginnt.

T

temporärer Zeiger
Ein temporärer Zeiger nimmt das Ergebnis einer Größenänderung auf, bevor der besitzende Zeiger aktualisiert wird, und erhält so die Adresse des alten Blocks, falls die Anforderung fehlschlägt.
Bei 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
Ein Überlauf bei der Größenberechnung tritt auf, wenn eine Berechnung der Bytezahl aus einer Anzahl den Wertebereich von size_t überschreitet und durch Wraparound einen kleineren darstellbaren Wert ergibt.
Prüfe die Elementanzahl gegen SIZE_MAX / sizeof *p, bevor du sie mit der Elementgröße multiplizierst.

U

UndefinedBehaviorSanitizer
UndefinedBehaviorSanitizer fügt Prüfungen für ausgewähltes undefiniertes Verhalten hinzu, das keine Adressen betrifft, etwa Überläufe bei vorzeichenbehafteten Ganzzahlen, und kann ASan ergänzen.
Eine Entwicklungsversion kann -fsanitize=address,undefined verwenden, um die beiden kompatiblen Prüfungssammlungen zu aktivieren.
undefiniertes Verhalten
Undefiniertes Verhalten liegt vor, wenn C nach einer Regelverletzung keine Anforderungen an das Programmergebnis stellt und dadurch Abstürze oder unbemerkte Beschädigungen möglich sind.
Ein Schreibzugriff außerhalb der Speichergrenzen kann in einem Test harmlos erscheinen und bei einer anderen Eingabe einen anderen Wert beschädigen.
Ursprungsverfolgung
Die Ursprungsverfolgung verfolgt einen nicht initialisierten Wert zu dem Speicher zurück, aus dem er stammt, und hilft, die fehlende Wertzuweisung zu finden.
Memchecks --track-origins=yes kann erklären, warum eine spätere Verzweigung von nicht gesetzten Heap-Daten abhängt.

V

verlorener Speicherblock
Ein verlorener Speicherblock ist ein weiterhin reservierter Block, dessen nutzbare Adresse nicht mehr gespeichert ist, sodass das Programm ihn nicht auf gewöhnlichem Weg freigeben kann.
Wird der einzige besitzende Zeiger auf NULL gesetzt, ohne vorher free aufzurufen, geht die Adresse des allokierten Blocks verloren.
verschachtelte Speicherallokation
Eine verschachtelte Speicherallokation ist eine Lösung mit separat allokierten inneren Blöcken, deren Adressen in einem äußeren allokierten Behälter gespeichert werden.
Bei einer Tabelle mit separaten Zeilen müssen diese freigegeben werden, bevor das äußere Zeigerarray freigegeben wird.
vom Aufrufer bereitgestellter Puffer
Ein vom Aufrufer bereitgestellter Puffer ist Speicher, den der Aufrufer einer Funktion für ihre Ausgabe zur Verfügung stellt und dessen Größe und Lebensdauer er weiterhin steuert.
Ein Aufrufer kann sein eigenes 16 Byte großes Array an eine Hilfsfunktion übergeben, die es mit einer kurzen Begrüßung füllt.

W

Wiederverwendung von Puffern
Bei der Wiederverwendung von Puffern wird ein geeigneter allokierter Block über wiederholte Operationen hinweg behalten und sein Inhalt ersetzt, bevor der Block schließlich freigegeben wird.
Eine Schleife kann einen 100 Byte großen Arbeitsbereich wiederverwenden, statt bei jedem Durchlauf einen Block zu allokieren und zurückzulassen.

Z

Zeichenkettenliteral
Ein Zeichenkettenliteral ist in Anführungszeichen geschriebener Text im C-Quellcode. Es hat statische Speicherdauer und darf nicht über einen Zeiger verändert werden.
Verwende char word[] = "hello";, wenn eine lokale beschreibbare Kopie statt eines Zeigers auf das Literal benötigt wird.
Zeiger ins Objektinnere
Ein Zeiger ins Objektinnere adressiert eine Position innerhalb eines Objekts, etwa ein Arrayelement, statt der Anfangsadresse seines allokierten Speicherblocks.
Speichere über eine erfolgreiche Größenänderung hinweg einen Elementindex und berechne den Zeiger ins Objektinnere anhand des Ergebnisses neu.
Zugriff nach der Speicherfreigabe
Ein Zugriff nach der Speicherfreigabe ist die Verwendung eines allokierten Objekts nach dessen Freigabe. Sie kann scheinbar alte Daten liefern, Speicher beschädigen oder einen Absturz verursachen.
Die Ausgabe einer freigegebenen Zeichenkette über einen gespeicherten Alias ist ungültig, selbst wenn die erwarteten Wörter erscheinen.

Über diese Einheit

Von StudeliFehler melden

Erstelle deine eigenen Lernmaterialien

Mach aus deinen Notizen, Folien und Dokumenten Lernleitfäden, Übungsfragen, Karteikarten und 6 weitere Tools.

Jetzt starten