new - c++ smart pointer




Warum sollten C++-Programmierer die Verwendung von "neu" minimieren? (12)

Weil der Stapel schnell und narrensicher ist

In C ++ benötigt es nur eine einzige Anweisung, um Platz für jedes lokale Scope-Objekt in einer gegebenen Funktion auf dem Stack zuzuweisen, und es ist unmöglich, irgendeinen dieser Speicher zu verlieren. Dieser Kommentar beabsichtigte (oder sollte es haben), etwas wie "benutze den Stapel und nicht den Haufen" zu sagen .

Ich stolperte über Stapelüberlauffrage Speicherverlust mit std :: string bei der Verwendung von std :: list <std :: string> , und einer der Kommentare sagt dies:

Hör auf so viel new benutzen. Ich kann keinen Grund sehen, warum du irgendwo anders benutzt hast. Sie können Objekte nach Wert in C ++ erstellen und das ist einer der großen Vorteile der Sprache. Sie müssen nicht alles auf dem Heap reservieren. Hör auf zu denken wie ein Java-Programmierer.

Ich bin mir nicht sicher, was er damit meint. Warum sollten Objekte in C ++ so oft wie möglich nach Wert erstellt werden, und welchen Unterschied macht sie intern? Habe ich die Antwort falsch interpretiert?


Weil es anfällig für subtile Lecks ist, selbst wenn Sie das Ergebnis in einen intelligenten Zeiger einschließen .

Stellen Sie sich einen "sorgfältigen" Benutzer vor, der sich daran erinnert, Objekte in Smart Pointer zu schreiben:

foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));

Dieser Code ist gefährlich, da es keine Garantie gibt, dass entweder shared_ptr vor T1 oder T2 . Wenn also einer von new T1() oder new T2() fehlschlägt, nachdem der andere erfolgreich ist, wird das erste Objekt geleakt, weil kein shared_ptr vorhanden ist, um es zu zerstören und freizugeben.

Lösung: Verwenden Sie make_shared .


Ein wichtiger Grund dafür, den Heap nicht zu stark zu beanspruchen, ist die Leistung, insbesondere die Leistung des von C ++ verwendeten Standardspeicherverwaltungsmechanismus. Während die Zuweisung im Trivialfall ziemlich schnell sein kann, führt das Ausführen vieler new und das delete von Objekten ungleicher Größe ohne strikte Reihenfolge nicht nur zu einer Speicherfragmentierung, sondern verkompliziert auch den Zuweisungsalgorithmus und kann die Leistung in bestimmten Fällen absolut zerstören .

Das ist das Problem, dass Speicherpools erstellt werden, um zu lösen, was es ermöglicht, die inhärenten Nachteile traditioneller Heap-Implementierungen zu verringern, während Sie immer noch den Heap nach Bedarf verwenden können.

Besser noch, das Problem ganz zu vermeiden. Wenn Sie es auf den Stapel legen können, dann tun Sie es.


Es gibt zwei weit verbreitete Speicherzuweisungstechniken: automatische Zuweisung und dynamische Zuweisung. Üblicherweise gibt es für jeden einen entsprechenden Speicherbereich: den Stack und den Heap.

Stapel

Der Stapel weist Speicher immer sequenziell zu. Dies ist möglich, weil Sie den Speicher in umgekehrter Reihenfolge freigeben müssen (First-In, Last-Out: FILO). Dies ist die Speicherzuweisungstechnik für lokale Variablen in vielen Programmiersprachen. Es ist sehr, sehr schnell, weil es eine minimale Buchhaltung erfordert und die nächste zuzuweisende Adresse implizit ist.

In C ++ wird dies als automatischer Speicher bezeichnet, da der Speicher am Ende des Bereichs automatisch beansprucht wird. Sobald die Ausführung des aktuellen Codeblocks (mit {} ) abgeschlossen ist, wird der Speicher für alle Variablen in diesem Block automatisch gesammelt. Dies ist auch der Moment, in dem Destruktoren aufgerufen werden, um Ressourcen zu bereinigen.

Haufen

