Richtige Verwendung von Stacks und Heaps in C ++?


122

Ich habe eine Weile programmiert, aber es waren hauptsächlich Java und C #. Ich musste das Gedächtnis nie alleine verwalten. Ich habe vor kurzem angefangen, in C ++ zu programmieren, und bin ein wenig verwirrt darüber, wann ich Dinge auf dem Stapel speichern soll und wann ich sie auf dem Heap speichern soll.

Nach meinem Verständnis sollten Variablen, auf die sehr häufig zugegriffen wird, auf dem Stapel und den Objekten gespeichert werden, selten verwendete Variablen und große Datenstrukturen sollten alle auf dem Heap gespeichert werden. Ist das richtig oder bin ich falsch?


Antworten:


242

Nein, der Unterschied zwischen Stack und Heap ist nicht die Leistung. Es ist die Lebensdauer: Jede lokale Variable innerhalb einer Funktion (alles, was Sie nicht malloc () oder neu tun) befindet sich auf dem Stapel. Es verschwindet, wenn Sie von der Funktion zurückkehren. Wenn Sie möchten, dass etwas länger lebt als die Funktion, die es deklariert hat, müssen Sie es dem Heap zuweisen.

class Thingy;

Thingy* foo( ) 
{
  int a; // this int lives on the stack
  Thingy B; // this thingy lives on the stack and will be deleted when we return from foo
  Thingy *pointerToB = &B; // this points to an address on the stack
  Thingy *pointerToC = new Thingy(); // this makes a Thingy on the heap.
                                     // pointerToC contains its address.

  // this is safe: C lives on the heap and outlives foo().
  // Whoever you pass this to must remember to delete it!
  return pointerToC;

  // this is NOT SAFE: B lives on the stack and will be deleted when foo() returns. 
  // whoever uses this returned pointer will probably cause a crash!
  return pointerToB;
}

Um besser zu verstehen, was der Stapel ist, schauen Sie am anderen Ende nach - anstatt zu verstehen, was der Stapel in Bezug auf eine Hochsprache tut, schauen Sie nach "Aufrufstapel" und "Aufrufkonvention" und sehen Sie, was Die Maschine funktioniert wirklich, wenn Sie eine Funktion aufrufen. Der Computerspeicher besteht nur aus einer Reihe von Adressen. "Heap" und "Stack" sind Erfindungen des Compilers.


7
Es wäre sicher hinzuzufügen, dass Informationen mit variabler Größe im Allgemeinen auf dem Haufen landen. Die einzigen mir bekannten Ausnahmen sind VLAs in C99 (die nur begrenzte Unterstützung bieten) und die alloca () -Funktion, die selbst von C-Programmierern oft missverstanden wird.
Dan Olson

10
Gute Erklärung, wenn auch in einem Multi - Thread - Szenario mit häufigen Zuweisungen und / oder das Aufheben dieser Zuordnungen, Heap ist ein Streitpunkt, wodurch die Leistung zu beeinträchtigen. Dennoch ist Scope fast immer der entscheidende Faktor.
Peterchen

18
Sicher, und new / malloc () ist selbst eine langsame Operation, und es ist wahrscheinlicher, dass sich der Stapel im Dcache befindet als in einer beliebigen Heap-Zeile. Dies sind echte Überlegungen, die jedoch in der Regel der Frage der Lebensdauer nachgeordnet sind.
Crashworks

1
Ist es wahr "Computerspeicher ist nur eine Reihe von Adressen;" Heap "und" Stack "sind Erfindungen der Kompilierung"? Ich habe an vielen Stellen gelesen, dass der Stapel ein spezieller Bereich im Speicher unseres Computers ist.
Vineeth Chitteti

