Ich habe gesucht, aber diese drei Konzepte nicht sehr gut verstanden. Wann muss ich die dynamische Zuordnung (im Heap) verwenden und was ist ihr wirklicher Vorteil? Was sind die Probleme von Statik und Stapel? Könnte ich eine gesamte Anwendung schreiben, ohne Variablen im Heap zuzuweisen?
Ich habe gehört, dass andere Sprachen einen "Garbage Collector" enthalten, sodass Sie sich keine Sorgen um den Speicher machen müssen. Was macht der Müllsammler?
Was können Sie tun, um den Speicher selbst zu manipulieren, was Sie mit diesem Garbage Collector nicht tun können?
Einmal sagte mir jemand mit dieser Erklärung:
int * asafe=new int;
Ich habe einen "Zeiger auf einen Zeiger". Was heißt das? Es ist anders als:
asafe=new int;
?
Antworten:
Eine ähnliche Frage wurde gestellt, aber nicht nach der Statik.
Zusammenfassung der statischen, Heap- und Stapelspeicher:
Eine statische Variable ist im Grunde eine globale Variable, auch wenn Sie nicht global darauf zugreifen können. Normalerweise gibt es eine Adresse dafür, die sich in der ausführbaren Datei selbst befindet. Es gibt nur eine Kopie für das gesamte Programm. Unabhängig davon, wie oft Sie in einen Funktionsaufruf (oder eine Klasse) gehen (und in wie vielen Threads!), Bezieht sich die Variable auf denselben Speicherort.
Der Heap ist eine Menge Speicher, der dynamisch verwendet werden kann. Wenn Sie 4 KB für ein Objekt benötigen, durchsucht der dynamische Allokator die Liste des freien Speicherplatzes im Heap, wählt einen 4-KB-Block aus und gibt ihn Ihnen. Im Allgemeinen beginnt der dynamische Speicherzuweiser (malloc, new usw.) am Ende des Speichers und arbeitet rückwärts.
Zu erklären, wie ein Stapel wächst und schrumpft, liegt etwas außerhalb des Rahmens dieser Antwort. Es reicht jedoch zu sagen, dass Sie immer nur am Ende hinzufügen und entfernen. Stapel beginnen normalerweise hoch und wachsen auf niedrigere Adressen herab. Ihnen geht der Speicher aus, wenn der Stapel irgendwo in der Mitte auf den dynamischen Allokator trifft (beziehen Sie sich jedoch auf physischen oder virtuellen Speicher und Fragmentierung). Für mehrere Threads sind mehrere Stapel erforderlich (der Prozess reserviert im Allgemeinen eine Mindestgröße für den Stapel).
Wann möchten Sie jeden verwenden:
Statics / Globals sind nützlich für Speicher, von denen Sie wissen, dass Sie sie immer benötigen und die Sie niemals freigeben möchten. (Übrigens kann man sich vorstellen, dass eingebettete Umgebungen nur statischen Speicher haben. Der Stapel und der Heap sind Teil eines bekannten Adressraums, der von einem dritten Speichertyp gemeinsam genutzt wird: dem Programmcode. Programme führen häufig eine dynamische Zuweisung aus ihrem heraus statischer Speicher, wenn sie beispielsweise verknüpfte Listen benötigen. Unabhängig davon wird der statische Speicher selbst (der Puffer) nicht selbst "zugewiesen", sondern andere Objekte werden zu diesem Zweck aus dem vom Puffer gehaltenen Speicher zugewiesen. Sie können dies tun Auch in nicht eingebetteten Spielen und in Konsolenspielen werden häufig die eingebauten dynamischen Speichermechanismen vermieden, um den Zuweisungsprozess genau zu steuern, indem Puffer mit voreingestellten Größen für alle Zuweisungen verwendet werden.)
Stapelvariablen sind nützlich, wenn Sie wissen, dass die Variablen erhalten bleiben sollen, solange sich die Funktion im Gültigkeitsbereich befindet (irgendwo auf dem Stapel). Stapel eignen sich gut für Variablen, die Sie für den Code benötigen, in dem sie sich befinden, die jedoch außerhalb dieses Codes nicht benötigt werden. Sie eignen sich auch sehr gut, wenn Sie auf eine Ressource wie eine Datei zugreifen und möchten, dass die Ressource automatisch verschwindet, wenn Sie diesen Code verlassen.
Heap-Zuweisungen (dynamisch zugewiesener Speicher) sind nützlich, wenn Sie flexibler als oben beschrieben sein möchten. Häufig wird eine Funktion aufgerufen, um auf ein Ereignis zu reagieren (der Benutzer klickt auf die Schaltfläche "Box erstellen"). Für die richtige Antwort muss möglicherweise ein neues Objekt (ein neues Box-Objekt) zugewiesen werden, das lange nach dem Beenden der Funktion erhalten bleibt, damit es nicht auf dem Stapel liegen kann. Sie wissen jedoch nicht, wie viele Boxen Sie zu Beginn des Programms benötigen würden, daher kann es sich nicht um eine statische Box handeln.
Müllabfuhr
Ich habe in letzter Zeit viel darüber gehört, wie großartig Garbage Collectors sind, also wäre vielleicht eine etwas abweichende Stimme hilfreich.
Garbage Collection ist ein wunderbarer Mechanismus, wenn die Leistung kein großes Problem darstellt. Ich höre, dass GCs immer besser und ausgefeilter werden, aber Tatsache ist, dass Sie möglicherweise gezwungen sind, eine Leistungsstrafe zu akzeptieren (abhängig vom Anwendungsfall). Und wenn Sie faul sind, funktioniert es möglicherweise immer noch nicht richtig. Im besten Fall stellen Garbage Collectors fest, dass Ihr Speicher verloren geht, wenn festgestellt wird, dass keine Referenzen mehr vorhanden sind (siehe Referenzzählung)). Wenn Sie jedoch ein Objekt haben, das auf sich selbst verweist (möglicherweise durch Bezugnahme auf ein anderes Objekt, das auf sich selbst verweist), bedeutet die Referenzzählung allein nicht, dass der Speicher gelöscht werden kann. In diesem Fall muss der GC die gesamte Referenzsuppe untersuchen und herausfinden, ob es Inseln gibt, auf die nur von sich selbst Bezug genommen wird. Auf Anhieb würde ich vermuten, dass dies eine O (n ^ 2) -Operation ist, aber was auch immer es ist, es kann schlecht werden, wenn Sie sich überhaupt um die Leistung kümmern. (Bearbeiten: Martin B weist darauf hin, dass es O (n) für einigermaßen effiziente Algorithmen ist. Das ist immer noch O (n) zu viel, wenn Sie sich mit Leistung befassen und die Zuordnung in konstanter Zeit ohne Speicherbereinigung aufheben können.)
Persönlich, wenn ich Leute sagen höre, dass C ++ keine Speicherbereinigung hat, markiert mein Verstand dies als eine Funktion von C ++, aber ich bin wahrscheinlich in der Minderheit. Wahrscheinlich ist es für Menschen am schwierigsten, etwas über das Programmieren in C und C ++ zu lernen, Zeiger zu lernen und wie sie mit ihren dynamischen Speicherzuordnungen richtig umgehen. Einige andere Sprachen, wie Python, wären ohne GC schrecklich. Ich denke, es kommt darauf an, was Sie von einer Sprache erwarten. Wenn Sie eine zuverlässige Leistung wünschen, ist C ++ ohne Speicherbereinigung das einzige, was mir auf dieser Seite von Fortran einfällt. Wenn Sie eine einfache Bedienung und Trainingsräder wünschen (um einen Absturz zu vermeiden, ohne dass Sie die "richtige" Speicherverwaltung erlernen müssen), wählen Sie etwas mit einem GC aus. Selbst wenn Sie wissen, wie man Speicher gut verwaltet, sparen Sie Zeit, die Sie für die Optimierung anderer Codes aufwenden können. Es gibt wirklich keine großen Leistungseinbußen mehr, aber wenn Sie wirklich zuverlässige Leistung benötigen (und die Fähigkeit, genau zu wissen, was wann unter der Decke vor sich geht), würde ich mich an C ++ halten. Es gibt einen Grund, warum jede wichtige Spiel-Engine, von der ich je gehört habe, in C ++ ist (wenn nicht C oder Assembly). Python et al. Sind gut für Skripte geeignet, aber nicht die Hauptspiel-Engine.
quelle
Das Folgende ist natürlich alles nicht ganz genau. Nimm es mit einem Körnchen Salz, wenn du es liest :)
Nun, die drei Dinge, auf die Sie sich beziehen, sind die automatische, statische und dynamische Speicherdauer , die etwas damit zu tun hat, wie lange Objekte leben und wann sie zu leben beginnen.
Automatische Speicherdauer
Sie verwenden die automatische Speicherdauer für kurzlebige und kleine Daten, die nur lokal innerhalb eines Blocks benötigt werden:
Die Lebensdauer endet, sobald wir den Block verlassen, und sie beginnt, sobald das Objekt definiert ist. Sie sind die einfachste Art der Speicherdauer und viel schneller als insbesondere die dynamische Speicherdauer.
Statische Speicherdauer
Sie verwenden die statische Speicherdauer für freie Variablen, auf die jeder Code jederzeit zugreifen kann, wenn ihr Gültigkeitsbereich eine solche Verwendung zulässt (Namespace-Gültigkeitsbereich), und für lokale Variablen, die ihre Lebensdauer über den Exit ihres Gültigkeitsbereichs verlängern müssen (lokaler Gültigkeitsbereich) für Mitgliedsvariablen, die von allen Objekten ihrer Klasse gemeinsam genutzt werden müssen (Klassenbereich). Ihre Lebensdauer hängt von dem Bereich ab, in dem sie sich befinden. Sie können einen Namespace-Bereich sowie einen lokalen Bereich und einen Klassenbereich haben . Was für beide gilt, ist, dass, sobald ihr Leben beginnt, das Leben am Ende des Programms endet . Hier sind zwei Beispiele:
Das Programm wird gedruckt
ababab
, dalocalA
es beim Verlassen seines Blocks nicht zerstört wird. Sie können sagen, dass Objekte mit lokalem Gültigkeitsbereich ihre Lebensdauer beginnen, wenn die Steuerung ihre Definition erreicht . DennlocalA
es passiert, wenn der Körper der Funktion eingegeben wird. Bei Objekten im Namespace-Bereich beginnt die Lebensdauer beim Programmstart . Gleiches gilt für statische Objekte im Klassenbereich:Wie Sie sehen,
classScopeA
ist nicht an bestimmte Objekte seiner Klasse gebunden, sondern an die Klasse selbst. Die Adresse aller drei oben genannten Namen ist dieselbe und alle bezeichnen dasselbe Objekt. Es gibt spezielle Regeln darüber, wann und wie statische Objekte initialisiert werden, aber wir wollen uns jetzt nicht darum kümmern. Das ist mit dem Begriff Fiasko der statischen Initialisierungsreihenfolge gemeint .Dynamische Speicherdauer
Die letzte Speicherdauer ist dynamisch. Sie verwenden es, wenn Objekte auf einer anderen Insel leben sollen, und wenn Sie Zeiger um diese Referenz setzen möchten. Sie verwenden sie auch, wenn Ihre Objekte groß sind und wenn Sie Arrays mit einer Größe erstellen möchten, die nur zur Laufzeit bekannt ist . Aufgrund dieser Flexibilität sind Objekte mit dynamischer Speicherdauer kompliziert und langsam zu verwalten. Objekte mit dieser dynamischen Dauer beginnen ihre Lebensdauer, wenn ein geeigneter neuer Operatoraufruf erfolgt:
Seine Lebensdauer endet nur, wenn Sie delete für sie aufrufen . Wenn Sie das vergessen, beenden diese Objekte niemals ihre Lebensdauer. Bei Klassenobjekten, die einen vom Benutzer deklarierten Konstruktor definieren, werden die Destruktoren nicht aufgerufen. Objekte mit dynamischer Speicherdauer erfordern eine manuelle Behandlung ihrer Lebensdauer und der zugehörigen Speicherressource. Es gibt Bibliotheken, um die Verwendung zu vereinfachen. Die explizite Speicherbereinigung für bestimmte Objekte kann mithilfe eines intelligenten Zeigers eingerichtet werden:
Sie müssen sich nicht darum kümmern, delete aufzurufen: Der freigegebene ptr erledigt dies für Sie, wenn der letzte Zeiger, der auf das Objekt verweist, den Gültigkeitsbereich verlässt. Der gemeinsam genutzte ptr selbst hat eine automatische Speicherdauer. Daher wird seine Lebensdauer automatisch verwaltet, sodass geprüft werden kann, ob das auf ein dynamisches Objekt gerichtete Objekt in seinem Destruktor gelöscht werden soll. Eine Referenz zu shared_ptr finden Sie in den Boost-Dokumenten: http://www.boost.org/doc/libs/1_37_0/libs/smart_ptr/shared_ptr.htm
quelle
Es wurde ausführlich gesagt, genau wie "die kurze Antwort":
Lebensdauer der statischen Variablen (Klasse) = Programmlaufzeit (1)
Sichtbarkeit = bestimmt durch Zugriffsmodifikatoren (privat / geschützt / öffentlich)
Lebensdauer der statischen Variablen (globaler Bereich) = Programmlaufzeit (1)
Sichtbarkeit = die Kompilierungseinheit, in der sie instanziiert wird (2)
Heap-Variable
Lebensdauer = von Ihnen definiert (neu zu löschen)
Sichtbarkeit = von Ihnen definiert (was auch immer Sie den Zeiger zuweisen)
stack variable
Sichtbarkeit = von Deklaration bis Umfang verlassen wird
lifetime = von der Meldung bis Deklarieren Umfang verlassen wird
(1) genauer: von der Initialisierung bis zur Deinitialisierung der Kompilierungseinheit (dh C / C ++ - Datei). Die Reihenfolge der Initialisierung von Kompilierungseinheiten ist im Standard nicht definiert.
(2) Achtung: Wenn Sie eine statische Variable in einem Header instanziieren, erhält jede Kompilierungseinheit eine eigene Kopie.
quelle
Ich bin sicher, dass einer der Pedanten in Kürze eine bessere Antwort finden wird, aber der Hauptunterschied ist Geschwindigkeit und Größe.
Stapel
Dramatisch schneller zuzuordnen. Dies erfolgt in O (1), da es beim Einrichten des Stapelrahmens so zugewiesen wird, dass er im Wesentlichen frei ist. Der Nachteil ist, dass Sie entbeint werden, wenn Ihnen der Stapelspeicher ausgeht. Sie können die Stapelgröße anpassen, aber mit IIRC haben Sie ~ 2 MB zum Spielen. Sobald Sie die Funktion verlassen, wird alles auf dem Stapel gelöscht. Es kann daher problematisch sein, später darauf Bezug zu nehmen. (Zeiger zum Stapeln zugeordneter Objekte führen zu Fehlern.)
Haufen
Dramatisch langsamer zuzuordnen. Aber Sie haben GB zum Spielen und zeigen darauf.
Müllsammler
Der Garbage Collector ist ein Code, der im Hintergrund ausgeführt wird und Speicher freigibt. Wenn Sie Speicher auf dem Heap zuweisen, können Sie leicht vergessen, ihn freizugeben, was als Speicherverlust bezeichnet wird. Mit der Zeit wächst und wächst der Speicher, den Ihre Anwendung verbraucht, bis sie abstürzt. Wenn ein Garbage Collector regelmäßig den nicht mehr benötigten Speicher freigibt, können Sie diese Fehlerklasse beseitigen. Dies hat natürlich seinen Preis, da der Müllsammler die Dinge verlangsamt.
quelle
Das Problem bei der "statischen" Zuweisung besteht darin, dass die Zuweisung zur Kompilierungszeit erfolgt: Sie können sie nicht zum Zuweisen einer variablen Anzahl von Daten verwenden, deren Anzahl erst zur Laufzeit bekannt ist.
Das Problem bei der Zuordnung auf dem "Stapel" besteht darin, dass die Zuordnung zerstört wird, sobald das Unterprogramm, das die Zuordnung vornimmt, zurückkehrt.
Vielleicht, aber nicht eine nicht triviale, normale, große Anwendung (aber sogenannte "eingebettete" Programme könnten ohne den Heap unter Verwendung einer Teilmenge von C ++ geschrieben werden).
Es überwacht weiterhin Ihre Daten ("Markieren und Durchsuchen"), um festzustellen, wenn Ihre Anwendung nicht mehr darauf verweist. Dies ist praktisch für die Anwendung, da die Anwendung die Zuordnung der Daten nicht aufheben muss ... der Garbage Collector jedoch möglicherweise rechenintensiv ist.
Garbage Collectors sind keine übliche Funktion der C ++ - Programmierung.
Lernen Sie die C ++ - Mechanismen für die deterministische Speicherfreigabe kennen:
quelle
Die Stapelspeicherzuordnung (Funktionsvariablen, lokale Variablen) kann problematisch sein, wenn Ihr Stapel zu "tief" ist und Sie den für die Stapelzuweisung verfügbaren Speicher überlaufen. Der Heap ist für Objekte gedacht, auf die über mehrere Threads oder während des gesamten Programmlebenszyklus zugegriffen werden muss. Sie können ein ganzes Programm schreiben, ohne den Heap zu verwenden.
Sie können ganz einfach Speicher ohne Speicherbereinigung verlieren, aber Sie können auch bestimmen, wann Objekte und Speicher freigegeben werden. Ich habe Probleme mit Java, wenn es den GC ausführt, und ich habe einen Echtzeitprozess, da der GC ein exklusiver Thread ist (nichts anderes kann ausgeführt werden). Wenn die Leistung kritisch ist und Sie sicherstellen können, dass keine Objekte durchgesickert sind, ist die Verwendung eines GC sehr hilfreich. Andernfalls hassen Sie nur das Leben, wenn Ihre Anwendung Speicher verbraucht und Sie die Ursache eines Lecks ermitteln müssen.
quelle
Was ist, wenn Ihr Programm nicht im Voraus weiß, wie viel Speicher zugewiesen werden soll (daher können Sie keine Stapelvariablen verwenden). Sagen wir verknüpfte Listen, die Listen können wachsen, ohne vorher zu wissen, wie groß sie sind. Das Zuweisen auf einem Heap ist daher für eine verknüpfte Liste sinnvoll, wenn Sie nicht wissen, wie viele Elemente in den Heap eingefügt werden.
quelle
Ein Vorteil von GC in einigen Situationen ist ein Ärger in anderen; Das Vertrauen in GC ermutigt dazu, nicht viel darüber nachzudenken. Wartet theoretisch bis zur Leerlaufzeit oder bis es unbedingt sein muss, bis die Bandbreite gestohlen und die Antwortlatenz in Ihrer App verursacht wird.
Aber Sie müssen nicht darüber nachdenken. Wie bei allem anderen in Multithread-Apps können Sie nachgeben, wenn Sie nachgeben können. So ist es beispielsweise in .Net möglich, einen GC anzufordern. Auf diese Weise können Sie anstelle eines weniger häufig laufenden GC mit längerer Laufzeit häufiger einen GC mit kürzerer Laufzeit verwenden und die mit diesem Overhead verbundene Latenz verteilen.
Dies besiegt jedoch die Hauptattraktion von GC, die "ermutigt zu sein scheint, nicht viel darüber nachdenken zu müssen, weil es automatisch ist".
Wenn Sie zum ersten Mal der Programmierung ausgesetzt waren, bevor GC vorherrschte, und mit malloc / free und new / delete vertraut waren, kann es sogar vorkommen, dass Sie GC ein wenig nervig finden und / oder misstrauisch sind (da man misstrauisch gegenüber ' Optimierung, die eine wechselvolle Geschichte hat.) Viele Apps tolerieren zufällige Latenzzeiten. Bei Apps, bei denen dies nicht der Fall ist und bei denen eine zufällige Latenz weniger akzeptabel ist, besteht eine häufige Reaktion darin, GC-Umgebungen zu meiden und sich in Richtung rein nicht verwalteten Codes zu bewegen (oder Gott bewahre, eine lange sterbende Kunst, Assemblersprache).
Ich hatte vor einiger Zeit einen Sommerstudenten hier, einen Praktikanten, ein kluges Kind, das auf GC entwöhnt wurde; Er war so begeistert von der Überlegenheit von GC, dass er sich selbst beim Programmieren in nicht verwaltetem C / C ++ weigerte, dem malloc / free new / delete-Modell zu folgen, weil "Sie dies nicht in einer modernen Programmiersprache tun müssen". Und du weißt? Bei winzigen Apps mit kurzer Laufzeit können Sie zwar damit durchkommen, nicht jedoch bei Apps mit langer Laufzeit.
quelle
Der Stapel ist ein Speicher, der vom Compiler zugewiesen wird. Wenn wir das Programm kompilieren, weist der Standard-Compiler dem Betriebssystem Speicher zu (wir können die Einstellungen von den Compilereinstellungen in Ihrer IDE ändern), und das Betriebssystem gibt Ihnen den Speicher, abhängig davon Bei vielen verfügbaren Speichern auf dem System und vielen anderen Dingen wird das Zuweisen zum Stapelspeicher zugewiesen, wenn wir eine Variable deklarieren, die sie kopieren (als Formale referenzieren). Diese Variablen werden zum Stapeln weitergeleitet. Sie folgen standardmäßig einigen Namenskonventionen, ihrer CDECL in Visual Studios Beispiel: Infixnotation: c = a + b; Das Schieben des Stapels erfolgt von rechts nach links. DRÜCKEN, b stapeln, Operator, a stapeln und das Ergebnis dieser i, ec stapeln. In Pre-Fix-Notation: = + cab Hier werden alle Variablen auf den ersten Stapel (von rechts nach links) verschoben und dann die Operation ausgeführt. Dieser vom Compiler zugewiesene Speicher ist fest. Nehmen wir also an, dass unserer Anwendung 1 MB Speicher zugewiesen ist. Nehmen wir beispielsweise an, dass Variablen 700 KB Speicher verwenden (alle lokalen Variablen werden auf den Stapel verschoben, sofern sie nicht dynamisch zugewiesen werden), sodass der verbleibende 324 KB Speicher dem Heap zugewiesen wird. Und dieser Stapel hat weniger Lebensdauer, wenn der Umfang der Funktion endet, werden diese Stapel gelöscht.
quelle