配列、ヒープ、スタック、値のタイプ


134
int[] myIntegers;
myIntegers = new int[100];

上記のコードで、新しいint [100]はヒープ上に配列を生成していますか?C#を介してCLRで読んだことから、答えは「はい」です。しかし、私が理解できないのは、配列内の実際のintに何が起こるかです。それらは値の型なので、たとえば、ボックスにボックス化する必要があると思います。たとえば、myIntegersをプログラムの他の部分に渡すと、常に残っているとスタックが乱雑になります。 。それとも私は間違っていますか?私はそれらがボックス化されるだけで、配列が存在する限りヒープ上に存在すると思います。

回答:


289

配列はヒープに割り当てられ、intはボックス化されません。

参照型はヒープに割り当てられ、値型はスタックに割り当てられると人々が言っ​​ているため、混乱の原因が考えられます。これは完全に正確な表現ではありません。

すべてのローカル変数とパラメーターはスタックに割り当てられます。これには、値タイプと参照タイプの両方が含まれます。2つの違いは、変数に格納されるものだけです。当然のことながら、値型の場合、型のは直接変数に格納され、参照型の場合、型の値はヒープに格納され、この値への参照は変数に格納されます。

フィールドについても同様です。集計タイプ(a classまたはastruct)の、そのインスタンスフィールドごとにストレージが含まれている必要があります。参照タイプのフィールドの場合、このストレージは値への参照のみを保持します。値自体は後でヒープに割り当てられます。値タイプのフィールドの場合、このストレージには実際の値が保持されます。

したがって、次のタイプが与えられます:

class RefType{
    public int    I;
    public string S;
    public long   L;
}

struct ValType{
    public int    I;
    public string S;
    public long   L;
}

これらの各タイプの値には、16バイトのメモリが必要です(32ビットのワードサイズを想定)。フィールドIそれぞれの場合には、その値を格納するために4つのバイトをとり、フィールドは、Sその参照を格納するために4つのバイトを取り、フィールドはLその値を格納する8つのバイトを取ります。両方の値のためのメモリだから、RefTypeValType次のようになります。

 0┌───────────────────┐
   │I│
 4├───────────────────┤
   │S│
 8├───────────────────┤
   │L│
   ││
16└───────────────────┘

今、あなたはタイプの関数内の3つのローカル変数を、持っていた場合RefTypeValTypeおよびint[]、このように:

RefType refType;
ValType valType;
int[]   intArray;

スタックは次のようになります。

 0┌───────────────────┐
   │refType│
 4├───────────────────┤
   │valType│
   ││
   ││
   ││
20├───────────────────┤
   │intArray│
24└───────────────────┘

次のように、これらのローカル変数に値を割り当てた場合:

refType = new RefType();
refType.I = 100;
refType.S = "refType.S";
refType.L = 0x0123456789ABCDEF;

valType = new ValType();
valType.I = 200;
valType.S = "valType.S";
valType.L = 0x0011223344556677;

intArray = new int[4];
intArray[0] = 300;
intArray[1] = 301;
intArray[2] = 302;
intArray[3] = 303;

次に、スタックは次のようになります。

 0┌───────────────────┐
   │0x4A963B68│-`refType`のヒープアドレス
 4├───────────────────┤
   │200│-`valType.I`の値
   │0x4A984C10│-「valType.S」のヒープアドレス
   │0x44556677│-`valType.L`の下位32ビット
   │0x00112233│-`valType.L`の上位32ビット
20├───────────────────┤
   │0x4AA4C288│-「intArray」のヒープアドレス
24└───────────────────┘

アドレス0x4A963B68(の値refType)のメモリは次のようになります。

 0┌───────────────────┐
   │100│-`refType.I`の値
 4├───────────────────┤
   │0x4A984D88│-「refType.S」のヒープアドレス
 8├───────────────────┤
   │0x89ABCDEF│-「refType.L」の下位32ビット
   │0x01234567│-`refType.L`の上位32ビット
16└───────────────────┘

アドレス0x4AA4C288(の値intArray)のメモリは次のようになります。

 0┌───────────────────┐
   │4│-配列の長さ
 4├───────────────────┤
   │300│-`intArray [0]`
 8├───────────────────┤
   │301│-`intArray [1]`
12├───────────────────┤
   │302│-`intArray [2]`
16├───────────────────┤
   │303│-`intArray [3]`
