Wann sollte ich das neue Schlüsselwort in C ++ verwenden?

272

Ich benutze C ++ seit kurzer Zeit und habe mich über das neue Schlüsselwort gewundert. Soll ich es einfach benutzen oder nicht?

1) Mit dem neuen Schlüsselwort ...

MyClass* myClass = new MyClass();
myClass->MyField = "Hello world!";

2) Ohne das neue Schlüsselwort ...

MyClass myClass;
myClass.MyField = "Hello world!";

Aus Sicht der Implementierung scheinen sie nicht so unterschiedlich zu sein (aber ich bin mir sicher, dass sie es sind) ... Meine Hauptsprache ist jedoch C #, und natürlich ist die erste Methode das, was ich gewohnt bin.

Die Schwierigkeit scheint zu sein, dass Methode 1 mit den Standard-C ++ - Klassen schwieriger zu verwenden ist.

Welche Methode soll ich verwenden?

Update 1:

Ich habe kürzlich das neue Schlüsselwort für Heapspeicher (oder freien Speicher ) für ein großes Array verwendet, das außerhalb des Gültigkeitsbereichs lag (dh von einer Funktion zurückgegeben wurde). Wo ich zuvor den Stack verwendet habe, der dazu führte, dass die Hälfte der Elemente außerhalb des Gültigkeitsbereichs beschädigt wurde, stellte der Wechsel zur Heap-Nutzung sicher, dass die Elemente in Kontakt waren. Yay!

Update 2:

Ein Freund von mir hat mir kürzlich gesagt, dass es eine einfache Regel für die Verwendung des newSchlüsselworts gibt. Geben Sie jedes Mal ein new, wenn Sie tippen delete.

Foobar *foobar = new Foobar();
delete foobar; // TODO: Move this to the right place.

Dies hilft, Speicherlecks zu vermeiden, da Sie den Löschvorgang immer irgendwo ablegen müssen (dh wenn Sie ihn ausschneiden und entweder in einen Destruktor oder auf andere Weise einfügen).

Nick Bolton
quelle
6
Die kurze Antwort lautet: Verwenden Sie die Kurzversion, wenn Sie damit durchkommen können. :)
Jalf
11
Eine bessere Technik als immer das Schreiben eines entsprechenden Löschvorgangs - verwenden Sie STL-Container und intelligente Zeiger wie std::vectorund std::shared_ptr. Diese schließen die Anrufe an newund deletefür Sie ab, sodass die Wahrscheinlichkeit, dass Speicher verloren geht, noch geringer ist. Fragen Sie sich zum Beispiel: Denken Sie immer daran, deleteüberall dort, wo eine Ausnahme ausgelöst werden könnte , ein entsprechendes zu setzen ? Es deleteist schwieriger, s von Hand einzugeben, als Sie vielleicht denken.
AshleysBrain
@nbolton Re: UPDATE 1 - Eines der schönen Dinge an C ++ ist, dass Sie damit benutzerdefinierte Typen auf dem Stapel speichern können, während der gesammelte Müll wie C # Sie zwingt , die Daten auf dem Heap zu speichern . Das Speichern von Daten auf dem Heap verbraucht mehr Ressourcen als das Speichern von Daten auf dem Stapel . Daher sollten Sie den Stapel dem Heap vorziehen , es sei denn, Ihr UDT benötigt viel Speicher, um seine Daten zu speichern. (Dies bedeutet auch, dass Objekte standardmäßig als Wert übergeben werden.) Eine bessere Lösung für Ihr Problem wäre die Übergabe des Arrays an die Funktion als Referenz .
Charles Addis

Antworten:

303

Methode 1 (unter Verwendung new)

  • Ordnet Speicher für das Objekt im freien Speicher zu (Dies ist häufig dasselbe wie der Heap ).
  • Erfordert, dass Sie deleteIhr Objekt später explizit angeben. (Wenn Sie es nicht löschen, können Sie einen Speicherverlust verursachen.)
  • Speicher bleibt zugewiesen, bis Sie deletees. (dh Sie könnten returnein Objekt, das Sie mit erstellt haben new)
  • Das Beispiel in der Frage verliert Speicher, es sei denn, der Zeiger ist deleted; und es sollte immer gelöscht werden , unabhängig davon, welcher Steuerpfad verwendet wird oder ob Ausnahmen ausgelöst werden.

