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

873

Ich stolperte über Stapelüberlauf Frage Speicherleck mit std :: string bei der Verwendung von std :: list <std :: string> , und einer der Kommentare sagt dieser:

Hör auf newso viel zu benutzen . Ich kann keinen Grund erkennen, warum Sie irgendwo neu verwendet haben. Sie können Objekte nach Wert in C ++ erstellen. Dies ist einer der großen Vorteile der Verwendung der Sprache.
Sie müssen nicht alles auf dem Heap zuordnen.
Hör auf, wie ein Java- Programmierer zu denken .

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 es intern?
Habe ich die Antwort falsch interpretiert?

Bitgarden
quelle

Antworten:

1037

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

Stapel

Der Stapel weist den Speicher immer sequentiell zu. Dies ist möglich, da 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, da es nur 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 (begrenzt durch {}) abgeschlossen ist, wird automatisch Speicher für alle Variablen in diesem Block gesammelt. Dies ist auch der Moment, in dem Destruktoren aufgerufen werden, um Ressourcen zu bereinigen.

Haufen

Der Heap ermöglicht einen flexibleren Speicherzuweisungsmodus. Die Buchhaltung ist komplexer und die Zuordnung ist langsamer. Da es keinen impliziten Freigabepunkt gibt, müssen Sie den Speicher manuell mit deleteoder delete[]( freein C) freigeben . Das Fehlen eines impliziten Freigabepunkts ist jedoch der Schlüssel zur Flexibilität des Heaps.

Gründe für die Verwendung der dynamischen Zuordnung

Selbst wenn die Verwendung des Heaps langsamer ist und möglicherweise zu Speicherverlusten oder Speicherfragmentierung führt, gibt es sehr gute Anwendungsfälle für die dynamische Zuordnung, da diese weniger begrenzt ist.

Zwei Hauptgründe für die Verwendung der dynamischen Zuordnung:

  • Sie wissen nicht, wie viel Speicher Sie zur Kompilierungszeit benötigen. Wenn Sie beispielsweise eine Textdatei in eine Zeichenfolge einlesen, wissen Sie normalerweise nicht, welche Größe die Datei hat. Daher können Sie erst entscheiden, wie viel Speicher zugewiesen werden soll, wenn Sie das Programm ausführen.

  • Sie möchten Speicher zuweisen, der nach dem Verlassen des aktuellen Blocks bestehen bleibt. Beispielsweise möchten Sie möglicherweise eine Funktion schreiben string readfile(string path), 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 beibehalten.

Warum eine dynamische Zuordnung oft nicht erforderlich ist

In C ++ gibt es ein ordentliches Konstrukt, das als Destruktor bezeichnet wird . Mit diesem Mechanismus können Sie Ressourcen verwalten, indem Sie die Lebensdauer der Ressource an der Lebensdauer einer Variablen ausrichten. Diese Technik heißt RAII und ist das Unterscheidungsmerkmal von C ++. Es "verpackt" Ressourcen in Objekte. std::stringist ein perfektes Beispiel. Dieser Ausschnitt:

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

weist tatsächlich eine variable Menge an Speicher zu. Das std::stringObjekt reserviert Speicher mithilfe des Heaps und gibt ihn in seinem Destruktor frei. In diesem Fall haben Sie nicht brauchen , um manuell alle Ressourcen verwalten und immer noch die Vorteile der dynamischen Speicherzuweisung bekam.

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 ist keine dynamische Speicherzuweisung erforderlich. Das Programm erfordert mehr Eingabe (!) Und birgt das Risiko, die Speicherfreigabe zu vergessen. Dies geschieht ohne offensichtlichen Nutzen.

Warum Sie die automatische Speicherung so oft wie möglich verwenden sollten

Grundsätzlich fasst der letzte Absatz es zusammen. Wenn Sie den automatischen Speicher so oft wie möglich verwenden, werden Ihre Programme:

  • schneller zu tippen;
  • schneller beim Laufen;
  • weniger anfällig für Speicher- / Ressourcenlecks.

Bonuspunkte

In der genannten 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 tatsächlich viel riskanter 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::stringein Kopierkonstruktor richtig definiert wird. Betrachten Sie das folgende Programm:

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

Bei Verwendung der Originalversion stürzt dieses Programm wahrscheinlich ab, da es deletezweimal dieselbe Zeichenfolge verwendet. Unter Verwendung der modifizierten Version, jede LineInstanz eine eigene Zeichenfolge besitzen Instanz , jedes mit seinem eigenen Speicher und beide werden am Ende des Programms freigegeben.

Weitere Hinweise

Die umfassende Verwendung von RAII wird aus all den oben genannten Gründen als bewährte Methode in C ++ angesehen. Es gibt jedoch einen zusätzlichen Vorteil, der nicht sofort offensichtlich ist. Grundsätzlich ist es besser als die Summe seiner Teile. Der ganze Mechanismus setzt sich zusammen . Es skaliert.

Wenn Sie die LineKlasse als Baustein verwenden:

 class Table
 {
      Line borders[4];
 };

Dann

 int main ()
 {
     Table table;
 }