Der Heap ermöglicht einen flexibleren Speicherzuordnungsmodus. Die Buchhaltung ist komplexer und die Zuweisung ist langsamer. Da es keinen impliziten Freigabepunkt gibt, müssen Sie den Speicher manuell freigeben, indem Sie delete oder delete[] ( free in C) verwenden. Das Fehlen eines impliziten Freigabepunkts ist jedoch der Schlüssel zur Flexibilität des Heapspeichers.

Gründe für die dynamische Zuordnung

Auch wenn die Verwendung des Heapspeichers langsamer ist und möglicherweise zu Speicherlecks oder Speicherfragmentierung führt, gibt es durchaus brauchbare Anwendungsfälle für die dynamische Zuweisung, da diese weniger eingeschränkt sind.

Zwei Hauptgründe für die dynamische Zuordnung:

  • Sie wissen nicht, wie viel Speicher Sie zur Kompilierzeit benötigen. Wenn Sie z. B. eine Textdatei in eine Zeichenfolge lesen, wissen Sie normalerweise nicht, welche Größe die Datei hat. Sie können also nicht entscheiden, wie viel Speicher zuzuordnen ist, bis Sie das Programm ausführen.

  • Sie möchten Speicher reservieren, der nach Verlassen des aktuellen Bausteins bestehen bleibt. Beispielsweise möchten Sie möglicherweise eine Funktionszeichenfolge string readfile(string path) schreiben, die den Inhalt einer Datei zurückgibt. In diesem Fall könnten Sie, selbst wenn der Stapel den gesamten Dateiinhalt enthalten könnte, nicht von einer Funktion zurückkehren und den zugewiesenen Speicherblock behalten.

Warum dynamische Zuordnung oft unnötig ist

In C ++ gibt es ein ordentliches Konstrukt namens Destruktor . Mit diesem Mechanismus können Sie Ressourcen verwalten, indem Sie die Lebensdauer der Ressource mit der Lebensdauer einer Variablen abstimmen. Diese Technik wird RAII und ist der Unterscheidungspunkt von C ++. Es "umschließt" Ressourcen in Objekte. std::string ist ein perfektes Beispiel. Dieser Ausschnitt:

int main ( int argc, char* argv[] )
{
    std::string program(argv[0]);
}

weist tatsächlich eine variable Speichermenge zu. Das std::string Objekt reserviert Speicher unter Verwendung des Heapspeichers und gibt es in seinem Destruktor frei. In diesem Fall mussten Sie keine Ressourcen manuell verwalten und erhielten trotzdem die Vorteile der dynamischen Speicherzuweisung.

Insbesondere impliziert dies, dass in diesem Ausschnitt:

int main ( int argc, char* argv[] )
{
    std::string * program = new std::string(argv[0]);  // Bad!
    delete program;
}

es gibt eine nicht benötigte dynamische Speicherzuweisung. Das Programm erfordert mehr Tipparbeit (!) Und birgt das Risiko, dass die Freigabe des Speichers vergessen wird. Es tut dies ohne erkennbaren Nutzen.

Warum sollten Sie den automatischen Speicher so oft wie möglich verwenden?

Im Wesentlichen fasst der letzte Absatz es zusammen. Wenn Sie den automatischen Speicher so oft wie möglich verwenden, machen Sie Ihre Programme:

  • schneller tippen;
  • schneller beim Lauf;
  • weniger anfällig für Speicher / Ressourcen-Lecks.

Bonuspunkte

In der angesprochenen Frage gibt es zusätzliche Bedenken. Insbesondere die folgende Klasse:

class Line {
public:
    Line();
    ~Line();
    std::string* mString;
};

Line::Line() {
    mString = new std::string("foo_bar");
}

Line::~Line() {
    delete mString;
}

Ist eigentlich viel gefährlicher zu verwenden als die folgende:

class Line {
public:
    Line();
    std::string mString;
};

Line::Line() {
    mString = "foo_bar";
    // note: there is a cleaner way to write this.
}

Der Grund dafür ist, dass std::string einen Kopierkonstruktor richtig definiert. Betrachten Sie das folgende Programm:

int main ()
{
    Line l1;
    Line l2 = l1;
}