Methode 2 (nicht verwendet new)

  • Ordnet Speicher für das Objekt auf dem Stapel zu (wo alle lokalen Variablen gespeichert sind). Im Allgemeinen ist weniger Speicher für den Stapel verfügbar. Wenn Sie zu viele Objekte zuweisen, besteht die Gefahr eines Stapelüberlaufs.
  • Sie werden deletees später nicht brauchen .
  • Der Speicher wird nicht mehr zugewiesen, wenn der Gültigkeitsbereich überschritten wird. (dh Sie sollten keinen returnZeiger auf ein Objekt auf dem Stapel haben)

Soweit man verwendet; Sie wählen die Methode aus, die für Sie unter Berücksichtigung der oben genannten Einschränkungen am besten geeignet ist.

Einige einfache Fälle:

  • Wenn Sie sich keine Gedanken über Anrufe machen möchten delete(und möglicherweise Speicherlecks verursachen ), sollten Sie diese nicht verwenden new.
  • Wenn Sie von einer Funktion einen Zeiger auf Ihr Objekt zurückgeben möchten, müssen Sie verwenden new
Daniel LeCheminant
quelle
4
Ein Nitpick - Ich glaube, dass der neue Operator Speicher aus "Free Store" zuweist, während Malloc aus "Heap" zuweist. Es ist nicht garantiert, dass dies dasselbe ist, obwohl dies in der Praxis normalerweise der Fall ist. Siehe gotw.ca/gotw/009.htm .
Fred Larson
4
Ich denke, Ihre Antwort könnte klarer sein. (In 99% der
Fälle
4
@jalf: Methode 2 ist diejenige, die das Neue nicht verwendet: - / In jedem Fall wird Ihr Code oft viel einfacher (z. B. Behandlung von Fehlerfällen) mit Methode 2 (die ohne das Neue)
Daniel LeCheminant
Noch ein Trottel ... Sie sollten klarer machen, dass Nicks erstes Beispiel Speicher verliert, während sein zweites nicht, selbst angesichts von Ausnahmen.
Arafangion
4
@Fred, Arafangion: Danke für deinen Einblick; Ich habe Ihre Kommentare in die Antwort aufgenommen.
Daniel LeCheminant
118

Es gibt einen wichtigen Unterschied zwischen den beiden.

Alles, was nicht mit zugewiesen wurde, newverhält sich ähnlich wie Werttypen in C # (und die Leute sagen oft, dass diese Objekte auf dem Stapel zugewiesen sind, was wahrscheinlich der häufigste / offensichtlichste Fall ist, aber nicht immer wahr ist. Genauer gesagt, Objekte, die ohne Verwendung zugewiesen wurden, newhaben automatischen Speicher Dauer Alles, was mit zugeordnet newist, wird auf dem Heap zugewiesen, und ein Zeiger darauf wird zurückgegeben, genau wie bei Referenztypen in C #.

Alles, was auf dem Stapel zugewiesen ist, muss eine konstante Größe haben, die zur Kompilierungszeit festgelegt wird (der Compiler muss den Stapelzeiger korrekt setzen, oder wenn das Objekt Mitglied einer anderen Klasse ist, muss er die Größe dieser anderen Klasse anpassen). . Aus diesem Grund sind Arrays in C # Referenztypen. Sie müssen es sein, denn mit Referenztypen können wir zur Laufzeit entscheiden, wie viel Speicher benötigt werden soll. Gleiches gilt hier. Nur Arrays mit konstanter Größe (eine Größe, die zur Kompilierungszeit bestimmt werden kann) können mit automatischer Speicherdauer (auf dem Stapel) zugewiesen werden. Arrays mit dynamischer Größe müssen auf dem Heap durch Aufrufen zugewiesen werden new.

(Und hier hört jede Ähnlichkeit mit C # auf)

Jetzt hat alles, was auf dem Stapel zugewiesen ist, eine "automatische" Speicherdauer (Sie können eine Variable tatsächlich als deklarieren auto, dies ist jedoch die Standardeinstellung, wenn kein anderer Speichertyp angegeben ist, sodass das Schlüsselwort in der Praxis nicht wirklich verwendet wird, aber hier ist es kommt von)

Automatische Speicherdauer bedeutet genau, wie es sich anhört. Die Dauer der Variablen wird automatisch behandelt. Im Gegensatz dazu muss alles, was auf dem Heap zugewiesen ist, von Ihnen manuell gelöscht werden. Hier ist ein Beispiel:

void foo() {
  bar b;
  bar* b2 = new bar();
}

Diese Funktion erzeugt drei erwägenswerte Werte:

In Zeile 1 wird eine Variable bvom Typ barauf dem Stapel deklariert (automatische Dauer).

In Zeile 2 deklariert es einen barZeiger b2auf dem Stapel (automatische Dauer) und ruft new auf, wobei ein barObjekt auf dem Heap zugewiesen wird. (dynamische Dauer)

Wenn die Funktion zurückkehrt, geschieht Folgendes: Erstens b2wird der Gültigkeitsbereich verlassen (die Reihenfolge der Zerstörung ist immer entgegengesetzt zur Reihenfolge der Konstruktion). Ist b2aber nur ein Zeiger, so passiert nichts, der Speicher, den es belegt, wird einfach freigegeben. Und wichtig ist, dass der Speicher, auf den er verweist (die barInstanz auf dem Heap), NICHT berührt wird. Nur der Zeiger wird freigegeben, da nur der Zeiger eine automatische Dauer hatte. Zweitens bwird der Gültigkeitsbereich verlassen. Da es eine automatische Dauer hat, wird sein Destruktor aufgerufen und der Speicher wird freigegeben.

Und die barInstanz auf dem Haufen? Es ist wahrscheinlich immer noch da. Niemand hat sich die Mühe gemacht, es zu löschen, also haben wir Speicher verloren.

Aus diesem Beispiel können wir ersehen, dass bei allen Objekten mit automatischer Dauer der Destruktor garantiert aufgerufen wird, wenn der Gültigkeitsbereich überschritten wird. Das ist nützlich. Aber alles, was auf dem Heap zugewiesen ist, hält so lange an, wie wir es brauchen, und kann dynamisch dimensioniert werden, wie im Fall von Arrays. Das ist auch nützlich. Damit können wir unsere Speicherzuordnungen verwalten. Was ist, wenn die Foo-Klasse in ihrem Konstruktor Speicher auf dem Heap zugewiesen und diesen Speicher in ihrem Destruktor gelöscht hat? Dann könnten wir das Beste aus beiden Welten bekommen, sichere Speicherzuordnungen, die garantiert wieder freigegeben werden, aber ohne die Einschränkungen, alles auf den Stapel zu zwingen.

Und genau so funktioniert der meiste C ++ - Code. Schauen Sie sich std::vectorzum Beispiel die Standardbibliothek an . Dies wird normalerweise auf dem Stapel zugewiesen, kann jedoch dynamisch angepasst und in der Größe geändert werden. Dazu wird nach Bedarf intern Speicher auf dem Heap zugewiesen. Der Benutzer der Klasse sieht dies nie, daher besteht keine Möglichkeit, dass Speicher verloren geht oder Sie vergessen, die von Ihnen zugewiesenen Daten zu bereinigen.

Dieses Prinzip wird als RAII (Resource Acquisition is Initialization) bezeichnet und kann auf jede Ressource erweitert werden, die erworben und freigegeben werden muss. (Netzwerk-Sockets, Dateien, Datenbankverbindungen, Synchronisationssperren). Alle können im Konstruktor erworben und im Destruktor freigegeben werden, sodass Sie sicher sind, dass alle Ressourcen, die Sie erwerben, wieder freigegeben werden.

Verwenden Sie in der Regel niemals new / delete direkt aus Ihrem High-Level-Code. Wickeln Sie es immer in eine Klasse ein, die den Speicher für Sie verwalten kann und die sicherstellt, dass er wieder freigegeben wird. (Ja, es kann Ausnahmen von dieser Regel geben. Insbesondere bei intelligenten Zeigern müssen Sie newdirekt aufrufen und den Zeiger an seinen Konstruktor übergeben, der dann übernimmt und sicherstellt delete, dass er korrekt aufgerufen wird. Dies ist jedoch immer noch eine sehr wichtige Faustregel )

jalf
quelle
2
"Alles, was nicht mit new zugewiesen wurde, wird auf dem Stapel abgelegt." Nicht in den Systemen, an denen ich gearbeitet habe ... normalerweise werden initialisierte (und nicht initialisierte) globale (statische) Daten in ihren eigenen Segmenten abgelegt. Zum Beispiel .data, .bss usw. Linkersegmente. Pedantic, ich weiß ...
Dan
Natürlich hast du recht. Ich habe nicht wirklich über statische Daten nachgedacht. Mein schlechtes natürlich. :)
Jalf
2
Warum muss alles, was auf dem Stapel zugeordnet ist, eine konstante Größe haben?
user541686
Es gibt nicht immer einige Möglichkeiten, dies zu umgehen, aber im Allgemeinen ist dies der Fall, da es sich auf einem Stapel befindet. Wenn es sich oben im Stapel befindet, kann die Größe möglicherweise geändert werden. Sobald jedoch etwas anderes darauf geschoben wird, wird es "eingemauert" und von Objekten auf beiden Seiten umgeben, sodass die Größe nicht wirklich geändert werden kann . Ja, sagen , dass es immer hat hat eine feste Größe ein bisschen eine Vereinfachung, aber es vermittelt die Grundidee (und ich würde Flickschusterei mit den C - Funktionen nicht empfehlen , die Sie mit Stapelzuordnungen zu kreativ sein lassen)
JALF
14

Welche Methode soll ich verwenden?

Dies wird fast nie durch Ihre Eingabeeinstellungen bestimmt, sondern durch den Kontext. Wenn Sie das Objekt über einige Stapel hinweg aufbewahren müssen oder wenn es für den Stapel zu schwer ist, weisen Sie es dem freien Speicher zu. Da Sie ein Objekt zuweisen, sind Sie auch für die Freigabe des Speichers verantwortlich. Suchen Sie den deleteOperator.

Um die Verwendung von Free-Store-Management zu vereinfachen, haben die Leute Dinge wie auto_ptrund erfunden unique_ptr. Ich empfehle Ihnen dringend, sich diese anzuschauen. Sie könnten sogar bei Ihren Tippproblemen hilfreich sein ;-)