20└───────────────────┘

これで、intArray別の関数に渡した場合、スタックにプッシュされる値0x4AA4C288は、配列のコピーではなく配列のアドレスになります。


52
すべてのローカル変数がスタックに格納されるというステートメントは不正確であることに注意します。無名関数の外部変数であるローカル変数は、ヒープに格納されます。イテレータブロックのローカル変数はヒープに格納されます。非同期ブロックのローカル変数はヒープに保存されます。登録されたローカル変数は、スタックにもヒープにも格納されません。省略されたローカル変数は、スタックにもヒープにも格納されません。
Eric Lippert、2011年

5
LOL、いつも一目ぼれのリッパー氏。:)後者の2つのケースを除いて、いわゆる「ローカル」はコンパイル時にローカルではなくなることを指摘せざるを得ません。実装は、それらをクラスメンバーのステータスに引き上げます。これが、ヒープに格納される唯一の理由です。したがって、これは単なる実装の詳細です(スニッカー)。もちろん、レジスター・ストレージはさらに低いレベルの実装の詳細であり、省略は考慮されません。
Pダディ

3
もちろん、私の投稿はすべて実装の詳細ですが、ご存知のとおり、すべて変数値の概念を分離しようとしています。変数(ローカル、フィールド、パラメーターなど)は、スタック、ヒープ、または実装で定義されたその他の場所に格納できますが、実際には重要ではありません。重要なのは、その変数がそれが表す値を直接格納するのか、それとも単に他の場所に格納されるその値への参照を格納するのかです。それはコピーのセマンティクスに影響を与えるので重要です:その変数をコピーすることがその値またはそのアドレスをコピーするかどうか。
Pダディ

16
どうやら「ローカル変数」とはどういう意味か、私とは違う考えを持っているようです。「ローカル変数」はその実装の詳細によって特徴付けられると信じているようです。この信念は、C#仕様で知っていることによって正当化されるものではありません。ローカル変数は、実際には、ブロック内で宣言された変数であり、その名前は、ブロックに関連付けられた宣言スペース全体でのみスコープ内にあります。実装の詳細として、クロージャクラスのフィールドに引き上げられたローカル変数は、C#の規則に従ってローカル変数であることを保証します。
Eric Lippert、2011年

15
そうは言っても、もちろんあなたの答えは一般的に優れています。が概念的に変数と異なるという点は、それが基本であるため、可能な限り頻繁かつ大音量で行う必要があることです。それでも、非常に多くの人々が彼らについての最も奇妙な神話を信じています!良い戦いを戦うためにあなたにとても良い。
Eric Lippert、2011年

23

はい、アレイはヒープ上に配置されます。

配列内の整数はボックス化されません。値型がヒープに存在するからといって、必ずしもボックス化されるとは限りません。ボクシングは、intなどの値型が型オブジェクトの参照に割り当てられている場合にのみ発生します。

例えば

ボックス化しない:

int i = 42;
myIntegers[0] = 42;

ボックス:

object i = 42;
object[] arr = new object[10];  // no boxing here 
arr[0] = 42;

この件に関するエリックの投稿もご覧ください。


1
しかし、私はそれを理解していません。スタックに値型を割り当てるべきではありませんか?または、値タイプと参照タイプの両方をヒープまたはスタックの両方に割り当てることができますが、それらは通常、単に1つの場所または別の場所に格納されているだけです。
貪欲なエリジウム2009

4
@Jorge、参照型ラッパー/コンテナーのない値型がスタックに存在します。ただし、参照型コンテナ内で使用されると、ヒープ内に存在します。配列は参照型であるため、intのメモリはヒープ内になければなりません。
JaredPar 2009

2
@Jorge:参照型はヒープにのみ存在し、スタックには存在しません。逆に、スタックの場所へのポインタを参照型のオブジェクトに格納することは(検証可能なコードでは)不可能です。
アントンティクヒ2009

1
iをarr [0]に割り当てるつもりだったと思います。定数の割り当てでも「42」のボクシングが発生しますが、iを作成したので、それを使用することもできます;-)
Marcus Griep、

@AntonTykhyy:CLRがエスケープ分析を実行できないと私が知っているルールはありません。オブジェクトがそれを作成した関数の存続期間を超えて参照されないことが検出された場合、値の型かどうかに関係なく、スタック上にオブジェクトを構築することは完全に正当であり、さらに望ましいことです。「値の型」と「参照の型」は、基本的には変数が占めるメモリの内容を表し、オブジェクトがどこに存在するかについての厳密な規則ではありません。
cHao 2013

