C++-Programmierung/ Speicherverwaltung/ new und delete
C++ bietet die Möglichkeit der sogenannten Freispeicherverwaltung. Das heißt, Sie können zu jedem Zeitpunkt eine beliebige Menge an Speicher anfordern (vom Betriebssystem). Allerdings müssen Sie diesen Speicher später auch selbst wieder frei geben (ans Betriebssystem zurückgeben), was Gefahren nach sich zieht: Sie können vergessen, nicht mehr benötigten Speicher zurückzugeben - das Betriebssystem kann ohne einen expliziten Hinweis (also das Freigeben des Speichers durch Ihr Programm) nicht wissen, ob der Speicher noch benötigt wird. Das unnötige Vorhandensein nicht mehr benötigten Speichers bezeichnet man als Speicherleck (engl. „Memory Leak“) oder auch Speicherleiche. Umgekehrt wird auch das versehentliche Freigeben von eigentlich noch benötigtem Speicher als Speicherleck bezeichnet (es führt zu "dangling pointers").
Andere Sprachen, wie etwa Java, rücken diesem Problem mit einer sogenannten „Garbage Collection“ (englisch für Müllabfuhr; in diesem Fall für „Automatische Speicherbereinigung“) zu Leibe. Dabei wird im Hintergrund periodisch nach vom Programm angeforderten Speicherbereichen gesucht, auf die keine Variable/Zeiger im Programm mehr verweist. C++ besitzt kein derartiges Werkzeug, was zwar zu einer höheren Performance führt, aber andererseits auch eine besondere Sorgfalt von Ihnen als Programmierer verlangt. Im Lauf dieses Kapitels werden Sie jedoch Hilfsmittel kennenlernen, die Sie tatkräftig bei der Vermeidung von Speicherlecks unterstützen.
Anforderung und Freigabe von Speicher
[Bearbeiten]Angefordert wird Speicher in C++ über den Operator new
. Im Gegensatz zur C-Variante wird in C++ immer Speicher für einen bestimmten Datentyp angefordert. Dieser darf allerdings nicht const
oder volatile
und auch keine Funktion sein. Der new
-Operator erhält als Argument also einen Datentyp, anhand dessen er die Menge des benötigten Speichers selbstständig bestimmt. An den Konstruktor des Datentyps können, wenn nötig, Argumente übergeben werden, denn new
gibt nicht nur einen Speicherbereich zurück, sondern legt in diesem Speicherbereich auch gleich noch ein Objekt des übergebenen Typs an. Zu beachten ist hierbei, dass für einen Aufruf ohne zusätzliche Konstruktorargumente keine leeren Klammern angegeben werden dürfen.
new
gibt bei erfolgreicher Durchführung einen Zeiger auf den entsprechenden Datentyp zurück. Diesen Zeiger müssen Sie dann (beispielsweise in einer Variable) speichern. Wenn Sie das Objekt nicht mehr benötigen, müssen Sie es mittels delete
-Operator wieder freigeben. delete
erwartet zum Löschen den Zeiger, den new
Ihnen geliefert hat. Analog zum Konstruktoraufruf bei new
ruft delete
zunächst den Destruktor des Objekts auf und gibt anschließend den Speicher an das Betriebssystem zurück.
Lassen Sie niemals eine Speicheradresse zweimal durch delete
löschen, denn dies führt fast immer zum Programmabsturz. Am einfachsten vermeiden Sie dies, indem Sie Ihre Zeigervariable nach dem Löschen auf 0 setzen, denn das Löschen eines Nullzeigers durch delete
ist gestattet.
Die Situation, dass ein Speicherbereich zweimalig freigegeben werden soll, ist zudem meist ein Hinweis auf eine inkonsistente Programmstruktur - etwas stimmt mit der Programmlogik nicht.
Im Folgenden sehen Sie noch mal ein kleines Beispiel, bei dem ein double
dynamisch angelegt wird. Dabei wird beim zweiten Anlegen ein Argument zur Initialisierung angegeben, was einem Konstruktorargument entspricht.
#include <iostream>
int main(){
double* zeiger = new double; // Anfordern eines doubles
std::cout << *zeiger << std::endl; // gibt den (zufälligen) Wert des doubles aus
delete zeiger; // Löschen der Variable
zeiger = 0; // Zeiger auf 0 setzen
delete zeiger; // ok, weil Zeiger 0 ist (nur zur Vorführung)
zeiger = new double(7.5); // Anfordern eines doubles, initialisieren mit 7.5
std::cout << *zeiger << std::endl; // gibt 7.5 aus
delete zeiger; // Löschen der Variable
}
Falls beim Anfordern des Speichers durch new
etwas schief geht, etwa weil nicht mehr genug Speicher zur Verfügung steht, wird eine std::bad_alloc
-Ausnahme geworfen. Sie können also nach einem new
-Aufruf davon ausgehen, dass dieser erfolgreich war, müssen aber mittels eines try
-Blocks eventuell geworfene Ausnahmen abfangen. Mehr Informationen hierzu finden Sie im Kapitel „Übersicht“ zur Ausnahmebehandlung.