Ordnet vier std::stringInstanzen, vier LineInstanzen, eine TableInstanz und den gesamten Inhalt der Zeichenfolge zu, und alles wird automatisch freigegeben .

André Caron
quelle
55
+1 für die Erwähnung von RAII am Ende, aber es sollte etwas über Ausnahmen und das Abwickeln von Stapeln geben.
Tobu
7
@Tobu: Ja, aber dieser Beitrag ist schon ziemlich lang und ich wollte ihn eher auf die Frage von OP konzentrieren. Am Ende schreibe ich einen Blog-Beitrag oder so und verlinke von hier aus darauf.
André Caron
15
Wäre eine großartige Ergänzung, um den Nachteil der Stapelzuweisung zu erwähnen (zumindest bis C ++ 1x) - Sie müssen häufig unnötig kopieren, wenn Sie nicht vorsichtig sind. zB Monsterspuckt ein a aus Treasure, Worldwenn es stirbt. In seiner Die()Methode fügt es der Welt den Schatz hinzu. Es muss world->Add(new Treasure(/*...*/))in anderen verwendet werden, um den Schatz zu bewahren, nachdem er gestorben ist. Die Alternativen sind shared_ptr(möglicherweise übertrieben) auto_ptr(schlechte Semantik für die Übertragung des Eigentums), Wertübergabe (verschwenderisch) und move+ unique_ptr(noch nicht weit verbreitet).
kizzx2
7
Was Sie über vom Stapel zugewiesene lokale Variablen gesagt haben, kann etwas irreführend sein. "Der Stapel" bezieht sich auf den Aufrufstapel, in dem Stapelrahmen gespeichert sind . Es sind diese Stapelrahmen, die in LIFO-Manier gespeichert werden. Die lokalen Variablen für einen bestimmten Frame werden so zugewiesen, als wären sie Mitglieder einer Struktur.
Irgendwann
7
@ Someguy: In der Tat ist die Erklärung nicht perfekt. Die Umsetzung hat Freiheit in ihrer Zuteilungspolitik. Die Variablen müssen jedoch auf LIFO-Weise initialisiert und zerstört werden, sodass die Analogie gilt. Ich denke nicht, dass es Arbeit ist, die die Antwort weiter verkompliziert.
André Caron
171

Weil der Stapel schneller und dicht ist

In C ++ ist nur eine einzige Anweisung erforderlich, um Speicherplatz auf dem Stapel für jedes lokale Bereichsobjekt in einer bestimmten Funktion zuzuweisen, und es ist unmöglich, einen dieser Speicher zu verlieren. Dieser Kommentar wollte (oder hätte beabsichtigen sollen) etwas wie "benutze den Stapel und nicht den Haufen" sagen .

DigitalRoss
quelle
18
"Es braucht nur eine einzige Anweisung, um Platz zuzuweisen" - oh, Unsinn. Sicher, es ist nur eine Anweisung erforderlich, um den Stapelzeiger hinzuzufügen, aber wenn die Klasse eine interessante interne Struktur hat, gibt es viel mehr als das Hinzufügen zum Stapelzeiger. Es ist ebenso gültig zu sagen, dass in Java keine Anweisungen zum Zuweisen von Speicherplatz erforderlich sind, da der Compiler die Referenzen zur Kompilierungszeit verwaltet.
Charlie Martin
31
@ Charlie ist richtig. Automatische Variablen sind schnell und kinderleicht wären genauer.
Oliver Charlesworth
28
@Charlie: Die Klasseneinbauten müssen so oder so eingerichtet werden. Der Vergleich wird bei der Zuweisung des benötigten Speicherplatzes durchgeführt.
Oliver Charlesworth
51
Husten int x; return &x;
Peterchen
16
schnell ja. Aber sicher nicht kinderleicht. Nichts ist kinderleicht. Sie können einen
StackOverflow
107

Der Grund dafür ist kompliziert.

Erstens wird in C ++ kein Müll gesammelt. Daher muss für jedes neue ein entsprechendes Löschen vorhanden sein. Wenn Sie diesen Löschvorgang nicht ausführen, liegt ein Speicherverlust vor. Nun zu einem einfachen Fall wie diesem:

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

Das ist einfach. Aber was passiert, wenn "Do stuff" eine Ausnahme auslöst? Ups: Speicherverlust. Was passiert, wenn "Do stuff" returnfrühzeitig auftritt ? Ups: Speicherverlust.

Und das ist für den einfachsten Fall . Wenn Sie diese Zeichenfolge an jemanden zurückgeben, muss dieser 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 einfach Folgendes tun:

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

