Frage Warum sollten C ++ - Programmierer die Verwendung von "neu" minimieren?


Ich stolperte über Stack Overflow Frage Speicherverlust mit std :: string bei Verwendung von std :: list <std :: string>, und einer der Kommentare sagt das:

Hör auf zu benutzen new so sehr. Ich kann keinen Grund sehen, warum du irgendwo neu benutzt hast   du machtest. Sie können Objekte nach Wert in C ++ erstellen und es ist einer der   große Vorteile bei der Verwendung der Sprache. Sie müssen nicht zuordnen   alles auf dem Haufen. 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?


750
2018-06-28 00:08


Ursprung


Antworten:


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 zuzuteilende Adresse implizit ist.

In C ++ wird dies aufgerufen automatischer Speicher weil der Speicher am Ende des Bereichs automatisch beansprucht wird. Sobald die Ausführung des aktuellen Codeblocks (mit {}) ist abgeschlossen, Speicher für alle Variablen in diesem Block wird automatisch gesammelt. Dies ist auch der Moment wo Destruktoren werden aufgerufen, 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 delete oder delete[] (free in C). 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. Zum Beispiel möchten Sie vielleicht eine Funktion schreiben string readfile(string path) Das gibt den Inhalt einer Datei zurück. 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 a Destruktor. Mit diesem Mechanismus können Sie Ressourcen verwalten, indem Sie die Lebensdauer der Ressource mit der Lebensdauer einer Variablen abstimmen. Diese Technik wird aufgerufen RAII und ist der kennzeichnende Punkt 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 mit dem Heap und gibt es in seinem Destruktor frei. In diesem Fall hast du es getan nicht müssen alle Ressourcen manuell verwalten und haben dennoch 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 Typisierung (!) 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 so oft wie möglich automatischen Speicher verwenden, machen 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 ist, dass std::string definiert einen Kopierkonstruktor richtig. Betrachten Sie das folgende Programm:

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

Mit der ursprünglichen Version wird dieses Programm wahrscheinlich abstürzen, wie es verwendet delete zweimal auf derselben Saite. Verwenden Sie jeweils die modifizierte Version Line Instanz wird eine eigene Zeichenfolge besitzen Beispiel, jeder mit seinem eigenen Speicher und beide werden am Ende des Programms veröffentlicht.

Weitere Hinweise

Umfangreiche Nutzung von RAII wird aus allen oben genannten Gründen in C ++ als bewährte Methode betrachtet. 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 du das benutzt Line Klasse als Baustein:

 class Table
 {
      Line borders[4];
 };

Dann

 int main ()
 {
     Table table;
 }

teilt vier zu std::string Instanzen, vier Line Instanzen, eins Table Instanz und alle Inhalte der Zeichenfolge und alles wird automatisch befreit.


898
2018-06-28 00:47



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 zu sagen "Verwenden Sie den Stapel und nicht den Heap".


155
2018-06-28 00:14



Es ist kompliziert.

Zuerst wird C ++ nicht als Müll gesammelt. Daher muss für jedes neue ein entsprechender Löschvorgang durchgeführt werden. Wenn Sie diesen Löschvorgang nicht durchführen können, liegt ein Speicherverlust vor. Nun, für einen einfachen Fall wie diesen:

std::string *someString = new std::string(...);
//Do stuff
delete someString;

Das ist einfach. Aber was passiert, wenn "Do stuff" eine Ausnahme auslöst? Hoppla: Speicherleck. Was passiert, wenn Probleme auftreten? return früh? Hoppla: Speicherleck.

Und das ist für die einfachste Fall. Wenn Sie diese Zeichenfolge an jemanden zurückgeben, müssen Sie sie jetzt löschen. Und wenn sie es als Argument übergeben, muss die Person, die es erhält, es löschen? Wann sollten sie es löschen?

Oder Sie können das einfach tun:

std::string someString(...);
//Do stuff

Nein delete. Das Objekt wurde auf dem "Stapel" erstellt, und es wird zerstört, sobald es den Gültigkeitsbereich verlässt. Sie können das Objekt sogar zurückgeben und so seinen Inhalt an die aufrufende Funktion übergeben. Sie können das Objekt an Funktionen übergeben (normalerweise als Referenz oder als const-Verweis: void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis). Und so weiter.

Alles ohne new und delete. Es ist keine Frage, wem der Speicher gehört oder wer dafür verantwortlich ist. Wenn Sie tun:

std::string someString(...);
std::string otherString;
otherString = someString;

Es versteht sich, dass otherString hat eine Kopie der Daten von someString. Es ist kein Zeiger; es ist ein separates Objekt. Sie können denselben Inhalt haben, aber Sie können einen ändern, ohne den anderen zu beeinflussen:

someString += "More text.";
if(otherString == someString) { /*Will never get here */ }

Sehen Sie die Idee?


94
2018-06-28 00:17



Objekte erstellt von new muss irgendwann sein deletedamit 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 (z. B. 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 mögen auto_ptr, shared_ptr Lösen Sie das Dangling-Referenz-Problem, aber sie erfordern Kodierung Disziplin und haben andere Probleme (Kopierbarkeit, Referenz-Schleifen, etc.).

In stark multithreadbasierten Szenarien new ist ein Streitpunkt zwischen Threads; Es kann zu einer Überbeanspruchung der Leistung kommen 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.


65
2018-06-28 00:11



  • 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 verlassen wir uns auf einige allgemein zugängliche Zeiger, Stapelobjekte (maximal möglich) und Techniken wie auto_ptr (RAII-Objekte)
  • 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.

27
2018-06-28 02:59



Zu einem großen Teil ist das jemand, der seine eigenen Schwächen zu einer allgemeinen Regel erhebt. Da ist nichts falsch an sich mit dem Erstellen von Objekten mit dem new Operator. Es gibt ein Argument dafür, dass Sie dies mit einer gewissen Disziplin tun müssen: Wenn Sie ein Objekt erstellen, müssen Sie sicherstellen, dass es zerstört wird.

Der einfachste Weg, dies zu tun, ist das Objekt im automatischen Speicher zu erstellen, so dass C ++ weiß, es zu zerstören, wenn es den Gültigkeitsbereich verlässt:

 {
    File foo = File("foo.dat");

    // do things

 }

Beobachten Sie jetzt, dass, wenn Sie nach der Endklammer aus diesem Block fallen, foo ist außerhalb des Geltungsbereichs. C ++ ruft seinen dtor automatisch für Sie auf. Im Gegensatz zu Java müssen Sie nicht auf den GC warten, um es zu finden.

Hattest du geschrieben?

 {
     File * foo = new File("foo.dat");

Sie möchten es explizit mit übereinstimmen

     delete foo;
  }

oder noch besser, zuteilen Sie Ihre File * als "intelligenter Zeiger". Wenn Sie nicht vorsichtig sind, kann dies zu Undichtigkeiten führen.

Die Antwort selbst macht die falsche Annahme, dass wenn Sie nicht verwenden new Sie ordnen nicht auf dem Haufen zu; Tatsächlich wissen Sie das in C ++ nicht. Sie wissen höchstens, dass eine kleine Menge Speicher, z. B. ein Zeiger, sicher auf dem Stapel liegt. Überlegen Sie jedoch, ob die Implementierung von File etwas Ähnliches ist

  class File {
    private:
      FileImpl * fd;
    public:
      File(String fn){ fd = new FileImpl(fn);}

dann FileImpl werden immer noch auf dem Stapel zugewiesen werden.

Und ja, du solltest besser sicher sein

     ~File(){ delete fd ; }

in der Klasse auch; Ohne es werden Sie Speicher aus dem Heap verlieren, selbst wenn Sie es nicht getan haben anscheinend Allokieren auf dem Heap überhaupt.


17
2018-06-28 00:11



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

Berufung new Dies 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 vorhersagbare Ausführungszeit benötigt (wie in einem Echtzeit-System oder Spiel / Physik-Simulation) müssen Sie vermeiden new in deiner Zeit kritische Schleifen.

Operator new ist eine implizite Thread-Synchronisation

Ja, Sie haben gehört, Ihr Betriebssystem muss sicherstellen, dass Ihre Seitentabellen konsistent sind und als solche aufgerufen werden new wird dazu führen, dass Ihr Thread eine implizite Mutex-Sperre erhält. Wenn Sie ständig anrufen new aus vielen Threads serialisierst du eigentlich deine Threads (ich habe das mit 32 CPUs gemacht, die alle anschlagen new um ein paar hundert Bytes zu bekommen, autsch! das war eine königliche p.i.t.a. debuggen)

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


16
2018-02-12 17:57



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!


13
2018-06-28 00:10



new() sollte nicht als verwendet werden wenig wie möglich. Es sollte als verwendet werden vorsichtig wie möglich. 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, muss nicht verwendet werden new()mit dem zugehörigen delete() und Überprüfen von NULL-Zeigern. 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 Umfang hinausgehen muss, dann new() ist die richtige Antwort. Achte einfach darauf, dass du darauf achtest, wann und wie du anrufst delete() und die Möglichkeiten von NULL-Zeigern, die gelöschte Objekte und alle anderen Fehler verwenden, die mit Zeigern kommen.


13
2018-06-28 00:38