dirkgently
quelle
10

Wenn Sie in C ++ schreiben, schreiben Sie wahrscheinlich für die Leistung. Die Verwendung von new und free store ist viel langsamer als die Verwendung des Stacks (insbesondere bei Verwendung von Threads). Verwenden Sie ihn daher nur, wenn Sie ihn benötigen.

Wie andere bereits gesagt haben, benötigen Sie new, wenn Ihr Objekt außerhalb der Funktion oder des Objektbereichs leben muss, das Objekt sehr groß ist oder wenn Sie die Größe eines Arrays zur Kompilierungszeit nicht kennen.

Vermeiden Sie außerdem, jemals Löschen zu verwenden. Wickeln Sie Ihr neues stattdessen in einen intelligenten Zeiger. Lassen Sie den Smart-Pointer-Aufruf für Sie löschen.

In einigen Fällen ist ein intelligenter Zeiger nicht intelligent. Speichern Sie std :: auto_ptr <> niemals in einem STL-Container. Der Zeiger wird aufgrund von Kopiervorgängen im Container zu früh gelöscht. Ein anderer Fall ist, wenn Sie einen wirklich großen STL-Container mit Zeigern auf Objekte haben. boost :: shared_ptr <> hat eine Menge Geschwindigkeitsaufwand, da die Referenzzählungen nach oben und unten erhöht werden. In diesem Fall ist es besser, den STL-Container in ein anderes Objekt einzufügen und diesem Objekt einen Destruktor zu geben, der bei jedem Zeiger im Container delete aufruft.