Nein delete. Das Objekt wurde auf dem "Stapel" erstellt und 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 übertragen. Sie können das Objekt an Funktionen übergeben (normalerweise als Referenz oder Konstantenreferenz : void SomeFunc(std::string &iCanModifyThis, const std::string &iCantModifyThis). Und so weiter.

Alles ohne newund delete. Es ist keine Frage, wem der Speicher gehört oder wer für das Löschen verantwortlich ist. Wenn Sie tun:

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

Es versteht sich, dass otherStringeine Kopie der Daten von hat someString. Es ist kein Zeiger; es ist ein separates Objekt. Sie haben möglicherweise denselben Inhalt, 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?

Nicol Bolas
quelle
1
In diesem Sinne ... Wenn ein Objekt dynamisch zugewiesen wird main(), für die Dauer des Programms vorhanden ist, aufgrund der Situation nicht einfach auf dem Stapel erstellt werden kann und Zeiger darauf an alle Funktionen übergeben werden, die Zugriff darauf benötigen Kann dies bei einem Programmabsturz zu einem Leck führen oder ist es sicher? Ich würde letzteres annehmen, da das Betriebssystem, das den gesamten Speicher des Programms freigibt, es auch logisch freigeben sollte, aber ich möchte nichts annehmen, wenn es darum geht new.
Justin Time - Stellen Sie Monica
4
@JustinTime Sie müssen sich nicht darum kümmern, Speicher für dynamisch zugewiesene Objekte freizugeben, die für die gesamte Lebensdauer des Programms erhalten bleiben sollen. Wenn ein Programm ausgeführt wird, erstellt das Betriebssystem einen Atlas des physischen Speichers oder des virtuellen Speichers. Jede Adresse im virtuellen Speicherbereich wird einer Adresse des physischen Speichers zugeordnet. Wenn das Programm beendet wird, wird alles freigegeben, was dem virtuellen Speicher zugeordnet ist. Solange das Programm vollständig beendet wird, müssen Sie sich keine Sorgen machen, dass der zugewiesene Speicher niemals gelöscht wird.
Aiman ​​Al-Eryani
75

Objekte, die von erstellt wurden, newmüssen möglicherweise entfernt werden, deletedamit sie nicht auslaufen. Der Destruktor wird nicht aufgerufen, der Speicher wird nicht freigegeben, das ganze Bit. Da C ++ keine Garbage Collection hat, ist dies ein Problem.

Durch Wert erstellte Objekte (dh auf Stapel) sterben automatisch ab, wenn sie den Gültigkeitsbereich verlassen. Der Destruktoraufruf wird vom Compiler eingefügt und der Speicher wird bei Funktionsrückgabe automatisch freigegeben.

Intelligente Zeiger mögen unique_ptr, shared_ptrlösen das baumelnde Referenz Problem, aber sie erfordern Codierung Disziplin und haben andere potenzielle Probleme (Kopierbarkeit, Referenz Schleifen, etc.).

In stark multithreaded Szenarien gibt newes auch einen Streitpunkt zwischen Threads. Eine Überbeanspruchung kann sich auf die Leistung auswirken new. Die Erstellung von Stapelobjekten erfolgt per Definition threadlokal, da jeder Thread seinen eigenen Stapel hat.

Der Nachteil von Wertobjekten besteht darin, dass sie nach der Rückkehr der Hostfunktion sterben. Sie können dem Aufrufer keinen Verweis auf diese Objekte zurückgeben, indem Sie sie nur kopieren, zurückgeben oder nach Wert verschieben.

Seva Alekseyev
quelle
9
+1. Betreff "Objekte, die von erstellt wurden, newmüssen möglicherweise entfernt werden, deletedamit sie nicht auslaufen." - Schlimmer noch, new[]muss mit übereinstimmen delete[], und Sie erhalten undefiniertes Verhalten, wenn Sie delete new[]Speicher oder delete[] newSpeicher haben - nur sehr wenige Compiler warnen davor (einige Tools wie Cppcheck tun dies, wenn sie können).
Tony Delroy
3
@ TonyDelroy Es gibt Situationen, in denen der Compiler dies nicht warnen kann. Wenn eine Funktion einen Zeiger zurückgibt, kann dieser erstellt werden, wenn er neu (ein einzelnes Element) oder neu [] ist.
fbafelipe
32
  • C ++ verwendet keinen eigenen Speichermanager. In anderen Sprachen wie C # verfügt Java über einen Garbage Collector, der den Speicher verwaltet
  • C ++ - Implementierungen verwenden normalerweise Betriebssystemroutinen, um den Speicher zuzuweisen, und zu viel Neu / Löschen kann den verfügbaren Speicher fragmentieren
  • Wenn bei einer Anwendung der Speicher häufig verwendet wird, ist es ratsam, ihn vorab zuzuweisen und freizugeben, wenn er nicht benötigt wird.
  • Eine unsachgemäße Speicherverwaltung kann zu Speicherverlusten führen und ist sehr schwer nachzuverfolgen. Die Verwendung von Stapelobjekten im Funktionsumfang ist daher eine bewährte Technik
  • Der Nachteil bei der Verwendung von Stapelobjekten besteht darin, dass beim Zurückgeben, Übergeben an Funktionen usw. mehrere Kopien von Objekten erstellt werden. Intelligente Compiler sind sich dieser Situationen jedoch bewusst und wurden für die Leistung gut optimiert
  • In C ++ ist es sehr mühsam, wenn der Speicher an zwei verschiedenen Stellen zugewiesen und freigegeben wird. Die Verantwortung für die Freigabe ist immer eine Frage und meistens stützen 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, wenn wir eine nicht ordnungsgemäße Speicherverwaltung für die Anwendung verwenden. Die Abstürze aufgrund von Speicherbeschädigungen sind die schlimmsten und schwer zu verfolgenden.
Sarat
quelle
5
Tatsächlich verfügt jede Sprache, die Speicher zuweist, über einen Speichermanager, einschließlich c. Die meisten sind nur sehr einfach, dh int * x = malloc (4); int * y = malloc (4); ... beim ersten Aufruf wird Speicher zugewiesen, auch bekannt als os nach Speicher fragen (normalerweise in Blöcken 1k / 4k), sodass beim zweiten Aufruf nicht tatsächlich Speicher zugewiesen wird, sondern Sie einen Teil des letzten zugewiesenen Blocks erhalten. IMO, Garbage Collectors sind keine Speichermanager, da sie nur die automatische Freigabe des Speichers handhaben. Um als Speichermanager bezeichnet zu werden, sollte er nicht nur die Freigabe, sondern auch die Zuweisung von Speicher übernehmen.
Rahly
1
Lokale Variablen verwenden den Stapel, sodass der Compiler keinen Aufruf an malloc()oder seine Freunde sendet, um den erforderlichen Speicher zuzuweisen. Der Stapel kann jedoch kein Element innerhalb des Stapels freigeben. Die einzige Möglichkeit, wie der Stapelspeicher jemals freigegeben wird, besteht darin, sich von der Oberseite des Stapels abzuwickeln.
Mikko Rantalainen
C ++ "verwendet keine Betriebssystemroutinen"; Das ist nicht Teil der Sprache, es ist nur eine übliche Implementierung. C ++ läuft möglicherweise sogar ohne Betriebssystem.
Einpoklum
22

Ich sehe, dass einige wichtige Gründe, so wenig Neues wie möglich zu machen, übersehen werden:

Der Operator newhat eine nicht deterministische Ausführungszeit

Das Aufrufen newkann dazu führen, dass das Betriebssystem Ihrem Prozess eine neue physische Seite zuweist oder nicht. Dies kann sehr langsam sein, wenn Sie dies häufig tun. Oder es ist bereits ein geeigneter Speicherort bereit, wir wissen es nicht. Wenn Ihr Programm eine konsistente und vorhersehbare Ausführungszeit haben muss (wie in einem Echtzeitsystem oder einer Spiel- / Physiksimulation), müssen Sie newin Ihrer Zeit kritische Schleifen vermeiden .

Der Operator newist eine implizite Thread-Synchronisation

Ja, Sie haben mich gehört, Ihr Betriebssystem muss sicherstellen, dass Ihre Seitentabellen konsistent sind. Daher führt ein Aufruf newdazu, dass Ihr Thread eine implizite Mutex-Sperre erhält. Wenn Sie ständig newvon vielen Threads aus aufrufen, serialisieren Sie tatsächlich Ihre Threads (ich habe dies mit 32 CPUs getan, von denen jede newein paar hundert Bytes abruft, autsch! Das war eine königliche Pita zum Debuggen).

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

Emily L.
quelle
2
Beides kann vermieden werden, indem die Platzierung neu / gelöscht und der Speicher vorab zugewiesen wird. Oder Sie können den Speicher selbst zuweisen / freigeben und dann den Konstruktor / Destruktor aufrufen. So funktioniert std :: vector normalerweise.
Rxantos
1
@rxantos Bitte lesen Sie OP. Bei dieser Frage geht es darum, unnötige Speicherzuweisungen zu vermeiden. Es gibt auch keine Platzierung löschen.
Emily L.
@ Emily Dies ist, was das OP bedeutete, denke ich:void * someAddress = ...; delete (T*)someAddress
xryl669
1
Die Verwendung des Stacks ist auch in der Ausführungszeit nicht deterministisch. Es sei denn, Sie haben angerufen mlock()oder etwas Ähnliches. Dies liegt daran, dass dem System möglicherweise der Arbeitsspeicher ausgeht und keine verfügbaren physischen Speicherseiten für den Stapel verfügbar sind. Daher muss das Betriebssystem möglicherweise einige Caches (Clear Dirty Memory) austauschen oder auf die Festplatte schreiben, bevor die Ausführung fortgesetzt werden kann.
Mikko Rantalainen
1
@mikkorantalainen Das ist technisch gesehen richtig, aber in einer Situation mit wenig Arbeitsspeicher sind alle Wetten für die Leistung ungültig, wenn Sie auf die Festplatte pushen, sodass Sie nichts tun können. Es macht den Rat ohnehin nicht ungültig, neue Anrufe zu vermeiden, wenn dies angemessen ist.
Emily L.
21

Pre-C ++ 17:

Weil es zu subtilen Lecks neigt, selbst wenn Sie das Ergebnis in einen intelligenten Zeiger einschließen .

Stellen Sie sich einen "vorsichtigen" Benutzer vor, der sich daran erinnert, Objekte in intelligente Zeiger zu verpacken:

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

Dieser Code ist gefährlich , weil es gibt keine Garantie , dass entweder shared_ptraufgebaut ist , bevor entweder T1oder T2. Wenn einer der beiden erfolgreich ist new T1()oder new T2()nach dem anderen fehlschlägt, wird das erste Objekt durchgesickert, da kein Objekt shared_ptrvorhanden ist, das es zerstören und freigeben kann.

Lösung: verwenden make_shared.

Post-C ++ 17:

Dies ist kein Problem mehr: C ++ 17 legt eine Einschränkung für die Reihenfolge dieser Operationen fest. In diesem Fall wird sichergestellt, dass auf jeden Aufruf von new()sofort die Konstruktion des entsprechenden intelligenten Zeigers folgen muss, ohne dass eine andere Operation dazwischen liegt. Dies bedeutet, dass zum Zeitpunkt des new()Aufrufs des zweiten Objekts garantiert ist, dass das erste Objekt bereits in seinen intelligenten Zeiger eingeschlossen wurde, wodurch Lecks im Falle einer Ausnahme verhindert werden.

Eine detailliertere Erläuterung der neuen Bewertungsreihenfolge, die durch C ++ 17 eingeführt wurde, wurde von Barry in einer anderen Antwort bereitgestellt .

Vielen Dank an @Remy Lebeau für den Hinweis, dass dies unter C ++ 17 immer noch ein Problem ist (wenn auch weniger): Der shared_ptrKonstruktor kann seinen Steuerblock und seinen Wurf möglicherweise nicht zuordnen. In diesem Fall wird der an ihn übergebene Zeiger nicht gelöscht.

Lösung: verwenden make_shared.

user541686
quelle
5
Andere Lösung: Ordnen Sie niemals dynamisch mehr als ein Objekt pro Zeile zu.
Antimon
3
@Antimony: Ja, es ist viel verlockender, mehr als ein Objekt zuzuweisen, wenn Sie bereits eines zugewiesen haben, als wenn Sie keines zugewiesen haben.
user541686
1
Ich denke, eine bessere Antwort ist, dass der smart_ptr leckt, wenn eine Ausnahme aufgerufen wird und nichts sie fängt.
Natalie Adams
2
Selbst im Fall nach C ++ 17 kann ein Leck auftreten, wenn dies newerfolgreich ist und die nachfolgende shared_ptrKonstruktion fehlschlägt. std::make_shared()würde das auch lösen
Remy Lebeau
1
@Mehrdad Der betreffende shared_ptrKonstruktor reserviert Speicher für einen Steuerblock, in dem der gemeinsam genutzte Zeiger gespeichert und gelöscht wird. Ja, theoretisch kann ein Speicherfehler ausgelöst werden. Nur die Konstruktoren zum Kopieren, Verschieben und Aliasing werden nicht ausgelöst. make_sharedordnet das gemeinsam genutzte Objekt innerhalb des Steuerblocks selbst zu, so dass es nur 1 statt 2 gibt.
Remy Lebeau
17

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

Der einfachste Weg, dies zu tun, besteht darin, das Objekt im automatischen Speicher zu erstellen, damit C ++ es zerstören kann, wenn es außerhalb des Gültigkeitsbereichs liegt:

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

    // do things

 }

