Ja, einer liegt auf dem Stapel, der andere auf dem Haufen. Es gibt zwei wichtige Unterschiede:
- Erstens die offensichtliche und weniger wichtige: Die Heap-Zuweisungen sind langsam. Stapelzuordnungen sind schnell.
- Zweitens und viel wichtiger ist RAII . Da die vom Stapel zugewiesene Version automatisch bereinigt wird, ist dies hilfreich . Der Destruktor wird automatisch aufgerufen, sodass Sie sicherstellen können, dass alle von der Klasse zugewiesenen Ressourcen bereinigt werden. Dies ist wichtig, um Speicherverluste in C ++ zu vermeiden. Sie vermeiden sie, indem Sie sich niemals
delete
selbst aufrufen , sondern sie in delete
stapelzugewiesene Objekte einschließen, die intern aufgerufen werden, wie dies in ihrem Destruktor typisch ist. Wenn Sie versuchen, alle Zuordnungen manuell zu verfolgen und delete
zum richtigen Zeitpunkt anzurufen , garantiere ich Ihnen, dass pro 100 Codezeilen mindestens ein Speicherverlust auftritt.
Betrachten Sie als kleines Beispiel diesen Code:
class Pixel {
public:
Pixel(){ x=0; y=0;};
int x;
int y;
};
void foo() {
Pixel* p = new Pixel();
p->x = 2;
p->y = 5;
bar();
delete p;
}
Ziemlich unschuldiger Code, oder? Wir erstellen ein Pixel, rufen dann eine nicht verwandte Funktion auf und löschen das Pixel. Gibt es ein Speicherleck?
Und die Antwort ist "möglicherweise". Was passiert, wenn bar
eine Ausnahme ausgelöst wird? delete
wird nie aufgerufen, das Pixel wird nie gelöscht und wir verlieren Speicherplatz. Betrachten Sie nun Folgendes:
void foo() {
Pixel p;
p.x = 2;
p.y = 5;
bar();
}
Dadurch wird kein Speicher verloren. In diesem einfachen Fall befindet sich natürlich alles auf dem Stapel, sodass es automatisch bereinigt wird, aber selbst wenn die Pixel
Klasse intern eine dynamische Zuordnung vorgenommen hätte, würde dies auch nicht auslaufen. Die Pixel
Klasse würde einfach einen Destruktor erhalten, der sie löscht, und dieser Destruktor würde aufgerufen, egal wie wir die foo
Funktion verlassen . Auch wenn wir es verlassen, weil bar
eine Ausnahme geworfen hat. Das folgende, leicht erfundene Beispiel zeigt dies:
class Pixel {
public:
Pixel(){ x=new int(0); y=new int(0);};
int* x;
int* y;
~Pixel() {
delete x;
delete y;
}
};
void foo() {
Pixel p;
*p.x = 2;
*p.y = 5;
bar();
}
Die Pixel-Klasse weist jetzt intern etwas Heap-Speicher zu, aber ihr Destruktor kümmert sich um die Bereinigung, sodass wir uns bei der Verwendung der Klasse keine Sorgen machen müssen. (Ich sollte wahrscheinlich erwähnen, dass das letzte Beispiel hier stark vereinfacht ist, um das allgemeine Prinzip zu zeigen. Wenn wir diese Klasse tatsächlich verwenden, enthält sie auch mehrere mögliche Fehler. Wenn die Zuordnung von y fehlschlägt, wird x nie freigegeben Wenn das Pixel kopiert wird, versuchen beide Instanzen, dieselben Daten zu löschen. Nehmen Sie also das letzte Beispiel hier mit einem Körnchen Salz. Realer Code ist etwas kniffliger, zeigt aber die allgemeine Idee.
Natürlich kann dieselbe Technik auf andere Ressourcen als Speicherzuweisungen erweitert werden. Beispielsweise kann damit sichergestellt werden, dass Dateien oder Datenbankverbindungen nach der Verwendung geschlossen werden oder dass Synchronisierungssperren für Ihren Threading-Code freigegeben werden.