Zum Inhalt springen

Dynamischer Speicher: new, delete & ihre Gefahren

Lerne, wie dynamische Allokation mit new und delete funktioniert, verstehe die Gefahren von Speicherlecks, doppeltem Freigeben und Zugriff nach dem Freigeben, und erfahre, warum moderner C++-Code kein einziges rohes new/delete enthalten sollte.

Dynamische Allokation mit new und delete

Der Stack ist schnell, aber in Größe und Gültigkeitsbereich begrenzt. Wenn du Objekte brauchst, die den erzeugenden Gültigkeitsbereich überleben, oder deren Größe zur Kompilierzeit unbekannt ist, allokierst du auf dem Heap (dem Free Store).

new T(args) allokiert Speicher für ein T, ruft dessen Konstruktor auf und gibt ein T* zurück. delete p ruft den Destruktor auf und gibt den Speicher frei. Für Arrays: new T[n] und delete[] p. new mit delete[] oder new[] mit delete zu mischen ist undefiniertes Verhalten — der Compiler wird das nicht bemerken.

new, delete und ihre Array-Varianten

Hier ist die rohe Allokations-API. Studiere sie, damit du sie in altem Code wiedererkennst, aber schreibe so nichts Neues — verwende stattdessen Smart Pointer.

new_delete.cpp
#include <iostream>
#include <cstring>

struct Widget {
    int id;
    Widget(int i) : id(i) { std::cout << "Widget " << id << " created\n"; }
    ~Widget() { std::cout << "Widget " << id << " destroyed\n"; }
};

int main() {
    // Single object
    Widget* w = new Widget(1);
    std::cout << "Using widget " << w->id << '\n';
    delete w;  // destructor called, memory freed

    // Array
    Widget* arr = new Widget[3]{{10}, {20}, {30}};
    for (int i = 0; i < 3; ++i)
        std::cout << arr[i].id << ' ';
    std::cout << '\n';
    delete[] arr;  // MUST use delete[] for new[]

    // UNDEFINED BEHAVIOR — don't do these:
    // delete arr;      // wrong: used delete instead of delete[]
    // delete[] w;      // wrong: used delete[] instead of delete
}

Placement new

Placement new konstruiert ein Objekt an einer bestimmten Speicheradresse, ohne zu allokieren. Es wird von Allokatoren, Speicherpools und intern von Containern wie std::vector genutzt. Du musst den Destruktor explizit aufrufen — delete darf auf Objekte aus Placement new NICHT angewendet werden.

placement_new.cpp
#include <iostream>
#include <new>       // required for placement new
#include <cstddef>   // std::byte
#include <memory>    // std::align

struct Sensor {
    int id;
    double reading;
    Sensor(int i, double r) : id(i), reading(r) {
        std::cout << "Sensor " << id << " init\n";
    }
    ~Sensor() { std::cout << "Sensor " << id << " shutdown\n"; }
};

int main() {
    // Pre-allocate raw storage (aligned)
    alignas(Sensor) std::byte buffer[sizeof(Sensor)];

    // Construct in pre-allocated memory
    Sensor* s = new (buffer) Sensor(42, 3.14);
    std::cout << "Reading: " << s->reading << '\n';

    // Must destroy explicitly — do NOT use delete
    s->~Sensor();  // explicit destructor call
    // delete s;    // WRONG — memory was not allocated by new
}

Die drei tödlichen Speicherfehler

Rohes new/delete setzt dich drei Fehlerkategorien aus:

1. Speicherleck — mit new allokieren, aber nie delete aufrufen. Der Speicher geht nie an das Betriebssystem zurück (bis der Prozess endet). In einem langlaufenden Server verbraucht das irgendwann den gesamten verfügbaren Arbeitsspeicher.

2. Doppeltes Freigeben — delete zweimal auf denselben Zeiger anwenden. Das zweite delete beschädigt die internen Datenstrukturen des Heap-Allokators, was oft erst viel später und in völlig unbeteiligtem Code zu Abstürzen führt. Das ist extrem schwer zu debuggen.

3. Zugriff nach dem Freigeben — Speicher nach delete noch verwenden. Der Speicher kann bereits für eine andere Allokation wiederverwendet worden sein, du liest oder schreibst also womöglich die Daten eines fremden Objekts. Das ist eine Sicherheitslücke (ein Fehler der CVE-Klasse).

Stolperfalle

Betrachte: f(new A(), new B()). Der Compiler darf die Argumente in beliebiger Reihenfolge auswerten und die Schritte verschachteln:

1. Speicher für A allokieren
2. Speicher für B allokieren
3. A konstruieren
4. B konstruieren

Wirft Schritt 4 eine Ausnahme, ist A geleckt — niemand ruft delete darauf auf. Sogar f(std::shared_ptr(new A()), std::shared_ptr(new B())) hatte dieses Problem vor C++17 (das garantiert, dass jedes Argument vollständig ausgewertet wird, bevor das nächste beginnt). Die Lösung: verwende immer std::make_unique oder std::make_shared — sie erledigen Allokation und Konstruktion als eine einzige, ausnahmesichere Operation.