Beachten Sie nun, dass der Fall fooaußerhalb des Bereichs liegt , wenn Sie nach der Endstrebe von diesem Block fallen . C ++ ruft seinen dtor automatisch für Sie auf. Im Gegensatz zu Java müssen Sie nicht warten, bis der GC ihn gefunden hat.

Hattest du geschrieben?

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

Sie möchten es explizit mit abgleichen

     delete foo;
  }

oder noch besser, weisen Sie Ihren File *als "intelligenten Zeiger" zu. Wenn Sie nicht vorsichtig sind, kann dies zu Undichtigkeiten führen.

Die Antwort selbst macht die falsche Annahme, dass Sie, wenn Sie nicht verwenden new, nicht auf dem Heap zuordnen; In C ++ wissen Sie das nicht. Sie wissen höchstens, dass eine kleine Menge an Speicher, beispielsweise ein Zeiger, auf dem Stapel zugeordnet ist. Überlegen Sie jedoch, ob die Implementierung von File so etwas wie ist

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

dann FileImplwird noch auf dem Stapel zugeordnet.

Und ja, Sie sollten es besser haben

     ~File(){ delete fd ; }

auch in der Klasse; Ohne sie verlieren Sie Speicher aus dem Heap, auch wenn Sie den Heap anscheinend überhaupt nicht zugeordnet haben.