21

何が起こっているのかを理解するために、ここにいくつかの事実があります:

  • オブジェクトは常にヒープに割り当てられます。
  • ヒープにはオブジェクトのみが含まれます。
  • 値の型は、スタックに割り当てられるか、ヒープ上のオブジェクトの一部に割り当てられます。
  • 配列はオブジェクトです。
  • 配列には値タイプのみを含めることができます。
  • オブジェクト参照は値タイプです。

したがって、整数の配列がある場合、配列はヒープに割り当てられ、そこに含まれる整数はヒープ上の配列オブジェクトの一部です。整数は、個別のオブジェクトとしてではなく、ヒープ上の配列オブジェクト内に存在するため、ボックス化されません。

文字列の配列がある場合、それは実際には文字列参照の配列です。参照は値型であるため、ヒープ上の配列オブジェクトの一部になります。文字列オブジェクトを配列に入れると、実際には文字列オブジェクトへの参照が配列に入れられ、文字列はヒープ上の個別のオブジェクトになります。


はい、参照は値型とまったく同じように動作しますが、通常はそのように呼び出されないか、値型に含まれていないことに気付きました。たとえば、msdn.microsoft.com / en
us / library / s1ax56ch.aspxを

@ヘンク:はい、あなたは参照が値型変数の中にリストされていないことは正しいですが、メモリがそれらに割り当てられる方法になると、それらはあらゆる点で値型になり、メモリ割り当ての方法を理解することを理解することは非常に役立ちますすべてが合わさります。:)
Guffa 2009

5番目の点、「配列には値型のみを含めることができる」は疑問です。文字列配列はどうですか?string [] strings =新しいstring [4];
Sunil Purushothaman

9

あなたの質問の中心にあるのは、参照型と値型に関する誤解だと思います。これはおそらく、すべての.NETおよびJava開発者が苦労したことです。

配列は単なる値のリストです。それは(例えば、参照型の配列の場合string[])、配列は、種々の参照のリストであり、string基準となるように、ヒープ上のオブジェクトの参照タイプの。内部的には、これらの参照はメモリ内のアドレスへのポインタとして実装されています。これを視覚化したい場合、そのような配列はメモリ内(ヒープ上)で次のようになります。

[ 00000000, 00000000, 00000000, F8AB56AA ]

これは、ヒープ上のオブジェクトstringへの4つの参照を含むの配列ですstring(ここの数値は16進数です)。現在、最後のものだけがstring実際に何かを指しています(メモリは割り当て時にすべてゼロに初期化されます)。この配列は、基本的にはC#のこのコードの結果です。

string[] strings = new string[4];
strings[3] = "something"; // the string was allocated at 0xF8AB56AA by the CLR

上記の配列は、32ビットプログラムになります。64ビットプログラムでは、参照は2倍の大きさにF8AB56AAなります(になります00000000F8AB56AA)。

あなたは(と言う値型の配列を持っている場合int[])のように、アレイは、整数のリストである値型のは、ある値自体(名前の由来)。このような配列の視覚化は次のようになります。

[ 00000000, 45FF32BB, 00000000, 00000000 ]

これは4つの整数の配列で、2番目のintのみに値が割り当てられ(1174352571はその16進数の10進表記です)、残りの整数は0になります(前述のように、メモリはゼロに初期化されます) 16進数の00000000は10進数の0です)。この配列を生成したコードは次のようになります。

 int[] integers = new int[4];
 integers[1] = 1174352571; // integers[1] = 0x45FF32BB would be valid too

このint[]配列もヒープに格納されます。

別の例として、short[4]配列のメモリは次のようになります。

[ 0000, 0000, 0000, 0000 ]

a のshort2バイトの数値であるため。

値の型が格納される場所は、Eric Lippertがここで非常によく説明する実装の詳細にすぎず、値と参照の型の違い(動作の違い)に固有のものはありません。

メソッドに何か(参照型または値型)を渡すと、型のコピーが実際にメソッドに渡されます。参照型の場合、は参照であり(これはメモリの一部へのポインタと考えてください。これは実装の詳細でもあります)、値型の場合、値はそのものです。

