web-dev-qa-db-ja.com

C ++での適切なスタックとヒープの使用法

私はしばらくプログラミングをしていましたが、ほとんどJavaとC#です。実際に自分でメモリを管理する必要はありませんでした。最近C++でプログラミングを始めました。スタックに格納するときとヒープに格納するときについて混乱しています。

私の理解では、非常に頻繁にアクセスされる変数はスタックとオブジェクトに保存され、めったに使用されない変数と大きなデータ構造はすべてヒープに保存されるべきです。これは正しいですか、間違っていますか?

121
Alexander

いいえ、スタックとヒープの違いはパフォーマンスではありません。それは寿命です:関数内のローカル変数(malloc()またはnew以外のもの)はスタック上に存在します。関数から戻ると消えます。宣言した関数よりも長く存続させたい場合は、ヒープに割り当てる必要があります。

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

スタックが何であるかをより明確に理解するには、高レベル言語の観点からスタックが何をするのかを理解するのではなく、「コールスタック」と「呼び出し規約」を調べて、マシンは実際に関数を呼び出すときに実行します。コンピューターのメモリは、一連のアドレスです。 「ヒープ」と「スタック」はコンパイラの発明です。

240
Crashworks

私は言うだろう:

できる場合は、スタックに保存します。

必要に応じて、ヒープに保存します。

したがって、スタックはヒープよりも優先されます。スタックに何かを格納できない理由として考えられるものは次のとおりです。

  • 大きすぎる-32ビットOSのマルチスレッドプログラムでは、スタックのサイズは小さく(少なくともスレッド作成時には)固定されています(通常は数メガバイトです。これにより、アドレスを使い果たすことなく多くのスレッドを作成できます) 64ビットプログラム、またはシングルスレッド(とにかくLinux)プログラムの場合、これは大きな問題ではありません。32ビットLinuxでは、シングルスレッドプログラムは通常、ヒープの最上部に達するまで成長し続ける動的スタックを使用します。
  • 元のスタックフレームの範囲外でアクセスする必要があります-これが主な理由です。

賢明なコンパイラーでは、ヒープ上に固定サイズ以外のオブジェクト(通常はコンパイル時にサイズがわからない配列)を割り当てることができます。

42
MarkR

他の答えが示唆するよりも微妙です。スタック上のデータとヒープ上のデータの間には、宣言方法に基づいた絶対的な分割はありません。例えば:

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

関数の本体で、スタック上の10個の整数のvector(動的配列)を宣言します。ただし、vectorによって管理されるストレージはスタック上にありません。

ああ、しかし(他の回答が示唆する)そのストレージの寿命はvector自体の寿命によって制限されています。ここではスタックベースであるため、実装方法に違いはありません。値セマンティクスを持つスタックベースのオブジェクトとして。

そうではありません。関数が次のとおりだったとします:

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

    // fill v with numbers

    result.swap(v);
}

そのため、swap関数(および任意の複雑な値の型)を持つものは、そのデータの単一の所有者を保証するシステムの下で、ヒープデータへの一種の再バインド可能な参照として機能します。

したがって、最新のC++のアプローチは、neverヒープデータのアドレスをネイキッドローカルポインター変数に格納することです。すべてのヒープ割り当ては、クラス内で非表示にする必要があります。

それを行うと、プログラム内のすべての変数を単純な値型であるかのように考えることができ、ヒープを完全に忘れることができます(一部のヒープデータの新しい値のようなラッパークラスを記述する場合は例外です) 。

最適化に役立つ特別な知識を1つ保持するだけで済みます。可能な場合は、次のように1つの変数を別の変数に割り当てるのではなく、

a = b;

次のようにそれらを交換します。

a.swap(b);

はるかに高速であり、例外をスローしないためです。唯一の要件は、同じ値を保持し続けるためにbを必要としないことです(代わりにaの値を取得し、a = bで破棄されます)。

欠点は、このアプローチでは、実際の戻り値ではなく、出力パラメーターを介して関数から値を返すことを余儀なくされることです。しかし、C++ 0xでは 右辺値参照 で修正しています。