In der ursprünglichen Version wird dieses Programm wahrscheinlich abstürzen, da es zweimal in derselben Zeichenfolge delete . Unter Verwendung der modifizierten Version besitzt jede Line eine eigene Zeichenfolgeninstanz, jede mit einem eigenen Speicher, und beide werden am Ende des Programms freigegeben.

Weitere Hinweise

Die umfassende Verwendung von RAII wird in C ++ aus allen oben genannten Gründen als Best Practice angesehen. Es gibt jedoch einen zusätzlichen Vorteil, der nicht unmittelbar offensichtlich ist. Grundsätzlich ist es besser als die Summe seiner Teile. Der ganze Mechanismus komponiert . Es skaliert.

Wenn Sie die Line Klasse als Baustein verwenden:

 class Table
 {
      Line borders[4];
 };

Dann

 int main ()
 {
     Table table;
 }

weist vier std::string Instanzen, vier Zeileninstanzen , eine Table und den gesamten Inhalt der Zeichenfolge zu und alles wird automatisch freigegeben .


Ich denke, das Poster sollte sagen, dass You do not have to allocate everything on the heap und nicht den stack You do not have to allocate everything on the .

Grundsätzlich werden Objekte auf dem Stapel zugewiesen (wenn die Objektgröße natürlich erlaubt), wegen der billigen Kosten der Stapelzuordnung, anstatt einer haufenbasierten Zuweisung, die ziemlich viel Arbeit durch den Zuordner erfordert, und fügt Ausführlichkeit hinzu, weil dann das nötig ist Verwalten Sie die auf dem Heap zugewiesenen Daten.


Ich sehe, dass einige wichtige Gründe fehlen, um so wenige neue wie möglich zu machen:

Operator new hat eine nicht-deterministische Ausführungszeit

Das Aufrufen von new kann dazu führen, dass das Betriebssystem Ihrem Prozess eine neue physische Seite zuweist. Dies kann jedoch sehr langsam sein, wenn Sie dies häufig tun. Oder es kann bereits ein passender Speicherplatz vorhanden sein, den wir nicht kennen. Wenn Ihr Programm konsistente und vorhersehbare Ausführungszeit benötigt (wie in einem Echtzeit-System oder Spiel / Physik-Simulation) müssen Sie in Ihrer Zeit kritische Schleifen vermeiden.

Operator new ist eine implizite Thread-Synchronisation

Ja, Sie haben gehört, Ihr Betriebssystem muss sicherstellen, dass Ihre Seitentabellen konsistent sind. Wenn Sie also new aufrufen, wird Ihr Thread eine implizite Mutex-Sperre erhalten. Wenn Sie ständig von vielen Threads new aufrufen, serialisieren Sie tatsächlich Ihre Threads (ich habe dies mit 32 CPUs gemacht, die jeweils auf new , um jeweils ein paar hundert Bytes zu bekommen, autsch! Das war eine königliche Pita zum Debuggen)

Der Rest wie langsam, fragmentiert, fehleranfällig usw. wurde bereits von anderen Antworten erwähnt.


Objekte, die von new erstellt wurden, müssen schließlich delete damit sie nicht auslaufen. Der Destruktor wird nicht aufgerufen, Speicher wird nicht freigegeben, das ganze Bit. Da C ++ keine Speicherbereinigung hat, ist das ein Problem.

Objekte, die mit Wert erstellt wurden (dh auf einem Stapel), sterben automatisch, wenn sie den Gültigkeitsbereich verlassen. Der Destruktoraufruf wird vom Compiler eingefügt, und der Speicher wird bei der Rückgabe der Funktion automatisch freigegeben.

Intelligente Zeiger wie auto_ptr , shared_ptr lösen das Dangling-Referenzproblem, aber sie erfordern Codierungsdisziplin und haben andere Probleme (Kopierbarkeit, Referenzschleifen usw.).

Außerdem ist new in stark multithreadbasierten Szenarien ein Konfliktpunkt zwischen Threads. Es kann einen Leistungseinfluss geben, wenn Sie übermäßig new . Die Erstellung von Stack-Objekten ist per Definition Thread-lokal, da jeder Thread seinen eigenen Stack hat.

