Es gibt einen wichtigen Unterschied zwischen diesen beiden.
Alles, was nicht mit new
verhält sich ähnlich wie Werttypen in C# (und es wird oft gesagt, dass diese Objekte auf dem Stack zugewiesen werden, was wahrscheinlich der häufigste/offensichtlichste Fall ist, aber nicht immer stimmt). Genauer gesagt, Objekte, die ohne Verwendung von new
haben automatische Speicherdauer Alles zugeordnet mit new
wird auf dem Heap alloziert und ein Zeiger darauf zurückgegeben, genau wie Referenztypen in C#.
Alles, was auf dem Stack zugewiesen wird, muss eine konstante Größe haben, die zur Kompilierzeit bestimmt wird (der Compiler muss den Stack-Zeiger korrekt setzen, oder wenn das Objekt Mitglied einer anderen Klasse ist, muss er die Größe dieser anderen Klasse anpassen). Aus diesem Grund sind Arrays in C# Referenztypen. Das müssen sie auch sein, denn bei Referenztypen können wir zur Laufzeit entscheiden, wie viel Speicher wir anfordern wollen. Und das Gleiche gilt auch hier. Nur Arrays mit konstanter Größe (eine Größe, die zur Kompilierzeit bestimmt werden kann) können mit automatischer Speicherdauer (auf dem Stack) zugewiesen werden. Arrays mit dynamischer Größe müssen auf dem Heap zugewiesen werden, indem man new
.
(Und da hört jede Ähnlichkeit mit C# auf)
Jetzt hat alles, was auf dem Stapel zugewiesen wird, eine "automatische" Speicherdauer (Sie können eine Variable tatsächlich als auto
Dies ist jedoch die Standardeinstellung, wenn kein anderer Speichertyp angegeben ist, so dass das Schlüsselwort in der Praxis nicht wirklich verwendet wird, aber daher stammt es)
Automatische Speicherdauer bedeutet genau das, wonach es klingt: Die Dauer der Variablen wird automatisch behandelt. Im Gegensatz dazu muss alles, was auf dem Heap zugewiesen wird, von Ihnen manuell gelöscht werden. Hier ist ein Beispiel:
void foo() {
bar b;
bar* b2 = new bar();
}
Diese Funktion erzeugt drei Werte, die es zu berücksichtigen gilt:
In Zeile 1 wird eine Variable deklariert b
vom Typ bar
auf dem Stapel (automatische Dauer).
In Zeile 2 deklariert er eine bar
Zeiger b2
auf dem Stapel (automatische Dauer), et ruft new auf und weist eine bar
Objekt auf dem Heap. (dynamische Dauer)
Wenn die Funktion zurückkehrt, geschieht Folgendes: Erstens, b2
aus dem Rahmen fällt (die Reihenfolge der Zerstörung ist immer entgegengesetzt zur Reihenfolge des Aufbaus). Aber b2
ist nur ein Zeiger, es passiert also nichts, der Speicher, den er belegt, wird einfach freigegeben. Und wichtig ist, dass der Speicher, der zeigt auf (die bar
Instanz auf dem Heap) wird NICHT berührt. Nur der Zeiger wird freigegeben, da nur der Zeiger eine automatische Dauer hatte. Zweitens, b
den Anwendungsbereich verlässt, wird sein Destruktor aufgerufen und der Speicher freigegeben, da er eine automatische Dauer hat.
Und die bar
Instanz auf dem Heap? Sie ist wahrscheinlich noch da. Niemand hat sich die Mühe gemacht, sie zu löschen, also haben wir Speicher verloren.
Aus diesem Beispiel geht hervor, dass alles mit automatischer Dauer garantiert dass sein Destruktor aufgerufen wird, wenn er den Anwendungsbereich verlässt. Das ist nützlich. Aber alles, was auf dem Heap alloziert wird, hält so lange, wie wir es brauchen, und kann dynamisch dimensioniert werden, wie im Fall von Arrays. Auch das ist nützlich. Wir können dies nutzen, um unsere Speicherzuweisungen zu verwalten. Was wäre, wenn die Klasse Foo in ihrem Konstruktor Speicher auf dem Heap zuweisen und diesen Speicher in ihrem Destruktor wieder löschen würde? Dann hätten wir das Beste aus beiden Welten: sichere Speicherzuweisungen, die garantiert wieder freigegeben werden, aber ohne die Einschränkungen, die dadurch entstehen, dass alles auf dem Stack liegen muss.
Und genau so funktioniert der meiste C++-Code. Schauen Sie sich die Standardbibliothek von std::vector
zum Beispiel. Dieser wird normalerweise auf dem Stack zugewiesen, kann aber dynamisch in der Größe verändert werden. Dies geschieht durch die interne Zuweisung von Speicher auf dem Heap nach Bedarf. Der Benutzer der Klasse sieht dies nie, so dass es keine Möglichkeit gibt, Speicher zu verlieren oder zu vergessen, den zugewiesenen Speicher aufzuräumen.
Dieses Prinzip wird RAII (Resource Acquisition is Initialization) genannt und kann auf jede Ressource ausgedehnt werden, die erworben und freigegeben werden muss. (Netzwerksockel, Dateien, Datenbankverbindungen, Synchronisationssperren). Alle diese Ressourcen können im Konstruktor erworben und im Destruktor freigegeben werden, so dass gewährleistet ist, dass alle erworbenen Ressourcen wieder freigegeben werden.
Generell sollten Sie new/delete nie direkt in Ihrem High-Level-Code verwenden. Wickeln Sie es immer in eine Klasse ein, die den Speicher für Sie verwalten kann und die sicherstellt, dass er wieder freigegeben wird. (Ja, es kann Ausnahmen von dieser Regel geben. Insbesondere erfordern intelligente Zeiger den Aufruf von new
direkt, und übergeben Sie den Zeiger an seinen Konstruktor, der dann die Aufgabe übernimmt und dafür sorgt, dass delete
korrekt aufgerufen wird. Aber dies ist immer noch eine sehr wichtige Faustregel)