Charlie Martin
quelle
4
Sie sollten sich den Code in der Frage ansehen, auf die verwiesen wird. In diesem Code laufen definitiv viele Dinge schief.
André Caron
7
Ich bin damit einverstanden, dass die Verwendung new per se nichts Falsches ist , aber wenn Sie sich den Originalcode ansehen, auf den sich der Kommentar bezog, newwird er missbraucht. Der Code ist wie Java oder C # geschrieben, wo newer für praktisch jede Variable verwendet wird, wenn es viel sinnvoller ist, sich auf dem Stapel zu befinden.
Luke
5
Gutes Argument. Allgemeine Regeln werden jedoch normalerweise durchgesetzt, um häufige Fallstricke zu vermeiden. Unabhängig davon, ob dies eine Schwäche des Einzelnen war oder nicht, ist die Speicherverwaltung komplex genug, um eine allgemeine Regel wie diese zu rechtfertigen! :)
Robben_Ford_Fan_boy
9
@Charlie: Der Kommentar sagt nicht, dass Sie nie verwenden sollten new. Er sagt , dass , wenn Sie haben die Wahl zwischen dem dynamischen Zuordnung und automatischer Speicherung, automatische Speicherung verwenden.
André Caron
8
@Charlie: Es ist nichts falsch mit der Verwendung new, aber wenn Sie verwenden delete, machen Sie es falsch!
Matthieu M.
16