Der Nachteil von Wertobjekten besteht darin, dass sie abstürzen, sobald die Hostfunktion zurückkehrt - Sie können keinen Verweis auf diese zurückgeben an den Aufrufer, nur durch Kopieren oder Rückgabe nach Wert.


Wenn Sie new verwenden, werden Objekte dem Heap zugewiesen. Es wird normalerweise verwendet, wenn Sie eine Erweiterung erwarten. Wenn Sie ein Objekt wie

Class var;

es wird auf den Stapel gelegt.

Sie müssen immer zerstören auf das Objekt, das Sie auf dem Haufen mit neuen platziert haben. Dies eröffnet das Potenzial für Speicherlecks. Objekte auf dem Stapel sind nicht anfällig für Speicherverlust!


Zwei Gründe:

  1. In diesem Fall ist es unnötig. Sie machen Ihren Code unnötig komplizierter.
  2. Es weist Speicherplatz auf dem Heap zu und es bedeutet, dass Sie sich erinnern müssen, es später zu delete , oder es wird zu einem Speicherverlust führen.

new() sollte nicht so wenig wie möglich verwendet werden. Es sollte so sorgfältig wie möglich verwendet werden. Und es sollte so oft wie nötig verwendet werden, so wie es der Pragmatismus vorschreibt.

Die Zuteilung von Objekten auf dem Stapel, basierend auf ihrer impliziten Zerstörung, ist ein einfaches Modell. Wenn der erforderliche Bereich eines Objekts zu diesem Modell passt, ist es nicht notwendig, new() mit dem zugehörigen delete() und dem Überprüfen von NULL-Zeigern zu verwenden. In dem Fall, in dem Sie viele kurzlebige Objekte haben, sollte die Zuweisung auf dem Stapel die Probleme der Heap-Fragmentierung reduzieren.

Wenn die Lebensdauer Ihres Objekts jedoch über den aktuellen Gültigkeitsbereich hinausgehen muss, ist new() die richtige Antwort. Stellen Sie nur sicher, dass Sie darauf achten, wann und wie Sie delete() und die Möglichkeiten von NULL-Zeigern aufrufen, indem Sie gelöschte Objekte und alle anderen Fehler verwenden, die bei der Verwendung von Zeigern auftreten.



  • C ++ verwendet keinen eigenen Speichermanager. Andere Sprachen wie C #, Java hat einen Garbage Collector, um den Speicher zu verarbeiten
  • C ++, das Betriebssystemroutinen verwendet, um den Speicher zuzuordnen und zu viel Neues / Löschen, könnte den verfügbaren Speicher fragmentieren
  • Wenn der Speicher häufig verwendet wird, ist es ratsam, ihn vorher zuzuweisen und freizugeben, wenn er nicht benötigt wird.
  • Unsachgemäße Speicherverwaltung kann zu Speicherlecks führen und ist sehr schwer nachzuverfolgen. Die Verwendung von Stack-Objekten im Rahmen der Funktion ist also eine bewährte Technik
  • Der Nachteil bei der Verwendung von Stapelobjekten besteht darin, dass bei der Rückgabe, Weiterleitung zu Funktionen usw. mehrere Kopien von Objekten erstellt werden. Intelligente Compiler kennen diese Situationen jedoch sehr gut und wurden für die Leistung optimiert
  • In C ++ ist es sehr mühsam, wenn der Speicher an zwei verschiedenen Stellen zugewiesen und freigegeben wird. Die Verantwortung für die Veröffentlichung ist immer eine Frage und meistens sind wir auf einige allgemein zugängliche Zeiger, Stapelobjekte (maximal möglich) und Techniken wie auto_ptr (RAII Objekte) angewiesen.
  • Das Beste ist, dass Sie die Kontrolle über den Speicher haben, und das Schlimmste ist, dass Sie keine Kontrolle über den Speicher haben werden, wenn wir eine unsachgemäße Speicherverwaltung für die Anwendung verwenden. Die Abstürze aufgrund von Speicherbeschädigungen sind die schlimmsten und schwer zu verfolgen.




c++-faq