Ich bin auf die Frage zum Stapelüberlauf gestoßen Speicherverlust mit std :: string bei Verwendung von std :: list <std :: string> und einer der Kommentare sagt Folgendes:
Verwenden Sie
new
nicht mehr so oft. Ich kann keinen Grund erkennen, warum Sie irgendwo Neues verwendet haben. Sie können in C++ Objekte nach Wert 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?
Es gibt zwei weit verbreitete Speicherzuweisungstechniken: automatische Zuweisung und dynamische Zuweisung. In der Regel gibt es für jeden Speicherbereich einen entsprechenden Bereich: den Stapel und den Heap.
Der Stack ordnet 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, weil 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 Gültigkeitsbereichs automatisch beansprucht wird. Sobald die Ausführung des aktuellen Codeblocks (mit {}
abgegrenzt) abgeschlossen ist, wird automatisch der Speicher für alle Variablen in diesem Block erfasst. Dies ist auch der Moment, in dem Destruktoren aufgerufen werden, um Ressourcen zu bereinigen.
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 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.
Auch wenn die Verwendung des Heaps langsamer ist und möglicherweise zu Speicherverlusten oder Speicherfragmentierung führt, gibt es durchaus 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 zum Zeitpunkt der Kompilierung benötigen. Wenn Sie beispielsweise eine Textdatei in eine Zeichenfolge einlesen, wissen Sie normalerweise nicht, welche Größe die Datei hat, und können daher nicht entscheiden, wie viel Speicher Sie zuweisen möchten, bis 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 string readfile(string path)
schreiben, die den Inhalt einer Datei zurückgibt. Selbst wenn der Stapel den gesamten Dateiinhalt enthalten könnte, könnten Sie in diesem Fall nicht von einer Funktion zurückkehren und den zugewiesenen Speicherblock behalten.
In C++ gibt es ein ordentliches Konstrukt namens Destruktor . 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::string
ist ein perfektes Beispiel. Dieser Ausschnitt:
int main ( int argc, char* argv[] )
{
std::string program(argv[0]);
}
reserviert tatsächlich eine variable Menge an Speicher. Das std::string
-Objekt reserviert Speicher unter Verwendung des Heaps und gibt ihn in seinem Destruktor frei. In diesem Fall mussten Sie keine Ressourcen manuell verwalten und konnten dennoch die Vorteile der dynamischen Speicherzuweisung nutzen.
Insbesondere impliziert dies, dass in diesem Snippet:
int main ( int argc, char* argv[] )
{
std::string * program = new std::string(argv[0]); // Bad!
delete program;
}
es gibt nicht benötigte dynamische Speicherzuordnung. Das Programm erfordert mehr Eingaben (!) Und birgt das Risiko, die Freigabe des Speichers zu vergessen. Dies geschieht ohne erkennbaren Nutzen.
Grundsätzlich fasst der letzte Absatz es zusammen. Wenn Sie die automatische Speicherung so oft wie möglich verwenden, werden Ihre Programme folgendermaßen ausgeführt:
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 dafür ist, dass std::string
einen Kopierkonstruktor ordnungsgemäß definiert. Betrachten Sie das folgende Programm:
int main ()
{
Line l1;
Line l2 = l1;
}
Bei Verwendung der Originalversion stürzt dieses Programm wahrscheinlich ab, da es zweimal delete
für dieselbe Zeichenfolge verwendet. Mit der geänderten Version besitzt jede Line
-Instanz eine eigene String-Instanz mit jeweils eigenem Speicher, und beide werden am Ende freigegeben des Programms.
Die weitgehende Verwendung von RAII wird aus den oben genannten Gründen als Best Practice in C++ angesehen. Es gibt jedoch einen zusätzlichen Vorteil, der nicht sofort offensichtlich ist. Im Grunde ist es besser als die Summe seiner Teile. Der gesamte Mechanismus setzt sich zusammen . Es skaliert.
Wenn Sie die Klasse Line
als Baustein verwenden:
class Table
{
Line borders[4];
};
Dann
int main ()
{
Table table;
}
reserviert vier std::string
Instanzen, vier Line
Instanzen, eine Table
Instanz und den gesamten Inhalt des Strings und alles wird automatisch freigegeben .
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, diesen Speicher zu verlieren. Dieser Kommentar wollte (oder hätte sagen sollen) wie "benutze den Stapel und nicht den Haufen".
Der Grund dafür ist kompliziert.
Erstens wird in C++ kein Müll gesammelt. Daher muss für jeden neuen ein entsprechender Löschvorgang durchgeführt werden. Wenn Sie diesen Löschvorgang nicht durchführen, liegt ein Speicherverlust vor. Nun, für einen einfachen Fall wie diesen:
std::string *someString = new std::string(...);
//Do stuff
delete someString;
Das ist ganz einfach. Aber was passiert, wenn "Do stuff" eine Ausnahme auslöst? Hoppla: Speicherverlust. Was passiert, wenn "Do stuff" return
frühzeitig auftritt? Hoppla: 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 empfängt, es löschen? Wann sollten sie es löschen?
Oder Sie können dies einfach 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 new
und delete
. Es ist keine Frage, wem das Gedächtnis gehört oder wer dafür verantwortlich ist, es zu löschen. Wenn Sie tun:
std::string someString(...);
std::string otherString;
otherString = someString;
Es versteht sich, dass otherString
eine Kopie der Daten von someString
hat. Es ist kein Zeiger; es ist ein separates Objekt. Möglicherweise haben sie 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?
Von new
erstellte Objekte müssen möglicherweise delete
sein, damit sie nicht auslaufen. Der Destruktor wird nicht aufgerufen, der Speicher wird nicht freigegeben, das ganze Stück. Da C++ keine Garbage Collection hat, ist dies ein Problem.
Durch Wert erzeugte Objekte (d. H. Auf dem Stapel) sterben automatisch, wenn sie den Gültigkeitsbereich verlassen. Der Destruktoraufruf wird vom Compiler eingefügt, und der Speicher wird bei Funktionsrückgabe automatisch freigegeben.
Intelligente Zeiger wie unique_ptr
, shared_ptr
lösen das schwebende Referenzproblem, erfordern jedoch Kodierungsdisziplin und weisen andere potenzielle Probleme auf (Kopierbarkeit, Referenzschleifen usw.).
In Szenarien mit vielen Multithreads ist new
ein Konfliktpunkt zwischen Threads. Die Leistung kann beeinträchtigt werden, wenn new
zu häufig verwendet wird. Die Erstellung von Stapelobjekten erfolgt per Definition thread-lokal, da jeder Thread einen eigenen Stapel hat.
Der Nachteil von Wertobjekten besteht darin, dass sie sterben, sobald die Host-Funktion zurückgegeben wird. Sie können dem Aufrufer keinen Verweis auf diese Objekte zurückgeben, sondern nur durch Kopieren, Zurückgeben oder Verschieben nach Wert.
Ich sehe, dass ein paar wichtige Gründe, um so wenig neue wie möglich zu machen, übersehen werden:
new
hat eine nicht deterministische AusführungszeitDas Aufrufen von new
kann dazu führen, dass das Betriebssystem Ihrem Prozess eine neue physische Seite zuweist. Dies kann recht langsam sein, wenn Sie dies häufig tun. Oder es ist bereits ein passender Speicherplatz verfügbar, den wir nicht kennen. Wenn Ihr Programm eine konsistente und vorhersehbare Ausführungszeit haben muss (wie in einem Echtzeitsystem oder einer Spiel-/Physiksimulation), müssen Sie new
in Ihren zeitkritischen Schleifen vermeiden.
new
ist eine implizite Thread-SynchronisationJa, Sie haben mich gehört, Ihr Betriebssystem muss sicherstellen, dass Ihre Seitentabellen konsistent sind. Wenn Sie new
aufrufen, erhält Ihr Thread eine implizite Mutex-Sperre. Wenn Sie konsequent new
von vielen Threads aufrufen, serialisieren Sie Ihre Threads tatsächlich (ich habe dies mit 32 CPUs durchgeführt, von denen jede auf new
trifft, um jeweils ein paar hundert Bytes zu erhalten, autsch! Royal Pita (zu debuggen)
Der Rest wie langsam, fragmentiert, fehleranfällig usw. wurde bereits in anderen Antworten erwähnt.
Betrachten Sie einen "vorsichtigen" Benutzer, der sich daran erinnert, Objekte in intelligente Zeiger einzufügen:
foo(shared_ptr<T1>(new T1()), shared_ptr<T2>(new T2()));
Dieser Code ist gefährlich, weil es keine Garantie gibt, dass entweder shared_ptr
konstruiert ist vor entweder T1
oder T2
. Wenn also eines von new T1()
oder new T2()
fehlschlägt, nachdem das andere erfolgreich war, wird das erste Objekt gelöscht, da kein shared_ptr
vorhanden ist, um es zu zerstören und die Zuordnung aufzuheben.
Lösung: Verwenden Sie make_shared
.
Dies ist kein Problem mehr: In C++ 17 ist die Reihenfolge dieser Operationen eingeschränkt. In diesem Fall muss sichergestellt werden, dass nach jedem Aufruf von new()
sofort der entsprechende Smart Pointer erstellt wird, ohne dass ein anderer erstellt wird Betrieb dazwischen. Dies impliziert, dass zum Zeitpunkt des zweiten Aufrufs von new()
garantiert ist, dass das erste Objekt bereits in seinem intelligenten Zeiger eingeschlossen wurde, sodass im Falle eines Ausnahmefalls keine Lecks mehr auftreten.
Eine detailliertere Erklärung der neuen Auswertungsreihenfolge, die mit C++ 17 eingeführt wurde, lieferte Barry in einer anderen Antwort .
Vielen Dank an @ Remy Lebea für den Hinweis, dass dies immer noch ein Problem unter C++ 17 ist (obwohl weniger): Der Konstruktor shared_ptr
kann seinen Steuerblock und Throw nicht zuordnen. In diesem Fall wird der übergebene Zeiger nicht gelöscht.
Lösung: Verwenden Sie make_shared
.
Zu einem großen Teil ist dies jemand, der seine eigenen Schwächen zu einer allgemeinen Regel erhebt. Es gibt nichts Falsches per se beim Erstellen von Objekten mit dem Operator new
. Es gibt ein Argument dafür, dass Sie dies mit etwas 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 den Gültigkeitsbereich verlässt:
{
File foo = File("foo.dat");
// do things
}
Beachten Sie nun, dass foo
auß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 auf den GC warten, um ihn zu finden.
Hatten Sie geschrieben
{
File * foo = new File("foo.dat");
sie möchten es explizit mit übereinstimmen
delete foo;
}
oder noch besser, weisen Sie Ihren File *
als "intelligenten Zeiger" zu. Wenn Sie nicht aufpassen, kann dies zu Undichtigkeiten führen.
Die Antwort selbst geht fälschlicherweise davon aus, dass Sie, wenn Sie new
nicht verwenden, den Haufen nicht zuordnen. In C++ wissen Sie das nicht. Bestenfalls wissen Sie, dass auf dem Stapel mit Sicherheit eine kleine Menge an Speicher, beispielsweise ein Zeiger, zugeordnet ist. Überlegen Sie jedoch, ob die Implementierung von File in etwa so ist
class File {
private:
FileImpl * fd;
public:
File(String fn){ fd = new FileImpl(fn);}
dann wird FileImpl
noch auf dem Stapel zugewiesen.
Und ja, du solltest es besser haben
~File(){ delete fd ; }
auch in der Klasse; Andernfalls verlieren Sie Speicherplatz aus dem Heap, auch wenn Sie nicht anscheinend auf dem Heap allokieren.
new()
sollte nicht als wenig 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 new()
nicht zusammen mit dem zugehörigen delete()
verwendet und NULL-Zeiger überprüft werden. In dem Fall, in dem Sie viele kurzlebige Objekte auf dem Stapel haben, sollten die Probleme der Heap-Fragmentierung reduziert werden.
Wenn die Lebensdauer Ihres Objekts jedoch über den aktuellen Bereich hinausgehen muss, ist new()
die richtige Antwort. Achten Sie nur darauf, wann und wie Sie delete()
aufrufen und welche Möglichkeiten NULL-Zeiger bieten. Verwenden Sie dabei gelöschte Objekte und alle anderen Fallstricke, die mit der Verwendung von Zeigern einhergehen.
Wenn Sie new verwenden, werden Objekte dem Heap zugewiesen. Es wird im Allgemeinen verwendet, wenn Sie mit einer Erweiterung rechnen. Wenn Sie ein Objekt wie deklarieren,
Class var;
es wird auf den Stapel gelegt.
Sie müssen das Objekt, das Sie mit new auf den Haufen gelegt haben, immer mit destroy anrufen. Dies eröffnet die Möglichkeit von Speicherlecks. Objekte, die auf dem Stapel abgelegt werden, sind nicht anfällig für Speicherverluste!
Ein wichtiger Grund, um eine Überlastung 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 einer Menge von new
und delete
für Objekte mit ungleichmäßiger Größe ohne strenge Reihenfolge nicht nur zu einer Speicherfragmentierung, sondern kompliziert auch den Zuweisungsalgorithmus und kann in bestimmten Fällen die Leistung absolut beeinträchtigen.
Das ist das Problem, für das Speicherpools erstellt wurde, um die inhärenten Nachteile herkömmlicher Heap-Implementierungen zu mindern und den Heap dennoch nach Bedarf zu verwenden.
Besser noch, um das Problem insgesamt zu vermeiden. Wenn Sie es auf den Stapel legen können, dann tun Sie dies.
Ich bin eher anderer Meinung als der Gedanke, "zu viel" Neues zu verwenden. Obwohl die ursprüngliche Verwendung von new durch Poster mit Systemklassen etwas lächerlich ist. (int *i; i = new int[9999];
? Wirklich? int i[9999];
ist viel klarer.) Ich denke das ist, was die Ziege des Kommentators bekommen hat.
Wenn Sie mit Systemobjekten arbeiten, ist es sehr selten, dass Sie mehr als einen Verweis auf dasselbe Objekt benötigen. Solange der Wert der gleiche ist, ist das alles was zählt. Und Systemobjekte belegen normalerweise nicht viel Speicherplatz. (ein Byte pro Zeichen in einer Zeichenfolge). In diesem Fall sollten die Bibliotheken so konzipiert sein, dass sie die Speicherverwaltung berücksichtigen (sofern sie gut geschrieben sind). In diesen Fällen (bis auf eine oder zwei Nachrichten in seinem Code) ist new praktisch sinnlos und führt nur zu Verwirrung und potenziellen Fehlern.
Wenn Sie jedoch mit Ihren eigenen Klassen/Objekten arbeiten (z. B. der Line-Klasse des ursprünglichen Posters), müssen Sie sich selbst Gedanken über Probleme wie den Speicherbedarf, die Persistenz von Daten usw. machen. Zu diesem Zeitpunkt ist es von unschätzbarem Wert, mehrere Verweise auf denselben Wert zuzulassen - dies ermöglicht Konstruktionen wie verknüpfte Listen, Wörterbücher und Diagramme, bei denen mehrere Variablen nicht nur denselben Wert haben müssen, sondern auf genau dasselbe verweisen müssen Objekt im Speicher. Die Line-Klasse hat jedoch keine dieser Anforderungen. Der Code des ursprünglichen Posters benötigt also überhaupt kein new
.
Ich denke, das Plakat sollte You do not have to allocate everything on the
heap
und nicht das stack
lauten.
Grundsätzlich werden Objekte auf dem Stapel zugewiesen (sofern die Objektgröße dies zulässt), da die Stapelzuweisung kostengünstig ist und keine heapbasierte Zuweisung erforderlich ist, da der Zuweiser viel Arbeit leistet auf dem Heap zugewiesene Daten verwalten.
Zwei Gründe:
delete
zu speichern, da sonst ein Speicherverlust auftritt.new
ist das neue goto
.
Erinnern Sie sich, warum goto
so verunglimpft ist: Obwohl es sich um ein leistungsfähiges Tool für die Flusskontrolle auf niedriger Ebene handelt, 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 codiert (z. B. for
oder while
); Der ultimative Effekt ist, dass der Code, in dem goto
der geeignete Weg ist, eher selten ist. Wenn Sie versucht sind, goto
zu schreiben, tun Sie die Dinge wahrscheinlich schlecht (es sei denn, Sie wirklich wissen was du tust).
new
ist ähnlich - es wird oft verwendet, um Dinge unnötig kompliziert und schwerer zu lesen, und die nützlichsten Verwendungsmuster können in verschiedene Klassen codiert werden. Wenn Sie neue Verwendungsmuster verwenden müssen, für die es noch keine Standardklassen gibt, können Sie Ihre eigenen Klassen schreiben, die sie codieren!
Ich würde sogar argumentieren, dass new
schlimmer ist als goto
, da new
und delete
Anweisungen gepaart werden müssen.
Wie bei goto
tun Sie Dinge wahrscheinlich schlecht, wenn Sie jemals glauben, new
verwenden zu müssen - insbesondere, wenn Sie dies außerhalb der Implementierung einer Klasse tun, deren Lebenszweck es ist, eine beliebige Dynamik zu kapseln Zuweisungen müssen Sie tun.
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 seriösen Programmierer immer die höchste Priorität.
Ein weiteres Szenario ist die von uns verwendete Bibliothek, die eine Wertesemantik bereitstellt und eine dynamische Zuordnung überflüssig macht. Std::string
ist ein gutes Beispiel.
Für objektorientierten Code ist es jedoch ein Muss, einen Zeiger zu verwenden, dh, Sie müssen new
verwenden, um ihn im Voraus zu erstellen. Um die Komplexität des Ressourcenmanagements zu vereinfachen, verfügen wir über Dutzende Tools, mit denen dies so einfach wie möglich gestaltet werden kann, z. B. intelligente Zeiger. Das objektbasierte Paradigma oder das generische Paradigma nimmt eine Wertesemantik an und erfordert weniger oder gar kein new
, so wie es die Plakate an anderer Stelle angegeben haben.
Traditionelle Entwurfsmuster, insbesondere die in GoF genannten, verwenden new
häufig, da es sich um typischen OO Code handelt.
Noch ein Punkt zu all den obigen korrekten Antworten, es hängt davon ab, welche Art von Programmierung Sie durchführen. Beispiel: Kernel-Entwicklung unter Windows -> Der Stack ist stark eingeschränkt und Sie können möglicherweise keine Seitenfehler wie im Benutzermodus verarbeiten.
In solchen Umgebungen werden neue oder C-ähnliche API-Aufrufe bevorzugt und sogar benötigt.
Dies ist natürlich nur eine Ausnahme von der Regel.