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.
#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.
#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).
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.
Eigene Allokatoren im Überblick
Standardmäßig nutzen new/delete den globalen Allokator (typischerweise malloc/free), der universell einsetzbar, aber nicht für jedes Muster optimal ist. Eigene Allokatoren bieten:
- Arena-/Linear-Allokatoren — einen Zeiger weiterschieben und am Ende alles auf einmal freigeben. Ideal für Spieldaten pro Frame.
- Pool-Allokatoren — Blöcke fester Größe im Voraus allokieren. O(1) beim Allokieren/Freigeben, keine Fragmentierung.
- Stack-Allokatoren — LIFO-Allokation, extrem schnell.
C++-Container nehmen Allokatoren als Template-Parameter entgegen: std::vector. Der Namensraum std::pmr (polymorphe Speicherressourcen) aus C++17 bietet ein zur Laufzeit polymorphes Allokator-Framework, das genau dieses Template-Parameter-Problem umgeht.
Im modernen C++ (ab C++14) sollte gut geschriebener Anwendungscode kein einziges rohes new und delete enthalten:
- Alleiniger Besitz: verwende std::make_unique
- Geteilter Besitz: verwende std::make_shared
- Dynamische Arrays: verwende std::vector
- Zeichenketten: verwende std::string
- Optionale Objekte: verwende std::optional
Rohes new/delete darf in Bibliothekscode auftauchen (Allokatoren, eigene Container), Anwendungscode sollte es jedoch nie brauchen. Wenn du im Code-Review ein new siehst, frage nach dem Warum — es gibt fast immer eine sicherere Alternative.
new/delete[]odernew[]/deletezu mischen ist undefiniertes Verhalten- Speicherlecks, doppeltes Freigeben und Zugriff nach dem Freigeben sind die drei tödlichen Speicherfehler
f(new A(), new B())kann lecken, wenn ein Konstruktor wirft (vor C++17 sogar mit shared_ptr-Wrappern)- Placement new konstruiert an einer vorgegebenen Adresse — zerstöre manuell, verwende nie
delete - Moderner C++-Anwendungscode sollte kein rohes
new/deleteenthalten — nutze Smart Pointer und Container
Quiz — Teste dein Wissen
(15 XP)1. Was passiert, wenn du `delete` auf Speicher anwendest, der mit `new[]` allokiert wurde?
2. Warum kann `f(new A(), new B())` Speicher lecken?
3. Wie zerstörst du ein Objekt, das mit Placement new erzeugt wurde?