2
@kai Das ist eine Möglichkeit, es zu visualisieren, aber physisch gesehen ist es nicht unbedingt wahr. Das Betriebssystem ist für die Zuweisung des Stapels und des Heaps einer Anwendung verantwortlich. Der Compiler ist ebenfalls verantwortlich, verlässt sich jedoch in erster Linie auf das Betriebssystem. Der Stapel ist begrenzt und der Haufen nicht. Dies liegt an der Art und Weise, wie das Betriebssystem diese Speicheradressen in etwas strukturierteres sortiert, sodass mehrere Anwendungen auf demselben System ausgeführt werden können. Heap und Stack sind nicht die einzigen, aber sie sind normalerweise die einzigen, um die sich die meisten Entwickler Sorgen machen.
tsturzl

42

Ich würde sagen:

Speichern Sie es auf dem Stapel, wenn Sie können.

Speichern Sie es auf dem Haufen, wenn Sie müssen.

Ziehen Sie daher den Stapel dem Haufen vor. Einige mögliche Gründe, warum Sie etwas nicht auf dem Stapel speichern können, sind:

  • Es ist zu groß - bei Multithread-Programmen unter 32-Bit-Betriebssystemen hat der Stack eine kleine und feste Größe (mindestens zum Zeitpunkt der Thread-Erstellung) (normalerweise nur wenige Megabyte). Auf diese Weise können Sie viele Threads erstellen, ohne die Adresse zu erschöpfen Speicherplatz. Bei 64-Bit-Programmen oder Single-Threaded-Programmen (Linux sowieso) ist dies kein großes Problem. Unter 32-Bit-Linux verwenden Single-Threaded-Programme normalerweise dynamische Stapel, die weiter wachsen können, bis sie den oberen Rand des Heaps erreichen.
  • Sie müssen außerhalb des Bereichs des ursprünglichen Stapelrahmens darauf zugreifen - dies ist wirklich der Hauptgrund.

Mit sinnvollen Compilern ist es möglich, Objekte mit nicht fester Größe auf dem Heap zuzuweisen (normalerweise Arrays, deren Größe zur Kompilierungszeit nicht bekannt ist).


1
Alles, was mehr als ein paar KB beträgt, wird normalerweise am besten auf den Haufen gelegt. Ich kenne keine Einzelheiten, aber ich kann mich nicht erinnern, jemals mit einem Stapel gearbeitet zu haben, der "ein paar Megabyte" war.
Dan Olson

2
Das ist etwas, mit dem ich einen Benutzer am Anfang nicht beschäftigen würde. Für den Benutzer scheinen Vektoren und Listen auf dem Stapel zugeordnet zu sein, selbst wenn diese STL den Inhalt auf dem Heap speichert. Die Frage schien eher zu entscheiden, wann explizit new / delete aufgerufen werden soll.
David Rodríguez - Dribeas

1
Dan: Ich habe 2 Gigs (Ja, G wie in GIGS) unter 32-Bit-Linux auf den Stack gelegt. Stapelbeschränkungen sind vom Betriebssystem abhängig.
Mr.Ree

6
mrree: Der Nintendo DS-Stack ist 16 Kilobyte groß. Einige Stapelbeschränkungen sind hardwareabhängig.
Ameise

Ant: Alle Stacks sind hardwareabhängig, betriebssystemabhängig und auch compilerabhängig.
Viliami

24

Es ist subtiler als die anderen Antworten vermuten lassen. Es gibt keine absolute Trennung zwischen Daten auf dem Stapel und Daten auf dem Heap, basierend darauf, wie Sie sie deklarieren. Beispielsweise:

std::vector<int> v(10);

Im Hauptteil einer Funktion wird ein vector(dynamisches Array) von zehn Ganzzahlen auf dem Stapel deklariert . Der von der verwaltete Speicher vectorbefindet sich jedoch nicht auf dem Stapel.

Ah, aber (die anderen Antworten deuten darauf hin) die Lebensdauer dieses Speichers ist durch die Lebensdauer des Speichers selbst begrenzt vector, der hier stapelbasiert ist. Daher spielt es keine Rolle, wie er implementiert wird - wir können ihn nur als stapelbasiertes Objekt behandeln mit Wertesemantik.

Nicht so. Angenommen, die Funktion war:

void GetSomeNumbers(std::vector<int> &result)
{
    std::vector<int> v(10);

    // fill v with numbers

    result.swap(v);
}

