C ++ - Objektinstanziierung

113

Ich bin ein C-Programmierer, der versucht, C ++ zu verstehen. Viele Tutorials demonstrieren die Objektinstanziierung mithilfe eines Snippets wie:

Dog* sparky = new Dog();

was bedeutet, dass Sie später tun werden:

delete sparky;

was Sinn macht. Wenn für den Fall, dass keine dynamische Speicherzuweisung erforderlich ist, gibt es einen Grund, die oben genannten anstelle von zu verwenden

Dog sparky;

und lassen Sie den Destruktor aufgerufen werden, sobald Sparky den Rahmen verlässt?

Vielen Dank!

el_champo
quelle

Antworten:

166

Im Gegenteil, Sie sollten immer Stapelzuordnungen bevorzugen, sofern Sie als Faustregel niemals Neu / Löschen in Ihrem Benutzercode haben sollten.

Wie Sie sagen, wenn die Variable auf dem Stapel deklariert wird, wird ihr Destruktor automatisch aufgerufen, wenn sie den Gültigkeitsbereich verlässt. Dies ist Ihr Hauptwerkzeug zur Verfolgung der Ressourcenlebensdauer und zur Vermeidung von Lecks.

Wenn Sie also eine Ressource zuweisen müssen, egal ob es sich um Speicher (durch Aufrufen von new), Dateihandles, Sockets oder etwas anderes handelt, schließen Sie sie im Allgemeinen in eine Klasse ein, in der der Konstruktor die Ressource abruft und der Destruktor sie freigibt. Anschließend können Sie ein Objekt dieses Typs auf dem Stapel erstellen, und Sie können sicher sein, dass Ihre Ressource freigegeben wird, wenn sie den Gültigkeitsbereich verlässt. Auf diese Weise müssen Sie Ihre neuen / gelöschten Paare nicht überall verfolgen, um Speicherlecks zu vermeiden.

Der gebräuchlichste Name für diese Redewendung ist RAII

Schauen Sie sich auch Smart-Pointer-Klassen an, mit denen die resultierenden Zeiger in den seltenen Fällen umbrochen werden, in denen Sie außerhalb eines dedizierten RAII-Objekts etwas mit new zuweisen müssen. Sie übergeben den Zeiger stattdessen an einen intelligenten Zeiger, der dann seine Lebensdauer beispielsweise durch Referenzzählung verfolgt und den Destruktor aufruft, wenn die letzte Referenz den Gültigkeitsbereich verlässt. Die Standardbibliothek verfügt std::unique_ptrüber eine einfache bereichsbasierte Verwaltung, std::shared_ptrdie Referenzzählungen zur Implementierung von gemeinsamem Besitz durchführt.

Viele Tutorials demonstrieren die Objektinstanziierung mithilfe eines Snippets wie ...

Sie haben also festgestellt, dass die meisten Tutorials scheiße sind. ;) In den meisten Tutorials lernen Sie miese C ++ - Praktiken, einschließlich des Aufrufs von new / delete, um Variablen zu erstellen, wenn dies nicht erforderlich ist, und der Schwierigkeit, die Lebensdauer Ihrer Zuordnungen zu verfolgen.

jalf
quelle
Raw-Zeiger sind nützlich, wenn Sie eine auto_ptr-ähnliche Semantik (Eigentumsübertragung) wünschen, aber die nicht auslösende Swap-Operation beibehalten und den Overhead der Referenzzählung nicht möchten. Rand Fall vielleicht, aber nützlich.
Greg Rogers
2
Dies ist eine richtige Antwort, aber der Grund, warum ich mich nie daran gewöhnen würde, Objekte auf dem Stapel zu erstellen, ist, dass es nie ganz offensichtlich ist, wie groß dieses Objekt sein wird. Sie fragen nur nach einer Stapelüberlauf-Ausnahme.
dviljoen
3
Greg: Auf jeden Fall. Aber wie Sie sagen, ein Randfall. Im Allgemeinen werden Zeiger am besten vermieden. Aber sie sind aus einem bestimmten Grund in der Sprache, was nicht zu leugnen ist. :) dviljoen: Wenn das Objekt groß ist, wickeln Sie es in ein RAII-Objekt ein, das auf dem Stapel zugewiesen werden kann und einen Zeiger auf Heap-zugewiesene Daten enthält.
Jalf
3
@dviljoen: Nein, bin ich nicht. C ++ - Compiler blähen Objekte nicht unnötig auf. Das Schlimmste, was Sie sehen werden, ist normalerweise, dass es auf das nächste Vielfache von vier Bytes aufgerundet wird. Normalerweise nimmt eine Klasse, die einen Zeiger enthält, so viel Platz ein wie der Zeiger selbst, sodass Sie bei der Stapelverwendung nichts kosten.
Jalf
20

Obwohl es von Vorteil sein kann, Dinge auf dem Stapel zu haben, was die Zuweisung und das automatische Freigeben betrifft, hat dies einige Nachteile.

  1. Möglicherweise möchten Sie keine großen Objekte auf dem Stapel zuweisen.

  2. Dynamischer Versand! Betrachten Sie diesen Code:

