it-swarm.com.de

Richtige Stack- und Heap-Nutzung in C ++?

Ich habe eine Weile programmiert, aber es waren meistens Java und C #. Ich musste den Speicher nie selbst verwalten. Ich habe kürzlich angefangen, in C++ zu programmieren, und ich bin ein bisschen Wir sind unsicher, wann ich Dinge auf dem Stapel und wann auf dem Haufen lagern soll.

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

121
Alexander

Nein, der Unterschied zwischen Stack und Heap ist nicht die Leistung. Es ist die Lebensdauer: Jede lokale Variable in einer Funktion (alles, was Sie nicht malloc () oder neu) leben 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 auf dem Heap zuordnen.

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;
}

Für ein klareres Verständnis dessen, was der Stack ist, versuchen Sie es von der anderen Seite - anstatt zu verstehen, was der Stack in Bezug auf eine Hochsprache tut, schlagen Sie "Aufrufstapel" und "Aufrufkonvention" nach und sehen Sie, was Die Maschine tut es wirklich, wenn Sie eine Funktion aufrufen. Der Computerspeicher besteht nur aus einer Reihe von Adressen. "Heap" und "Stack" sind Erfindungen des Compilers.

240
Crashworks

Ich würde sagen:

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

Lagern 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 Stapel eine kleine und feste Größe (mindestens zur Thread-Erstellungszeit) (in der Regel nur ein paar Megabyte. Auf diese Weise können Sie viele Threads erstellen, ohne die Adresse zu erschöpfen Bei 64-Bit-Programmen oder Single-Threaded-Programmen (ohnehin Linux) stellt dies kein großes Problem dar. Unter 32-Bit-Linux verwenden Single-Threaded-Programme normalerweise dynamische Stapel, die so lange wachsen können, bis sie die Spitze des Heaps erreichen.
  • Sie müssen außerhalb des Bereichs des ursprünglichen Stapelrahmens darauf zugreifen - dies ist wirklich der Hauptgrund.

Mit vernünftigen Compilern ist es möglich, Objekte mit nicht fester Größe auf dem Heap zuzuweisen (normalerweise Arrays, deren Größe zum Zeitpunkt der Kompilierung nicht bekannt ist).

42
MarkR

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 es deklarieren. Beispielsweise:

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

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

Ah, aber (die anderen Antworten legen nahe) die Lebensdauer dieses Speichers ist durch die Lebensdauer des vector selbst begrenzt, der hier stapelbasiert ist, sodass es keinen Unterschied macht, wie er implementiert ist - wir können ihn nur behandeln als stapelbasiertes Objekt mit Wertesemantik.

Nicht so. Angenommen, die Funktion lautete:

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

    // fill v with numbers

    result.swap(v);
}

Daher kann alles mit einer swap -Funktion (und jeder komplexe Werttyp sollte eine haben) als eine Art rückbindbarer Verweis auf einige Heap-Daten unter einem System dienen, das einen einzelnen Eigentümer dieser Daten garantiert.

Daher besteht der moderne C++ - Ansatz darin, niemals die Adresse von Heap-Daten in nackten lokalen Zeigervariablen zu speichern. Alle Heap-Zuordnungen müssen in Klassen verborgen 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 wenn Sie eine neue wertähnliche Wrapper-Klasse für einige Heap-Daten schreiben, was ungewöhnlich sein sollte). .

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

a = b;

tausche sie so aus:

a.swap(b);

weil es viel schneller ist und keine Ausnahmen wirft. Die einzige Voraussetzung ist, dass Sie b nicht benötigen, um weiterhin denselben Wert zu behalten (stattdessen wird der Wert von a abgerufen, der in a = b Verworfen würde).

Der Nachteil ist, dass dieser Ansatz Sie dazu zwingt, Werte von Funktionen über Ausgabeparameter anstelle des tatsächlichen Rückgabewerts zurückzugeben. Aber sie beheben das in C++ 0x mit rvalue references .

In den kompliziertesten Situationen würden Sie diese Idee auf die Spitze treiben und eine intelligente Zeigerklasse wie shared_ptr Verwenden, die sich bereits in tr1 befindet. (Obwohl ich argumentieren würde, dass Sie, wenn Sie es zu brauchen scheinen, möglicherweise aus dem Sweet Spot der Anwendbarkeit von Standard C++ herausgezogen sind.)

24

Sie können ein Element auch auf dem Heap speichern, wenn es außerhalb des Bereichs der Funktion verwendet werden soll, in der es erstellt wird. Ein für Stapelobjekte verwendetes Idiom heißt RAII. Hierbei 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 nachverfolgen, wenn Sie möglicherweise Ausnahmen auslösen. Sie müssen sich nicht mit dem Löschen eines Heap-basierten Objekts in einem Ausnahmehandler befassen. Aus diesem Grund werden in modernem C++ normalerweise keine rohen Zeiger verwendet. Sie würden einen intelligenten Zeiger verwenden, der ein stapelbasierter Wrapper für einen rohen Zeiger auf ein Heap-basiertes Objekt sein kann.

6

Um die anderen Antworten zu ergänzen, kann es auch um Leistung gehen, zumindest ein bisschen. 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 Auffinden eines Speicherblocks, was keine zeitlich konstante Operation ist (und einige Zyklen und Overhead erfordert). Dies kann langsamer werden, wenn der Speicher fragmentiert wird und/oder Sie fast 100% Ihres Adressraums nutzen. Andererseits sind Stapelzuweisungen zeitlich konstante, im Grunde "freie" Operationen.

Eine andere Sache, die zu berücksichtigen ist (auch hier ist dies wirklich nur wichtig, wenn es zu einem Problem wird), ist, dass die Stapelgröße in der Regel festgelegt ist und viel kleiner 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 Ausnahmebedingung site titular aus. Normalerweise keine große Sache, aber eine andere Sache, die man berücksichtigen sollte.

5
Nick

Stack ist effizienter und einfacher zu verwaltende Daten.

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

Betrachten Sie einen rekursiven Algorithmus, der immer wieder in sich selbst aufruft. Es ist sehr schwer, die gesamte Stapelverwendung einzuschränken und/oder zu erraten! Während auf dem Heap der Allokator (malloc() oder new) durch Zurückgeben von NULL oder throwing auf einen zu geringen Speicherplatz hinweisen kann.

Source: Linux Kernel dessen Stack nicht größer als 8KB ist!

3
unixman83

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

Ein Haufen Probleme

2
Daniel Daranas

Die Wahl, ob auf dem Heap oder auf dem Stack zugeteilt werden soll, hängt davon ab, wie Ihre Variable zugeteilt wird. Wenn Sie etwas dynamisch zuweisen, verwenden Sie einen "neuen" Aufruf, den Sie aus dem Heap zuweisen. Wenn Sie etwas als globale Variable oder als Parameter in einer Funktion zuweisen, wird es auf dem Stapel zugewiesen.

1
Rob Lachlan

wahrscheinlich wurde dies recht gut beantwortet. Ich möchte Sie auf die folgende Artikelserie verweisen, um ein tieferes Verständnis der 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 Stack. http://www.altdevblogaday.com/2011/12/14/c-c-low-level-curriculum-part-3-the-stack/

0
hAcKnRoCk

Meiner Meinung nach gibt es zwei entscheidende Faktoren

1) Scope of variable
2) Performance.

In den meisten Fällen würde ich Stack vorziehen, 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 auch die Funktion zum Erstellen von Heap-Blöcken verwenden, um die Leistung zu steigern, anstatt jede Variable an einem anderen Speicherort zuzuweisen.

0
anand