new()sollte nicht so wenig wie möglich verwendet werden. Es sollte so vorsichtig wie möglich verwendet werden. Und es sollte so oft verwendet werden, wie es der Pragmatismus vorschreibt.

Die Zuordnung von Objekten auf dem Stapel unter Berücksichtigung ihrer impliziten Zerstörung ist ein einfaches Modell. Wenn der erforderliche Bereich eines Objekts zu diesem Modell passt, muss es nicht verwendet werden new(), da delete()NULL-Zeiger zugeordnet und überprüft werden. In dem Fall, in dem Sie viele kurzlebige Objekte auf dem Stapel haben, sollten die Probleme der Heap-Fragmentierung verringert werden.

Wenn die Lebensdauer Ihres Objekts jedoch über den aktuellen Bereich hinausgehen muss, new()ist dies die richtige Antwort. delete()Stellen Sie einfach sicher, dass Sie darauf achten, wann und wie Sie aufrufen und welche Möglichkeiten NULL-Zeiger bieten. Verwenden Sie dazu gelöschte Objekte und alle anderen Fallstricke, die mit der Verwendung von Zeigern einhergehen.

Andrew Edgecombe
quelle
9
"Wenn die Lebensdauer Ihres Objekts über den aktuellen Bereich hinausgehen muss, ist new () die richtige Antwort." ... warum nicht bevorzugt nach Wert zurückkehren oder eine Variable mit Aufruferbereich durch Nicht-Referenz constoder Zeiger akzeptieren ...?
Tony Delroy
2
@ Tony: Ja, ja! Ich bin froh zu hören, dass jemand Referenzen befürwortet. Sie wurden erstellt, um dieses Problem zu verhindern.
Nathan Osman
@TonyD ... oder kombinieren Sie sie: Geben Sie einen intelligenten Zeiger nach Wert zurück. Auf diese Weise muss der Anrufer und in vielen Fällen (dh wo make_shared/_uniqueist verwendbar) der Angerufene niemals newoder delete. Diese Antwort verfehlt die eigentlichen Punkte: (A) C ++ bietet Dinge wie RVO, Verschiebungssemantik und Ausgabeparameter - was häufig bedeutet, dass die Behandlung von Objekterstellung und Lebensdauerverlängerung durch Rückgabe von dynamisch zugewiesenem Speicher unnötig und nachlässig wird. (B) Selbst in Situationen, in denen eine dynamische Zuordnung erforderlich ist, bietet die stdlib RAII-Wrapper, die den Benutzer von den hässlichen inneren Details befreien.
underscore_d
14

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

Class var;

es wird auf den Stapel gelegt.

Sie müssen immer destroy für das Objekt aufrufen, das Sie mit new auf den Heap gelegt haben. Dies eröffnet die Möglichkeit von Speicherlecks. Auf dem Stapel platzierte Objekte sind nicht anfällig für Speicherverluste!

Tim
quelle
2
+1 "[Heap] wird normalerweise verwendet, wenn Sie eine Erweiterung erwarten" - wie das Anhängen an einen std::stringoder std::map, ja, scharfen Einblick. Meine anfängliche Reaktion war "aber auch sehr häufig, um die Lebensdauer eines Objekts vom Gültigkeitsbereich des Erstellungscodes zu entkoppeln", aber es constist besser , wirklich nach Wert zurückzukehren oder aufruferbezogene Werte durch Nichtreferenz oder Zeiger zu akzeptieren , außer wenn es sich um eine "Erweiterung" handelt zu. Es gibt einige andere Sound-Anwendungen wie Factory-Methoden ...
Tony Delroy
12

Ein bemerkenswerter Grund, um eine Überbeanspruchung des Heapspeichers zu vermeiden, ist die Leistung - insbesondere die Leistung des von C ++ verwendeten Standard-Speicherverwaltungsmechanismus. Während die Zuweisung im trivialen Fall recht schnell sein kann, führt das Ausführen vieler newund deleteungleichmäßiger Objekte ohne strenge Reihenfolge nicht nur zu einer Speicherfragmentierung, sondern kompliziert auch den Zuweisungsalgorithmus und kann in bestimmten Fällen die Leistung absolut zerstören.

Dies ist das Problem, für dessen Lösung Speicherpools erstellt wurden, um die inhärenten Nachteile herkömmlicher Heap-Implementierungen zu verringern und den Heap bei Bedarf weiterhin zu verwenden.