#include <iostream>

class A {
public:
  virtual void f();
  virtual ~A() {}
};

class B : public A {
public:
  virtual void f();
};

void A::f() {cout << "A";}
void B::f() {cout << "B";}

int main(void) {
  A *a = new B();
  a->f();
  delete a;
  return 0;
}

Dies wird "B" drucken. Nun wollen wir sehen, was bei der Verwendung von Stack passiert:

int main(void) {
  A a = B();
  a.f();
  return 0;
}

Dadurch wird "A" gedruckt, was für diejenigen, die mit Java oder anderen objektorientierten Sprachen vertraut sind, möglicherweise nicht intuitiv ist. Der Grund ist, dass Sie keinen Zeiger mehr auf eine Instanz von haben B. Stattdessen wird eine Instanz von Berstellt und in eine aVariable vom Typ kopiert A.

Einige Dinge können unbeabsichtigt passieren, insbesondere wenn Sie mit C ++ noch nicht vertraut sind. In C haben Sie Ihre Zeiger und das wars. Sie wissen, wie man sie benutzt und sie tun IMMER das Gleiche. In C ++ ist dies nicht der Fall. Man stelle sich vor , was passiert, wenn Sie ein in diesem Beispiel als Argument für eine Methode - die Dinge komplizierter und es macht einen großen Unterschied machen , wenn ader Typ ist Aoder A*oder auch A&(Call-by-reference). Viele Kombinationen sind möglich und verhalten sich alle unterschiedlich.

Universum
quelle
2
-1: Menschen, die die Wertesemantik nicht verstehen können und die einfache Tatsache, dass der Polymorphismus eine Referenz / einen Zeiger benötigt (um das Schneiden von Objekten zu vermeiden), stellen kein Problem mit der Sprache dar. Die Macht von c ++ sollte nicht als Nachteil angesehen werden, nur weil einige Leute ihre Grundregeln nicht lernen können.
underscore_d
Danke für die Warnung. Ich hatte ein ähnliches Problem mit der Methode, Objekte anstelle von Zeigern oder Referenzen aufzunehmen, und was für ein Durcheinander es war. Das Objekt enthält Zeiger, die aufgrund dieser Bewältigung zu früh gelöscht wurden.
BoBoDev
@underscore_d Ich stimme zu; Der letzte Absatz dieser Antwort sollte entfernt werden. Nehmen Sie die Tatsache, dass ich diese Antwort bearbeitet habe, nicht so, dass ich damit einverstanden bin. Ich wollte nur nicht, dass die Fehler darin gelesen werden.
TamaMcGlinn
@TamaMcGlinn stimmte zu. Danke für die gute Bearbeitung. Ich habe den Meinungsteil entfernt.
UniversE
13

Nun, der Grund für die Verwendung des Zeigers wäre genau der gleiche wie der Grund für die Verwendung von Zeigern in C, die mit malloc zugewiesen wurden: Wenn Sie möchten, dass Ihr Objekt länger als Ihre Variable lebt!

Es wird sogar dringend empfohlen, den neuen Operator NICHT zu verwenden, wenn Sie dies vermeiden können. Vor allem, wenn Sie Ausnahmen verwenden. Im Allgemeinen ist es viel sicherer, den Compiler Ihre Objekte freigeben zu lassen.

PierreBdR
quelle
13

Ich habe dieses Anti-Muster von Leuten gesehen, die die & Adresse des Betreibers nicht ganz verstehen. Wenn sie eine Funktion mit einem Zeiger aufrufen müssen, ordnen sie sie immer dem Heap zu, damit sie einen Zeiger erhalten.

void FeedTheDog(Dog* hungryDog);

Dog* badDog = new Dog;
FeedTheDog(badDog);
delete badDog;

Dog goodDog;
FeedTheDog(&goodDog);
Mark Ransom
quelle
Alternativ: void FeedTheDog (Dog & hungriger Hund); Hund Hund; FeedTheDog (Hund);
Scott Langham
1
@ ScottLangham stimmt, aber das war nicht der Punkt, den ich machen wollte. Manchmal rufen Sie eine Funktion auf, die beispielsweise einen optionalen NULL-Zeiger verwendet, oder es handelt sich um eine vorhandene API, die nicht geändert werden kann.
Mark Ransom
7

Behandeln Sie Haufen als eine sehr wichtige Immobilie und verwenden Sie sie sehr vernünftig. Die grundlegende Faustregel lautet, wann immer möglich Stapel zu verwenden und Heap zu verwenden, wann immer es keinen anderen Weg gibt. Durch Zuweisen der Objekte auf dem Stapel können Sie viele Vorteile erzielen, wie z.

(1). In Ausnahmefällen müssen Sie sich keine Gedanken über das Abwickeln des Stapels machen

(2). Sie müssen sich keine Gedanken über die Speicherfragmentierung machen, die durch die Zuweisung von mehr Speicherplatz als von Ihrem Heap-Manager erforderlich verursacht wird.