最も複雑な状況では、この考えを一般的な極端なものにし、すでにtr1にあるshared_ptrなどのスマートポインタークラスを使用します。 (必要と思われる場合は、標準C++の適用性のスイートスポットの外側に移動した可能性があると主張します。)

24

また、作成された関数のスコープ外で使用する必要がある場合は、アイテムをヒープに保存します。スタックオブジェクトで使用されるイディオムの1つはRAIIと呼ばれます。これには、スタックベースのオブジェクトをリソースのラッパーとして使用することが含まれます。オブジェクトが破棄されると、リソースはクリーンアップされます。スタックベースのオブジェクトを使用すると、例外をスローするタイミングを簡単に追跡できます。例外ハンドラーでヒープベースのオブジェクトを削除する必要はありません。これが、未加工のポインターが現代のC++で通常使用されない理由です。ヒープベースのオブジェクトへの未加工のポインターのスタックベースのラッパーであるスマートポインターを使用します。

6

他の答えに追加するために、少なくとも少しはパフォーマンスについてもできます。あなたに関係がない限り、これについて心配する必要はありませんが、

ヒープに割り当てるには、メモリブロックの追跡を見つける必要がありますが、これは一定時間の操作ではありません(そして、いくつかのサイクルとオーバーヘッドがかかります)。これは、メモリが断片化したり、アドレス空間の100%を使用し始めたりするにつれて遅くなる可能性があります。一方、スタックの割り当ては一定時間で、基本的に「無料」の操作です。

考慮すべきもう1つのことは(これが問題になる場合にのみ重要です)、通常、スタックサイズは固定されており、ヒープサイズよりもはるかに小さいことがあります。したがって、大きなオブジェクトまたは多くの小さなオブジェクトを割り当てる場合は、おそらくヒープを使用する必要があります。スタックスペースが不足すると、ランタイムはサイトタイトル例外をスローします。通常は大したことではありませんが、別の考慮事項があります。

5
Nick

スタックはより効率的で、スコープデータの管理が簡単です。

ただし、ヒープはfewKB)よりも大きいものに使用する必要があります(C++では簡単です。スタックにboost::scoped_ptrを作成するだけです割り当てられたメモリへのポインタを保持します)。

それ自体を呼び出し続ける再帰的アルゴリズムを検討してください。スタックの総使用量を制限したり推測したりすることは非常に困難です!一方、ヒープ上では、アロケーター(malloc()またはnew)は、NULLまたはthrowを返すことにより、メモリー不足を示すことができます。

ソース:スタックが8KB以下のLinuxカーネル!

3
unixman83

完全を期すために、組み込みソフトウェアのコンテキストでヒープを使用する問題に関するMiro Samekの記事を読むことができます。

問題の山

2
Daniel Daranas

ヒープまたはスタックのどちらに割り当てるかは、変数の割り当て方法に応じて選択されます。 「新規」呼び出しを使用して何かを動的に割り当てる場合、ヒープから割り当てます。グローバル変数として、または関数のパラメーターとして何かを割り当てると、スタックに割り当てられます。

1
Rob Lachlan

おそらくこれは非常によく回答されています。以下の一連の記事を参照して、低レベルの詳細をより深く理解してください。アレックスダービーには一連の記事があり、デバッガーについて説明しています。スタックに関するパート3です。 http://www.altdevblogaday.com/2011/12/14/c-c-low-level-curriculum-part-3-the-stack/

0
hAcKnRoCk

私の意見では、2つの決定要因があります

1) Scope of variable
2) Performance.

ほとんどの場合、スタックを使用することを好みますが、スコープ外の変数にアクセスする必要がある場合は、ヒープを使用できます。

ヒープの使用中にパフォーマンスを向上させるために、ヒープブロックを作成する機能を使用することもできます。これにより、各メモリ位置に各変数を割り当てるのではなく、パフォーマンスを向上させることができます。

0
anand