Besser noch, um das Problem insgesamt zu vermeiden. Wenn Sie es auf den Stapel legen können, tun Sie dies.

tylerl
quelle
Sie können jederzeit eine relativ große Menge an Speicher zuweisen und dann die Platzierung neu / löschen verwenden, wenn die Geschwindigkeit ein Problem darstellt.
Rxantos
Speicherpools sollen eine Fragmentierung vermeiden, die Freigabe beschleunigen (eine Freigabe für Tausende von Objekten) und die Freigabe sicherer machen.
Lothar
10

Ich denke, das Plakat sollte You do not have to allocate everything on theheapeher sagen als das stack.

Grundsätzlich werden Objekte auf dem Stapel zugewiesen (sofern die Objektgröße dies zulässt), und zwar aufgrund der günstigen Kosten für die Stapelzuweisung anstelle einer Heap-basierten Zuweisung, die einige Arbeit des Zuweisers erfordert und die Ausführlichkeit erhöht, da dies erforderlich ist Verwalten der auf dem Heap zugewiesenen Daten.

Khaled Nassar
quelle
10

Ich neige dazu, der Idee, neues "zu viel" zu verwenden, nicht zuzustimmen. Obwohl die Verwendung von Neu mit Systemklassen durch das Originalplakat etwas lächerlich ist. ( int *i; i = new int[9999];? Wirklich? int i[9999];ist viel klarer.) Ich denke , das hat die Ziege des Kommentators bekommen.

Wenn Sie mit Systemobjekten arbeiten, benötigen Sie sehr selten mehr als einen Verweis auf genau dasselbe Objekt. Solange der Wert gleich ist, ist das alles, was zählt. Und Systemobjekte nehmen normalerweise nicht viel Speicherplatz ein. (ein Byte pro Zeichen in einer Zeichenfolge). In diesem Fall sollten die Bibliotheken so konzipiert sein, dass diese Speicherverwaltung berücksichtigt wird (sofern sie gut geschrieben sind). In diesen Fällen (alle bis auf ein oder zwei Nachrichten in seinem Code) ist Neu praktisch sinnlos und dient nur dazu, Verwirrung und Fehlerpotentiale zu verursachen.

Wenn Sie jedoch mit Ihren eigenen Klassen / Objekten arbeiten (z. B. der Line-Klasse des Originalplakats), müssen Sie selbst über Probleme wie Speicherbedarf, Persistenz von Daten usw. nachdenken. Zu diesem Zeitpunkt ist es von unschätzbarem Wert, mehrere Verweise auf denselben Wert zuzulassen. Dies ermöglicht Konstrukte wie verknüpfte Listen, Wörterbücher und Diagramme, bei denen mehrere Variablen nicht nur denselben Wert haben müssen, sondern genau dasselbe Objekt im Speicher referenzieren müssen . Die Line-Klasse hat jedoch keine dieser Anforderungen. Der Code des Originalplakats ist also absolut nicht erforderlich new.

Chris Hayes
quelle
Normalerweise wird das Neu / Löschen verwendet, wenn Sie die Größe des Arrays nicht vorher kennen. Natürlich versteckt std :: vector für Sie new / delete. Sie verwenden sie immer noch, aber durch std :: vector. Heutzutage wird es verwendet, wenn Sie die Größe des Arrays nicht kennen und aus irgendeinem Grund den Overhead von std :: vector vermeiden möchten (der klein ist, aber immer noch vorhanden ist).
Rxantos
When you're working with your own classes/objects... Sie haben oft keinen Grund dazu! Ein winziger Teil der Qs bezieht sich auf Details des Behälterdesigns durch erfahrene Codierer. Im krassen Gegensatz dazu ein deprimierender Anteil ist über Verwirrung der Neulinge , die nicht wissen , die stdlib existiert - oder aktiv schreckliche Zuweisungen in ‚Programmierung‘ ‚Kurse‘ gegeben, wo ein Lehrer fordert sie das Rad neu zu erfinden pointlessly - bevor sie haben sogar lernte, was ein Rad ist und warum es funktioniert. Durch die Förderung einer abstrakteren Zuordnung kann C ++ uns vor Cs endlosem "Segfault mit verknüpfter Liste" bewahren. Bitte, lass es uns .
underscore_d
" Die Verwendung von Neu mit Systemklassen durch das Originalplakat ist ein bisschen lächerlich. ( int *i; i = new int[9999];? Wirklich? int i[9999];ist viel klarer.)" Ja, es ist klarer, aber um Devil's Advocate zu spielen, ist der Typ nicht unbedingt ein schlechtes Argument. Mit 9999 Elementen kann ich mir ein eng eingebettetes System vorstellen, das nicht genügend Stapel für 9999 Elemente hat: 9999 x 4 Bytes sind ~ 40 kB, x 8 ~ 80 kB. Daher müssen solche Systeme möglicherweise eine dynamische Zuordnung verwenden, vorausgesetzt, sie implementieren sie unter Verwendung eines alternativen Speichers. Dies könnte jedoch möglicherweise nur eine dynamische Zuordnung rechtfertigen, nicht jedoch new. a vectorwäre die eigentliche Lösung in diesem Fall
underscore_d
Stimmen Sie mit @underscore_d überein - das ist kein so gutes Beispiel. Ich würde meinem Stapel nicht einfach so 40.000 oder 80.000 Bytes hinzufügen. Ich würde sie wahrscheinlich auf dem Haufen zuordnen (mit std::make_unique<int[]>()natürlich).
Einpoklum
3