Zan Lynx
quelle
10

Die kurze Antwort ist: wenn Sie ein Anfänger in C ++ sind, sollten Sie nie verwenden werden newoder deletesich selbst.

Verwenden Sie stattdessen intelligente Zeiger wie z std::unique_ptr und std::make_unique(oder seltener std::shared_ptrund std::make_shared) verwenden. Auf diese Weise müssen Sie sich nicht annähernd so viele Gedanken über Speicherlecks machen. Und selbst wenn Sie fortgeschrittener sind, besteht die beste Vorgehensweise normalerweise darin, die benutzerdefinierte Art newund Weise, die Sie verwenden, deletein eine kleine Klasse (z. B. einen benutzerdefinierten intelligenten Zeiger) zu kapseln, die sich ausschließlich mit Problemen des Objektlebenszyklus befasst.

Natürlich führen diese intelligenten Zeiger hinter den Kulissen immer noch eine dynamische Zuweisung und Freigabe durch, sodass der Code, der sie verwendet, immer noch den damit verbundenen Laufzeitaufwand hat. Andere Antworten hier haben diese Probleme behandelt und wie man Entwurfsentscheidungen darüber trifft, wann intelligente Zeiger verwendet werden sollen, anstatt nur Objekte auf dem Stapel zu erstellen oder sie als direkte Mitglieder eines Objekts einzubeziehen, gut genug, dass ich sie nicht wiederholen werde. Aber meine Zusammenfassung wäre: Verwenden Sie keine intelligenten Zeiger oder dynamische Zuordnung, bis Sie etwas dazu zwingt.