Naveen
quelle
1
Die Größe des Objekts sollte berücksichtigt werden. Der Stapel ist größenbeschränkt, daher sollten sehr große Objekte dem Heap zugewiesen werden.
Adam
1
@Adam Ich denke, dass Naveen hier immer noch richtig ist. Wenn Sie ein großes Objekt auf den Stapel legen können, tun Sie es, weil es besser ist. Es gibt viele Gründe, die Sie wahrscheinlich nicht können. Aber ich denke er hat recht.
McKay
5

Der einzige Grund, über den ich mir Sorgen machen würde, ist, dass Dog jetzt auf dem Stapel und nicht auf dem Haufen zugewiesen ist. Wenn der Hund also Megabyte groß ist, liegt möglicherweise ein Problem vor:

Wenn Sie die neue / gelöschte Route wählen müssen, seien Sie vorsichtig mit Ausnahmen. Aus diesem Grund sollten Sie auto_ptr oder einen der Boost-Smart-Zeigertypen verwenden, um die Objektlebensdauer zu verwalten.

Roddy
quelle
1

Es gibt keinen Grund zu neu (auf dem Heap), wenn Sie auf dem Stapel zuordnen können (es sei denn, Sie haben aus irgendeinem Grund einen kleinen Stapel und möchten den Heap verwenden.

Möglicherweise möchten Sie einen shared_ptr (oder eine seiner Varianten) aus der Standardbibliothek verwenden, wenn Sie ihn auf dem Heap zuweisen möchten. Damit wird das Löschen für Sie erledigt, sobald alle Verweise auf shared_ptr nicht mehr vorhanden sind.

Scott Langham
quelle
Es gibt viele Gründe, zum Beispiel, ich antworte.
Dangerous
Ich nehme an, Sie möchten vielleicht, dass das Objekt den Funktionsumfang überlebt, aber das OP schien nicht zu suggerieren, dass dies das ist, was sie versucht haben.
Scott Langham
0

Es gibt einen zusätzlichen Grund, den noch niemand erwähnt hat, warum Sie Ihr Objekt möglicherweise dynamisch erstellen. Mit dynamischen, Heap-basierten Objekten können Sie Polymorphismus nutzen .

gefährliche Taube
quelle
2
Sie können auch polymorph mit stapelbasierten Objekten arbeiten. Ich sehe diesbezüglich keinen Unterschied zwischen stapel- und Heap-zugewiesenen Objekten. ZB: void MyFunc () {Dog dog; Hund füttern); } void Feed (Tier & Tier) {auto food = animal-> GetFavouriteFood (); PutFoodInBowl (Essen); } // In diesem Beispiel kann GetFavouriteFood eine virtuelle Funktion sein, die jedes Tier mit seiner eigenen Implementierung überschreibt. Dies funktioniert polymorph ohne Heap.
Scott Langham
-1: Polymorphismus erfordert nur eine Referenz oder einen Zeiger; Es ist völlig unabhängig von der Speicherdauer der zugrunde liegenden Instanz.
underscore_d
@underscore_d "Die Verwendung einer universellen Basisklasse bedeutet Kosten: Objekte müssen Heap-zugeordnet sein, um polymorph zu sein." - bjarne stroustrup stroustrup.com/bs_faq2.html#object
gefährlichdave
LOL, es ist mir egal, ob Stroustrup es selbst gesagt hat, aber das ist ein unglaublich peinliches Zitat für ihn, weil er falsch liegt. Es ist nicht schwer, dies selbst zu testen, wissen Sie: Instanziieren Sie einige DerivedA, DerivedB und DerivedC auf dem Stapel; Instanziieren Sie auch einen vom Stapel zugewiesenen Zeiger auf Base und bestätigen Sie, dass Sie ihn mit A / B / C platzieren und polymorph verwenden können. Wenden Sie kritisches Denken und Standardverweise auf Ansprüche an, auch wenn diese vom ursprünglichen Autor der Sprache stammen. Hier: stackoverflow.com/q/5894775/2757035
underscore_d
Um es so auszudrücken, ich habe ein Objekt, das 2 separate Familien mit jeweils fast 1000 polymorphen Objekten mit automatischer Speicherdauer enthält. Ich instanziiere dieses Objekt auf dem Stapel und indem ich über Referenzen oder Zeiger auf diese Elemente verweise, erreicht es die volle Fähigkeit des Polymorphismus über sie. Nichts in meinem Programm ist dynamisch / Heap-zugewiesen (abgesehen von Ressourcen, die den vom Stapel zugewiesenen Klassen gehören), und es ändert die Fähigkeiten meines Objekts nicht um ein Jota.
underscore_d
-4

Ich hatte das gleiche Problem in Visual Studio. Sie müssen verwenden:

yourClass-> classMethod ();

eher, als:

yourClass.classMethod ();

Hamed
quelle
3
Dies beantwortet die Frage nicht. Sie möchten lieber beachten, dass Sie eine andere Syntax verwenden müssen, um auf das auf dem Heap zugewiesene Objekt (über den Zeiger auf das Objekt) und das auf dem Stapel zugewiesene Objekt zuzugreifen.
Alexey Ivanov