Zwei Gründe:

  1. In diesem Fall ist es unnötig. Sie machen Ihren Code unnötig komplizierter.
  2. Es reserviert Speicherplatz auf dem Heap und bedeutet, dass Sie sich später daran erinnern müssen, da deletedies sonst zu einem Speicherverlust führen kann.
Dan
quelle
2

newist das neue goto.

Erinnern Sie sich daran, warum gotoes so verleumdet ist: Während es ein leistungsfähiges, einfaches Tool zur Flusskontrolle ist, wurde es häufig auf unnötig komplizierte Weise verwendet, was es schwierig machte, Code zu befolgen. Darüber hinaus wurden die nützlichsten und am einfachsten zu lesenden Muster in strukturierten Programmieranweisungen (z. B. foroder while) codiert . Der ultimative Effekt ist, dass der Code, zu dem gotoder geeignete Weg ist, eher selten ist. Wenn Sie versucht sind zu schreiben goto, tun Sie die Dinge wahrscheinlich schlecht (es sei denn, Sie wissen wirklich , was Sie tun).

newist ähnlich - es wird oft verwendet, um Dinge unnötig kompliziert und schwerer lesbar zu machen, und die nützlichsten Verwendungsmuster, die codiert werden können, wurden in verschiedene Klassen codiert. Wenn Sie neue Verwendungsmuster verwenden müssen, für die es noch keine Standardklassen gibt, können Sie außerdem Ihre eigenen Klassen schreiben, die diese codieren!

Ich würde sogar behaupten , dass newist schlimmer als auf gotoGrund der Notwendigkeit, Paar newund deleteAussagen.

Wie goto, wenn Sie jemals daran gedacht , müssen Sie verwenden new, sind Sie wahrscheinlich die Dinge schlecht zu tun - vor allem , wenn Sie so außerhalb der Implementierung einer Klasse , deren Zweck im Leben tun , ist zu kapseln , was dynamische Zuweisungen Sie tun müssen.

klutt
quelle
Und ich würde hinzufügen: "Sie brauchen es einfach nicht".
Einpoklum
1

Der Hauptgrund ist, dass Objekte auf dem Heap immer schwieriger zu verwenden und zu verwalten sind als einfache Werte. Das Schreiben von Code, der einfach zu lesen und zu warten ist, hat für jeden ernsthaften Programmierer immer oberste Priorität.

Ein weiteres Szenario ist, dass die von uns verwendete Bibliothek eine Wertsemantik bietet und eine dynamische Zuordnung unnötig macht. Std::stringist ein gutes Beispiel.

Für objektorientierten Code ist es jedoch newein Muss , einen Zeiger zu verwenden , dh ihn zuvor zu erstellen. Um die Komplexität des Ressourcenmanagements zu vereinfachen, verfügen wir über Dutzende von Tools, um es so einfach wie möglich zu gestalten, z. B. intelligente Zeiger. Das objektbasierte Paradigma oder generische Paradigma nimmt eine Wertesemantik an und erfordert weniger oder gar kein new, genau wie auf den Postern an anderer Stelle angegeben.

Herkömmliche Entwurfsmuster, insbesondere die im GoF- Buch erwähnten , verwenden häufig new, da es sich um typischen OO-Code handelt.

bingfeng zhao
quelle
4
Dies ist eine miserable Antwort. For object oriented code, using a pointer [...] is a must: Unsinn . Wenn Sie 'OO' abwerten, indem Sie sich nur auf eine kleine Teilmenge beziehen, Polymorphismus - auch Unsinn: Referenzen funktionieren auch. [pointer] means use new to create it beforehand: besonders Unsinn : Referenzen oder Zeiger können auf automatisch zugewiesene Objekte verwendet und polymorph verwendet werden; schau mir . [typical OO code] use new a lot: vielleicht in einem alten Buch, aber wen interessiert das? Jeder vage moderne C ++ meidet new/ rohe Zeiger, wo immer dies möglich ist - und ist dadurch in keiner Weise weniger OO
underscore_d
1

Ein weiterer Punkt zu allen oben genannten richtigen Antworten: Es hängt davon ab, welche Art von Programmierung Sie durchführen. Kernel-Entwicklung zum Beispiel in Windows -> Der Stapel ist stark eingeschränkt und Sie können möglicherweise keine Seitenfehler wie im Benutzermodus akzeptieren.

In solchen Umgebungen werden neue oder C-ähnliche API-Aufrufe bevorzugt und sind sogar erforderlich.

Dies ist natürlich nur eine Ausnahme von der Regel.

Michael Chourdakis
quelle