Daniel Schepler
quelle
interessant zu sehen, wie sich eine Antwort
Wolf
2

Ohne das newSchlüsselwort speichern Sie das auf dem Aufrufstapel . Das Speichern zu großer Variablen auf dem Stapel führt zu einem Stapelüberlauf .

vartec
quelle
2

Die einfache Antwort lautet Ja - new () erstellt ein Objekt auf dem Heap (mit dem unglücklichen Nebeneffekt, dass Sie seine Lebensdauer verwalten müssen (indem Sie explizit delete darauf aufrufen), während das zweite Formular ein Objekt im Stapel im aktuellen erstellt scope und dieses Objekt werden zerstört, wenn es den Gültigkeitsbereich verlässt.

Timo Geusch
quelle
1

Wenn Ihre Variable nur im Kontext einer einzelnen Funktion verwendet wird, ist es besser, eine Stapelvariable zu verwenden, dh Option 2. Wie andere bereits gesagt haben, müssen Sie die Lebensdauer von Stapelvariablen nicht verwalten - sie sind konstruiert und automatisch zerstört. Das Zuweisen / Freigeben einer Variablen auf dem Heap ist im Vergleich dazu langsam. Wenn Ihre Funktion häufig genug aufgerufen wird, werden Sie eine enorme Leistungsverbesserung feststellen, wenn Sie Stapelvariablen im Vergleich zu Heap-Variablen verwenden.

Es gibt jedoch einige offensichtliche Fälle, in denen Stapelvariablen nicht ausreichen.

Wenn die Stapelvariable einen großen Speicherbedarf hat, besteht die Gefahr, dass der Stapel überläuft. Standardmäßig beträgt die Stapelgröße jedes Threads 1 MB unter Windows . Es ist unwahrscheinlich, dass Sie eine Stapelvariable mit einer Größe von 1 MB erstellen. Sie müssen jedoch berücksichtigen, dass die Stapelauslastung kumulativ ist. Wenn Ihre Funktion eine Funktion aufruft, die eine andere Funktion aufruft, die eine andere Funktion aufruft, die ..., belegen die Stapelvariablen in all diesen Funktionen Speicherplatz auf demselben Stapel. Rekursive Funktionen können schnell auf dieses Problem stoßen, je nachdem, wie tief die Rekursion ist. Wenn dies ein Problem ist, können Sie den Stapel vergrößern (nicht empfohlen) oder die Variable auf dem Heap mit dem neuen Operator zuweisen (empfohlen).

Die andere, wahrscheinlichere Bedingung ist, dass Ihre Variable über den Umfang Ihrer Funktion hinaus "leben" muss. In diesem Fall würden Sie die Variable auf dem Heap so zuweisen, dass sie außerhalb des Bereichs einer bestimmten Funktion erreicht werden kann.

Matt Davis
quelle
1

Übergeben Sie myClass aus einer Funktion oder erwarten Sie, dass es außerhalb dieser Funktion existiert? Wie einige andere sagten, dreht sich alles um den Umfang, wenn Sie nicht auf dem Heap zuordnen. Wenn Sie die Funktion verlassen, verschwindet sie (eventuell). Einer der klassischen Fehler von Anfängern ist der Versuch, ein lokales Objekt einer Klasse in einer Funktion zu erstellen und zurückzugeben, ohne es dem Heap zuzuweisen. Ich kann mich daran erinnern, wie ich solche Dinge in meinen früheren Tagen mit C ++ debuggt habe.

itsmatt
quelle
0

Die zweite Methode erstellt die Instanz auf dem Stapel zusammen mit deklarierten Elementen intund der Liste der Parameter, die an die Funktion übergeben werden.

Die erste Methode bietet Platz für einen Zeiger auf dem Stapel, den Sie auf den Speicherort im Speicher gesetzt haben, an dem ein neuer MyClassauf dem Heap zugewiesen wurde - oder auf dem freien Speicher.

Die erste Methode erfordert auch, dass Sie deletedas erstellen new, womit Sie erstellen , während bei der zweiten Methode die Klasse automatisch zerstört und freigegeben wird, wenn sie außerhalb des Gültigkeitsbereichs liegt (normalerweise die nächste schließende Klammer).

grau verblassen
quelle
-1

Die kurze Antwort lautet: Ja, das Schlüsselwort "new" ist unglaublich wichtig, da bei der Verwendung die Objektdaten auf dem Heap gespeichert werden und nicht auf dem Stapel, was am wichtigsten ist!

RAGNO
quelle