Alles mit einer swapFunktion (und jeder komplexe Werttyp sollte eine haben) kann als eine Art wiederbindbarer Verweis auf einige Heap-Daten unter einem System dienen, das einen einzelnen Eigentümer dieser Daten garantiert.

Daher besteht der moderne C ++ - Ansatz darin, die Adresse von Heap-Daten niemals in nackten lokalen Zeigervariablen zu speichern. Alle Heap-Zuordnungen müssen in Klassen ausgeblendet sein.

Wenn Sie dies tun, können Sie sich alle Variablen in Ihrem Programm als einfache Werttypen vorstellen und den Heap insgesamt vergessen (außer beim Schreiben einer neuen wertähnlichen Wrapper-Klasse für einige Heap-Daten, was ungewöhnlich sein sollte). .

Sie müssen lediglich ein spezielles Wissen behalten, um die Optimierung zu unterstützen: Wenn möglich, anstatt eine Variable einer anderen wie folgt zuzuweisen:

a = b;

Tauschen Sie sie so aus:

a.swap(b);

weil es viel schneller ist und keine Ausnahmen auslöst. Die einzige Voraussetzung ist, dass Sie nicht bweiterhin denselben Wert beibehalten müssen ( astattdessen wird der Wert abgerufen, der in den Papierkorb verschoben wird a = b).

Der Nachteil ist, dass Sie bei diesem Ansatz gezwungen sind, Werte von Funktionen über Ausgabeparameter anstelle des tatsächlichen Rückgabewerts zurückzugeben. Aber sie beheben das in C ++ 0x mit rWert-Referenzen .

In den kompliziertesten Situationen von allen würden Sie diese Idee auf das äußerste Extrem bringen und eine intelligente shared_ptrZeigerklasse verwenden, wie sie bereits in tr1 enthalten ist. (Obwohl ich behaupten würde, dass Sie, wenn Sie es zu brauchen scheinen, möglicherweise außerhalb des Sweetspots der Anwendbarkeit von Standard C ++ umgezogen sind.)


6

Sie würden ein Element auch auf dem Heap speichern, wenn es außerhalb des Bereichs der Funktion verwendet werden muss, in der es erstellt wird. Eine mit Stapelobjekten verwendete Redewendung heißt RAII. Dabei wird das stapelbasierte Objekt als Wrapper für eine Ressource verwendet. Wenn das Objekt zerstört wird, wird die Ressource bereinigt. Stapelbasierte Objekte lassen sich leichter verfolgen, wann Sie möglicherweise Ausnahmen auslösen. Sie müssen sich nicht darum kümmern, ein Heap-basiertes Objekt in einem Ausnahmebehandler zu löschen. Aus diesem Grund werden Rohzeiger in modernem C ++ normalerweise nicht verwendet. Sie würden einen intelligenten Zeiger verwenden, der ein stapelbasierter Wrapper für einen Rohzeiger auf ein Heap-basiertes Objekt sein kann.


5

Um die anderen Antworten zu ergänzen, kann es auch um Leistung gehen, zumindest ein wenig. Nicht, dass Sie sich darüber Sorgen machen sollten, es sei denn, es ist für Sie relevant, aber:

Das Zuweisen im Heap erfordert das Finden einer Verfolgung eines Speicherblocks, was keine zeitkonstante Operation ist (und einige Zyklen und Overhead benötigt). Dies kann langsamer werden, wenn der Speicher fragmentiert wird und / oder Sie fast 100% Ihres Adressraums nutzen. Andererseits sind Stapelzuweisungen zeitkonstante, im Grunde "freie" Operationen.

Eine andere zu berücksichtigende Sache (wiederum wirklich nur wichtig, wenn es zu einem Problem wird) ist, dass normalerweise die Stapelgröße fest ist und viel niedriger als die Heap-Größe sein kann. Wenn Sie also große oder viele kleine Objekte zuweisen, möchten Sie wahrscheinlich den Heap verwenden. Wenn Ihnen der Stapelspeicherplatz ausgeht, löst die Laufzeit die Site-Titelausnahme aus. Normalerweise keine große Sache, aber eine andere Sache zu beachten.