// Calling this method creates a copy of the *reference* to the string
// and a copy of the int itself, so copies of the *values*
void SomeMethod(string s, int i){}

ボクシングは、値型を参照型に変換した場合にのみ発生します。このコードボックス:

object o = 5;

「実装の詳細」はfont-size:50pxである必要があると思います。;)
09

2

これらは、@ P Daddyによる上記の回答を描いたイラストです

ここに画像の説明を入力してください

ここに画像の説明を入力してください

そして、対応するコンテンツを自分のスタイルでイラスト化しました。

ここに画像の説明を入力してください


@P Daddyイラストを作ってみました。間違っている部分がないか確認してください。さらに、いくつか質問があります。1. 4つの長さのint型配列を作成すると、長さ情報(4)も常にメモリに格納されますか?
YoungMin Park 2017

2. 2番目の図では、コピーされた配列アドレスはどこに保存されますか?intArrayアドレスが格納されているスタック領域と同じですか?他のスタックですが、同じ種類のスタックですか?別の種類のスタックですか?3.低32ビット/高32ビットとはどういう意味ですか?4.新しいキーワードを使用してスタックに値のタイプ(この例では構造)を割り当てると、戻り値はどうなりますか?住所でもありますか?このステートメントConsole.WriteLine(valType)で確認すると、ConsoleApp.ValTypeのようなオブジェクトのような完全修飾名が表示されます。
YoungMin Park 2017

5. valType.I = 200; このステートメントは、valTypeのアドレスを取得することを意味しますか?このアドレスによって、私はIにアクセスし、そこに200を格納しますが、 "スタック上"にあります。
YoungMin Park 2017

1

整数の配列がヒープに割り当てられます。それ以上でもそれ以下でもありません。myIntegersは、intが割り当てられているセクションの開始を参照します。その参照はスタックにあります。

スタック上にあるObjectタイプmyObjects []のような参照タイプのオブジェクトの配列がある場合、オブジェクト自体を参照する値の束を参照します。

要約すると、myIntegersをいくつかの関数に渡す場合、実際の整数の束が割り当てられている場所への参照のみを渡します。


1

サンプルコードにはボクシングはありません。

値型は、intの配列と同じようにヒープ上に存在できます。配列はヒープに割り当てられ、値の型であるintを格納します。配列の内容はdefault(int)に初期化され、たまたまゼロです。

値タイプを含むクラスを考えてみましょう:


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

変数hは、ヒープ上に存在するHasAnIntのインスタンスを参照します。たまたま値型が含まれているだけです。それはまったく問題ありません。「i」はクラスに含まれているため、たまたまヒープ上に存在します。この例でもボクシングはありません。


1

十分だと誰もが言っていますが、誰かがヒープ、スタック、ローカル変数、静的変数に関する明確な(ただし非公式の)サンプルとドキュメントを探している場合は、Jon Skeetの.NETのメモリに関する記事を参照してください。どこ

抜粋:

  1. 各ローカル変数(つまり、メソッドで宣言されたもの)はスタックに格納されます。これには参照型変数が含まれます-変数自体はスタック上にありますが、参照型変数の値は参照(またはnull)のみであり、オブジェクト自体ではないことに注意してください。メソッドパラメータもローカル変数としてカウントされますが、ref修飾子を使用して宣言されている場合、独自のスロットは取得されませんが、呼び出しコードで使用されている変数とスロットを共有します。詳細については、パラメーターの受け渡しに関する私の記事を参照してください。

  2. 参照型のインスタンス変数は常にヒープ上にあります。これは、オブジェクト自体が「生きている」場所です。

  3. 値型のインスタンス変数は、値型を宣言する変数と同じコンテキストに格納されます。インスタンスのメモリスロットには、インスタンス内の各フィールドのスロットが事実上含まれています。つまり、(前の2つの点を考えると)メソッド内で宣言された構造体変数は常にスタック上にありますが、クラスのインスタンスフィールドである構造体変数はヒープ上にあります。

  4. すべての静的変数は、参照型内で宣言されているか値型内で宣言されているかに関係なく、ヒープに格納されます。作成されるインスタンスの数に関係なく、合計でスロットは1つだけです。(ただし、その1つのスロットが存在するためにインスタンスを作成する必要はありません。)変数が存在する正確なヒープの詳細は複雑ですが、この件に関するMSDNの記事で詳しく説明されています。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.