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.
Sie sind erst identisch, wenn Sie das Löschen hinzufügen.
Ihr Beispiel ist zu trivial, aber der Destruktor enthält möglicherweise tatsächlich Code, der echte Arbeit leistet. Dies wird als RAII bezeichnet.
Fügen Sie also das Löschen hinzu. Stellen Sie sicher, dass dies auch dann geschieht, wenn sich Ausnahmen verbreiten.
Pixel* p = NULL; // Must do this. Otherwise new may throw and then // you would be attempting to delete an invalid pointer. try { p = new Pixel(); p->x = 2; p->y = 5; // Do Work delete p; } catch(...) { delete p; throw; }
Wenn Sie etwas Interessanteres wie eine Datei ausgewählt haben (eine Ressource, die geschlossen werden muss). Dann machen Sie es richtig in Java mit Zeigern, die Sie dazu brauchen.
File file; try { file = new File("Plop"); // Do work with file. } finally { try { file.close(); // Make sure the file handle is closed. // Oherwise the resource will be leaked until // eventual Garbage collection. } catch(Exception e) {};// Need the extra try catch to catch and discard // Irrelevant exceptions. // Note it is bad practice to allow exceptions to escape a finally block. // If they do and there is already an exception propagating you loose the // the original exception, which probably has more relevant information // about the problem. }
Der gleiche Code in C ++
std::fstream file("Plop"); // Do work with file. // Destructor automatically closes file and discards irrelevant exceptions.
Obwohl die Leute die Geschwindigkeit erwähnen (wegen des Findens / Zuweisens von Speicher auf dem Heap). Persönlich ist dies für mich kein entscheidender Faktor (die Allokatoren sind sehr schnell und wurden für die C ++ - Verwendung kleiner Objekte optimiert, die ständig erstellt / zerstört werden).
Der Hauptgrund für mich ist die Lebensdauer des Objekts. Ein lokal definiertes Objekt hat eine sehr spezifische und genau definierte Lebensdauer, und der Destruktor wird garantiert am Ende aufgerufen (und kann daher spezifische Nebenwirkungen haben). Ein Zeiger hingegen steuert eine Ressource mit einer dynamischen Lebensdauer.
Der Hauptunterschied zwischen C ++ und Java ist:
Das Konzept, wem der Zeiger gehört. Es liegt in der Verantwortung des Eigentümers, das Objekt zum richtigen Zeitpunkt zu löschen. Aus diesem Grunde ist man sehr selten sieht rohe Zeiger wie in realen Programmen (da es keine Eigentümerinformationen ist mit einem zugehörigen rohen Zeiger). Stattdessen werden Zeiger normalerweise in intelligente Zeiger eingeschlossen. Der Smart Pointer definiert die Semantik, wem der Speicher gehört und wer für die Bereinigung verantwortlich ist.
Beispiele sind:
std::auto_ptr<Pixel> p(new Pixel); // An auto_ptr has move semantics. // When you pass an auto_ptr to a method you are saying here take this. You own it. // Delete it when you are finished. If the receiver takes ownership it usually saves // it in another auto_ptr and the destructor does the actual dirty work of the delete. // If the receiver does not take ownership it is usually deleted. std::tr1::shared_ptr<Pixel> p(new Pixel); // aka boost::shared_ptr // A shared ptr has shared ownership. // This means it can have multiple owners each using the object simultaneously. // As each owner finished with it the shared_ptr decrements the ref count and // when it reaches zero the objects is destroyed. boost::scoped_ptr<Pixel> p(new Pixel); // Makes it act like a normal stack variable. // Ownership is not transferable.
Da sind andere.
quelle
Logischerweise machen sie dasselbe - außer beim Aufräumen. Nur der von Ihnen geschriebene Beispielcode weist im Zeigerfall einen Speicherverlust auf, da dieser Speicher nicht freigegeben wird.
Aus einem Java-Hintergrund stammend, sind Sie möglicherweise nicht vollständig darauf vorbereitet, wie viel von C ++ sich darauf konzentriert, zu verfolgen, was zugewiesen wurde und wer für die Freigabe verantwortlich ist.
Wenn Sie gegebenenfalls Stapelvariablen verwenden, müssen Sie sich keine Gedanken über die Freigabe dieser Variablen machen, sie verschwindet mit dem Stapelrahmen.
Wenn Sie sehr vorsichtig sind, können Sie natürlich immer manuell auf dem Heap und kostenlos zuweisen. Ein Teil der guten Softwareentwicklung besteht jedoch darin, die Dinge so zu erstellen, dass sie nicht kaputt gehen können, anstatt Ihrem übermenschlichen Programmierer zu vertrauen. fu, um niemals einen Fehler zu machen.
quelle
Ich bevorzuge die erste Methode, wenn ich die Chance dazu bekomme, weil:
quelle
"Warum nicht Zeiger für alles in C ++ verwenden?"
Eine einfache Antwort - weil es ein großes Problem beim Verwalten des Speichers wird - Zuweisen und Löschen / Freigeben.
Automatische / Stapelobjekte entfernen einen Teil der geschäftigen Arbeit davon.
Das ist nur das erste, was ich zu dieser Frage sagen würde.
quelle
Eine gute allgemeine Faustregel ist, NIEMALS neue zu verwenden, es sei denn, Sie müssen dies unbedingt tun. Ihre Programme sind einfacher zu warten und weniger fehleranfällig, wenn Sie keine neuen Programme verwenden, da Sie sich keine Gedanken darüber machen müssen, wo Sie sie bereinigen müssen.
quelle
Der Code:
Pixel p; p.x = 2; p.y = 5;
führt keine dynamische Speicherzuweisung durch - es gibt keine Suche nach freiem Speicher, keine Aktualisierung der Speichernutzung, nichts. Es ist völlig kostenlos. Der Compiler reserviert zur Kompilierungszeit Speicherplatz auf dem Stapel für die Variable - er hat viel Speicherplatz zu reservieren und erstellt einen einzelnen Opcode, um den Stapelzeiger um die erforderliche Menge zu bewegen.
Die Verwendung von new erfordert den gesamten Speicherverwaltungsaufwand.
Die Frage lautet dann: Möchten Sie Stapel- oder Heap-Speicherplatz für Ihre Daten verwenden? Stapelvariablen (oder lokale Variablen wie 'p' erfordern keine Dereferenzierung, während die Verwendung von new eine Indirektionsebene hinzufügt.
quelle
Ja, das macht zunächst Sinn, da es aus Java oder C # stammt. Es scheint keine große Sache zu sein, sich daran zu erinnern, den von Ihnen zugewiesenen Speicher freizugeben. Aber wenn Sie dann Ihr erstes Gedächtnisleck bekommen, kratzen Sie sich am Kopf, weil Sie schwören, dass Sie alles befreit haben. Dann, wenn es das zweite Mal passiert und das dritte Mal, werden Sie noch frustrierter. Nach sechs Monaten Kopfschmerzen aufgrund von Speicherproblemen werden Sie es langsam satt und der vom Stapel zugewiesene Speicher wird immer attraktiver. Wie schön und sauber - legen Sie es einfach auf den Stapel und vergessen Sie es. Ziemlich bald werden Sie den Stapel verwenden, wenn Sie damit durchkommen können.
Aber - es gibt keinen Ersatz für diese Erfahrung. Mein Rat? Probieren Sie es jetzt aus. Du wirst sehen.
quelle
Meine Bauchreaktion besteht nur darin, Ihnen zu sagen, dass dies zu ernsthaften Speicherlecks führen kann. Einige Situationen, in denen Sie möglicherweise Zeiger verwenden, können zu Verwirrung darüber führen, wer für das Löschen dieser Zeiger verantwortlich sein sollte. In einfachen Fällen wie Ihrem Beispiel ist es leicht zu erkennen, wann und wo Sie delete aufrufen sollten. Wenn Sie jedoch Zeiger zwischen Klassen übergeben, kann es etwas schwieriger werden.
Ich würde empfehlen, in der Boost Smart Pointers-Bibliothek nach Ihren Zeigern zu suchen .
quelle
Der beste Grund, nicht alles neu zu machen, ist, dass Sie sehr deterministisch bereinigen können, wenn sich Dinge auf dem Stapel befinden. Bei Pixeln ist dies nicht so offensichtlich, aber bei einer Datei wird dies vorteilhaft:
{ // block of code that uses file File aFile("file.txt"); ... } // File destructor fires when file goes out of scope, closing the file aFile // can't access outside of scope (compiler error)
Wenn Sie eine Datei neu erstellen, müssen Sie daran denken, sie zu löschen, um das gleiche Verhalten zu erzielen. Scheint im obigen Fall ein einfaches Problem zu sein. Betrachten Sie jedoch komplexeren Code, z. B. das Speichern der Zeiger in einer Datenstruktur. Was ist, wenn Sie diese Datenstruktur an einen anderen Code übergeben? Wer ist für die Bereinigung verantwortlich? Wer würde alle Ihre Dateien schließen?
Wenn Sie nicht alles neu erstellen, werden die Ressourcen nur vom Destruktor bereinigt, wenn die Variable den Gültigkeitsbereich verlässt. So können Sie sicherer sein, dass Ressourcen erfolgreich bereinigt werden.
Dieses Konzept wird als RAII - Resource Allocation Is Initialization bezeichnet und kann Ihre Fähigkeit zur Beschaffung und Entsorgung von Ressourcen drastisch verbessern.
quelle
Der erste Fall ist nicht immer stapelweise zugeordnet. Wenn es Teil eines Objekts ist, wird es überall dort zugewiesen, wo sich das Objekt befindet. Zum Beispiel:
class Rectangle { Pixel top_left; Pixel bottom_right; } Rectangle r1; // Pixel is allocated on the stack Rectangle *r2 = new Rectangle(); // Pixel is allocated on the heap
Die Hauptvorteile von Stapelvariablen sind:
Sobald das Objekt erstellt wurde, gibt es keinen Leistungsunterschied zwischen einem auf dem Heap zugewiesenen und einem auf dem Stapel zugewiesenen Objekt (oder wo auch immer).
Sie können jedoch keinen Polymorphismus verwenden, es sei denn, Sie verwenden einen Zeiger. Das Objekt hat einen vollständig statischen Typ, der zur Kompilierungszeit festgelegt wird.
quelle
Objektlebensdauer. Wenn die Lebensdauer Ihres Objekts die Lebensdauer des aktuellen Bereichs überschreiten soll, müssen Sie den Heap verwenden.
Wenn Sie die Variable andererseits nicht über den aktuellen Bereich hinaus benötigen, deklarieren Sie sie auf dem Stapel. Es wird automatisch zerstört, wenn es den Gültigkeitsbereich verlässt. Seien Sie nur vorsichtig, wenn Sie die Adresse weitergeben.
quelle
Ich würde sagen, es geht viel um Geschmackssache. Wenn Sie eine Schnittstelle erstellen, über die Methoden Zeiger anstelle von Referenzen verwenden können, können Sie dem Aufrufer erlauben, null zu übergeben. Da Sie dem Benutzer erlauben, null zu übergeben, wird der Benutzer dies tun in Null übergeben.
Da Sie sich fragen müssen: "Was passiert, wenn dieser Parameter Null ist?", Müssen Sie defensiver codieren und sich ständig um Nullprüfungen kümmern. Dies spricht für die Verwendung von Referenzen.
Manchmal möchten Sie jedoch wirklich in der Lage sein, Null zu übergeben, und dann kommen Referenzen nicht in Frage :) Zeiger geben Ihnen mehr Flexibilität und ermöglichen es Ihnen, fauler zu sein, was wirklich gut ist. Niemals zuweisen, bis Sie wissen, dass Sie zuweisen müssen!
quelle
Das Problem sind nicht Zeiger an sich (abgesehen von der Einführung von
NULL
Zeigern), sondern die manuelle Speicherverwaltung.Der lustige Teil ist natürlich, dass jedes Java-Tutorial, das ich gesehen habe, erwähnt hat, dass der Garbage Collector so cool ist, weil Sie nicht daran denken müssen, anzurufen
delete
, wenn C ++ in der Praxis nur benötigt,delete
wenn Sie anrufennew
(unddelete[]
wenn Sie anrufen)new[]
).quelle
Verwenden Sie Zeiger und dynamisch zugewiesene Objekte NUR, WENN SIE MÜSSEN. Verwenden Sie nach Möglichkeit statisch zugewiesene (globale oder Stapel-) Objekte.
Zur Verdeutlichung meine ich in diesem Zusammenhang "statisch" nicht dynamisch zugeordnet. IOW, alles NICHT auf dem Haufen. Ja, sie können auch Probleme mit der Objektlebensdauer haben - in Bezug auf die Reihenfolge der Singleton-Zerstörung -, aber wenn sie auf den Haufen geklebt werden, wird normalerweise nichts gelöst.
quelle
Warum nicht für alles Zeiger verwenden?
Sie sind langsamer.
Compiler-Optimierungen sind mit Zeigerzugriffssymantiken nicht so effektiv. Sie können sie auf einer beliebigen Anzahl von Websites nachlesen, aber hier ist ein anständiges PDF von Intel.
Überprüfen Sie die Seiten 13,14,17,28,32,36;
for (i = j + 1; i <= *n; ++i) { X(i) -= temp * AP(k); }
... eine Reihe von Variationen zu diesem Thema ....
quelle
Die Frage aus einem anderen Blickwinkel betrachten ...
In C ++ können Sie Objekte mit Zeigern (
Foo *
) und Referenzen (Foo &
) referenzieren . Wo immer möglich, verwende ich eine Referenz anstelle eines Zeigers. Wenn Sie beispielsweise als Referenz auf eine Funktion / Methode übergeben, können Sie mithilfe von Referenzen (hoffentlich) die folgenden Annahmen treffen:delete
das Objekt sein. Es ist wie zu sagen: "Hier, verwenden Sie diese Daten, aber geben Sie sie zurück, wenn Sie fertig sind."quelle
Die Frage ist: Warum würden Sie Zeiger für alles verwenden? Stapelzugeordnete Objekte sind nicht nur sicherer und schneller zu erstellen, sondern es wird auch noch weniger eingegeben, und der Code sieht besser aus.
quelle
Etwas, das ich nicht erwähnt habe, ist die erhöhte Speichernutzung. Angenommen, 4-Byte-Ganzzahlen und Zeiger
wird 8 Bytes verwenden, und
Pixel* p = new Pixel();
wird 12 Bytes verwenden, eine 50% ige Erhöhung. Es klingt nicht nach viel, bis Sie genug für ein 512x512-Bild zugewiesen haben. Dann sprechen Sie 2 MB statt 3 MB. Dadurch wird der Aufwand für die Verwaltung des Heapspeichers mit all diesen Objekten ignoriert.
quelle
Auf dem Stapel erstellte Objekte werden schneller erstellt als zugewiesene Objekte.
Warum?
Da das Zuweisen von Speicher (mit dem Standard-Speichermanager) einige Zeit in Anspruch nimmt (um einen leeren Block zu finden oder diesen Block sogar zuzuweisen).
Außerdem haben Sie keine Probleme mit der Speicherverwaltung, da sich das Stapelobjekt automatisch selbst zerstört, wenn es sich außerhalb des Gültigkeitsbereichs befindet.
Der Code ist einfacher, wenn Sie keine Zeiger verwenden. Wenn Sie in Ihrem Entwurf Stapelobjekte verwenden können, empfehle ich Ihnen, dies zu tun.
Ich selbst würde das Problem mit intelligenten Zeigern nicht komplizieren.
OTOH Ich habe ein wenig im eingebetteten Feld gearbeitet und das Erstellen von Objekten auf dem Stapel ist nicht sehr intelligent (da der für jede Aufgabe / jeden Thread zugewiesene Stapel nicht sehr groß ist - Sie müssen vorsichtig sein).
Es ist also eine Frage der Wahl und der Einschränkungen, es gibt keine Antwort, die zu allen passt.
Und vergessen Sie wie immer nicht , es so einfach wie möglich zu halten.
quelle
Wenn Sie Rohzeiger verwenden, haben Sie grundsätzlich kein RAII.
quelle
Das hat mich sehr verwirrt, als ich ein neuer C ++ - Programmierer war (und es war meine Muttersprache). Es gibt viele sehr schlechte C ++ - Tutorials, die im Allgemeinen in eine von zwei Kategorien zu fallen scheinen: "C / C ++" - Tutorials, was wirklich bedeutet, dass es sich um ein C-Tutorial handelt (möglicherweise mit Klassen), und C ++ - Tutorials, die C ++ für Java mit Löschen halten .
Ich glaube, ich habe ungefähr 1 - 1,5 Jahre (mindestens) gebraucht, um irgendwo in meinem Code "neu" einzugeben. Ich habe häufig STL-Container wie Vektor verwendet, was sich für mich darum gekümmert hat.
Ich denke, viele Antworten scheinen entweder zu ignorieren oder es einfach zu vermeiden, direkt zu sagen, wie man dies vermeidet. Im Allgemeinen müssen Sie im Konstruktor nicht mit new zuordnen und im destructor mit delete aufräumen. Stattdessen können Sie das Objekt selbst direkt in die Klasse einfügen (anstatt einen Zeiger darauf) und das Objekt selbst im Konstruktor initialisieren. Dann erledigt der Standardkonstruktor in den meisten Fällen alles, was Sie brauchen.
In fast allen Situationen, in denen dies nicht funktioniert (z. B. wenn der Stapelspeicher knapp wird), sollten Sie wahrscheinlich ohnehin einen der Standardcontainer verwenden: std :: string, std :: vector und std :: map sind die drei, die ich am häufigsten benutze, aber std :: deque und std :: list sind auch ziemlich häufig. Die anderen (Dinge wie std :: set und das nicht standardmäßige Seil ) werden nicht so oft verwendet, verhalten sich aber ähnlich. Sie alle weisen aus dem freien Speicher zu (C ++ - Sprache für "den Heap" in einigen anderen Sprachen), siehe: C ++ STL-Frage: Allokatoren
quelle
Der erste Fall ist am besten, es sei denn, der Pixel-Klasse werden weitere Mitglieder hinzugefügt. Wenn immer mehr Mitglieder hinzugefügt werden, besteht die Möglichkeit einer Stapelüberlaufausnahme
quelle