Sowohl Heap als auch Stack sind ausgelagerter virtueller Speicher. Die Heap-Suchzeit ist im Vergleich zu dem, was für die Zuordnung in einem neuen Speicher erforderlich ist, erstaunlich schnell. Unter 32bit Linux kann ich> 2gig auf meinen Stack legen. Unter Macs ist der Stack meiner Meinung nach auf 65 Megabyte beschränkt.
Mr.Ree

3

Der Stapel ist effizienter und einfacher zu verwalten.

Heap sollte jedoch für alles verwendet werden, was größer als ein paar KB ist (in C ++ ist es einfach, erstellen Sie einfach einen boost::scoped_ptrauf dem Stapel, um einen Zeiger auf den zugewiesenen Speicher zu halten).

Stellen Sie sich einen rekursiven Algorithmus vor, der immer wieder in sich selbst aufruft. Es ist sehr schwer, die Gesamtstapelnutzung zu begrenzen oder zu erraten! Während auf dem Heap der Allokator ( malloc()oder new) durch Zurückgeben NULLoder throwIng auf Speichermangel hinweisen kann .

Quelle : Linux-Kernel, dessen Stack nicht größer als 8 KB ist!


Als Referenz für andere Leser: (A) Das "sollte" hier ist lediglich die persönliche Meinung des Benutzers, die bestenfalls aus 1 Zitat und 1 Szenario stammt, auf das viele Benutzer wahrscheinlich nicht stoßen (Rekursion). Außerdem bietet (B) die Standardbibliothek std::unique_ptr, die jeder externen Bibliothek wie Boost vorzuziehen ist (obwohl dies im Laufe der Zeit dem Standard zugute kommt).
underscore_d

2

Der Vollständigkeit halber können Sie den Artikel von Miro Samek über die Probleme bei der Verwendung des Heaps im Kontext eingebetteter Software lesen .

Ein Haufen Probleme


1

Die Wahl, ob auf dem Heap oder auf dem Stapel zugewiesen werden soll, wird für Sie getroffen, abhängig davon, wie Ihre Variable zugewiesen wird. Wenn Sie etwas dynamisch mithilfe eines "neuen" Aufrufs zuweisen, weisen Sie es vom Heap aus zu. Wenn Sie etwas als globale Variable oder als Parameter in einer Funktion zuweisen, wird es auf dem Stapel zugewiesen.


4
Ich vermute, er fragte, wann er die Dinge auf den Haufen legen sollte, nicht wie.
Steve Rowe

0

Meiner Meinung nach gibt es zwei entscheidende Faktoren

1) Scope of variable
2) Performance.

Ich würde in den meisten Fällen lieber Stack verwenden, aber wenn Sie Zugriff auf Variablen außerhalb des Gültigkeitsbereichs benötigen, können Sie Heap verwenden.

Um die Leistung bei der Verwendung von Heaps zu verbessern, können Sie die Funktionalität auch zum Erstellen von Heap-Blöcken verwenden. Dies kann dazu beitragen, die Leistung zu steigern, anstatt jede Variable an einem anderen Speicherort zuzuweisen.


0

wahrscheinlich wurde dies recht gut beantwortet. Ich möchte Sie auf die folgende Artikelserie verweisen, um ein tieferes Verständnis für Details auf niedriger Ebene zu erhalten. Alex Darby hat eine Reihe von Artikeln, in denen er Sie mit einem Debugger durchführt. Hier ist Teil 3 über den Stapel. http://www.altdevblogaday.com/2011/12/14/cc-low-level-curriculum-part-3-the-stack/


Der Link scheint tot zu sein, aber die Überprüfung des Internet Archive Wayback Machine zeigt an, dass er nur über den Stack spricht und daher nichts unternimmt, um die spezifische Frage von Stack versus Heap hier zu beantworten . -1
underscore_d
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.