배열, 힙 및 스택 및 값 유형


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

위의 코드에서 새로운 int [100]가 힙에서 배열을 생성합니까? C #을 통해 CLR에서 읽은 내용에서 대답은 그렇습니다. 그러나 내가 이해할 수없는 것은 배열 내부의 실제 int에 발생하는 것입니다. 그것들은 값 유형이기 때문에 예를 들어 myIntegers를 프로그램의 다른 부분에 전달할 수 있고 항상 스택에 남아 있으면 스택을 혼란스럽게 만들 수 있기 때문에 상자에 넣어야한다고 생각합니다. . 아니면 내가 틀렸어? 나는 그들이 박스에 담겨 있고 배열이 존재하는 한 힙에 살 것이라고 생각합니다.

답변:


289

배열은 힙에 할당되며 int는 상자로 표시되지 않습니다.

사람들이 참조 유형이 힙에 할당되고 값 유형이 스택에 할당되었다고 말했기 때문에 혼란의 원인이 될 수 있습니다. 이것은 완전히 정확한 표현이 아닙니다.

모든 지역 변수 및 매개 변수는 스택에 할당됩니다. 여기에는 값 형식과 참조 형식이 모두 포함됩니다. 이 둘의 차이점 은 변수에 저장된 것입니다 . 놀랍게도, 값 유형 의 경우 유형 이 변수에 직접 저장되고 참조 유형의 경우 유형 값이 힙에 저장되며이 값에 대한 참조 는 변수에 저장됩니다.

필드도 마찬가지입니다. 집계 유형 (a class또는 a struct) 의 인스턴스에 메모리가 할당되면 각 인스턴스 필드에 대한 스토리지를 포함해야합니다. 참조 유형 필드의 경우이 스토리지는 값에 대한 참조 만 보유하며 나중에 힙에 할당됩니다. 값 유형 필드의 경우이 스토리지는 실제 값을 보유합니다.

따라서 다음 유형이 주어집니다.

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 ┌────────────────────┐
   │ 나 │
 4 ├────────────────────┤
   │ S │
 8 ├────────────────────┤
   │ L │
   │ │
16 └────────────────────┘

지금 당신은 유형의 함수에 세 개의 지역 변수가 있다면 RefType, ValType그리고 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 │-valval.I의 값
   │ 0x4A984C10 │-`valType.S`의 힙 주소
   │ 0x44556677 │-32 비트의 낮은 valval.L
   │ 0x00112233 │-높은 32 비트`valType.L`
20 ├────────────────────┤
   │ 0x4AA4C288 │-`intArray`의 힙 주소
24 └────────────────────┘

주소의 메모리 0x4A963B68(의 값 refType)는 다음과 같습니다.

 0 ┌────────────────────┐
   │ 100 │-`refType.I '의 값
 4 ├────────────────────┤
   │ 0x4A984D88 │-`refType.S`의 힙 주소
 8 ├────────────────────┤
   │ 0x89ABCDEF │-32 비트의 낮은 RefType.L
   │ 0x01234567 │-높은 32 비트`refType.L`
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

5
LOL, 항상 니트 피커 씨 Lippert :) 나는 후자의 두 경우를 제외하고 소위 "로컬"이 컴파일 타임에 로컬이 아니라고 지적했다. 이 구현은 클래스 멤버의 상태로 올라 가게되는데, 이것이 그들이 힙에 저장되는 유일한 이유입니다. 따라서 그것은 단지 구현 세부 사항 (스니커)입니다. 물론 레지스터 스토리지는 훨씬 낮은 수준의 구현 세부 사항이며 제거는 계산되지 않습니다.
P Daddy

3
물론 전체 게시물은 구현 세부 사항이지만, 아시다시피, 모두 변수 의 개념을 분리하려는 시도였습니다 . 변수 (로컬, 필드, 매개 변수 등)를 스택, 힙 또는 기타 구현 정의 된 장소에 저장할 수 있지만 실제로 중요한 것은 아닙니다. 중요한 것은 변수가 변수가 나타내는 값을 직접 저장하는지 또는 단순히 다른 곳에 저장된 해당 값에 대한 참조를 저장하는지 여부입니다. 복사 의미론에 영향을 미치기 때문에 중요합니다. 변수를 복사하면 값을 복사하는지 또는 주소를 복사하는지 여부입니다.
P Daddy

16
분명히 당신은 그것이 "지역 변수"라는 것이 무엇을 의미하는지에 대한 다른 생각을 가지고 있습니다. "로컬 변수"는 구현 세부 사항에 의해 특징 지워진다 고 생각하는 것 같습니다 . 이 믿음은 C # 사양에서 알고있는 것으로 정당화되지 않습니다. 로컬 변수는 사실 블록 안에 선언 된 변수이며, 그 이름 은 블록과 관련된 선언 공간 전체에 걸쳐 범위 내에 있습니다. 클로저 클래스의 필드에 게양 된 구현 세부 사항 인 로컬 변수 는 여전히 C # 규칙에 따라 로컬 변수라는 것을 확신합니다 .
Eric Lippert

15
물론 당신의 대답은 일반적으로 훌륭합니다. 있다는 점 값을 개념적으로 다른 변수 는 기본적인 요구하므로, 소리 등 가능한 한 자주 만들어 할 것이이다. 그러나 많은 사람들이 그들에 대한 가장 이상한 신화를 믿습니다! 선한 싸움과 싸워서 정말 좋습니다.
Eric Lippert

23

예, 어레이는 힙에 위치합니다.

배열 내부의 정수는 박스로 표시되지 않습니다. 힙에 값 유형이 존재한다고해서 반드시 상자에 넣는 것은 아닙니다. 복싱은 int와 같은 값 유형이 유형 객체의 참조에 지정된 경우에만 발생합니다.

예를 들어

상자에 들어 있지 않습니다 :

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

박스 :

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

이 주제에 대한 Eric의 게시물을 확인할 수도 있습니다.


1
그러나 나는 그것을 얻지 못한다. 스택에 값 유형을 할당해서는 안됩니까? 또는 값과 참조 유형을 모두 힙 또는 스택에 할당 할 수 있으며 일반적으로 한곳 또는 다른 곳에 저장되어 있습니까?
삼켜 버린 엘리시움

4
@Jorge, 참조 유형 래퍼 / 컨테이너가없는 값 유형은 스택에 있습니다. 그러나 일단 참조 유형 컨테이너 내에서 사용되면 힙에있게됩니다. 배열은 참조 유형이므로 int의 메모리는 힙에 있어야합니다.
JaredPar

2
@ 조지 : 참조 유형은 힙에만 있고 스택에는 없습니다. 반대로, (확인 가능한 코드에서) 스택 위치에 대한 포인터를 참조 유형의 객체에 저장하는 것은 불가능합니다.
Anton Tykhyy

1
나는 당신이 i를 arr [0]에 할당하려고했다고 생각합니다. 상수 할당은 여전히 ​​"42"의 boxing을 야기하지만, i를 만들었으므로 그것을 사용할 수도 있습니다 ;-)
Marcus Griep

@AntonTykhyy : CLR이 이스케이프 분석을 할 수 없다는 것을 알고있는 규칙은 없습니다. 객체가 생성 된 함수의 수명이 지난 후에도 객체가 참조되지 않는다는 것을 감지하면 값 유형이든 아니든 스택에 객체를 구성하는 것이 전적으로 합법적이며 더 바람직합니다. "값 유형"및 "참조 유형"은 기본적으로 개체가 어디에 있는지에 대한 엄격하고 빠른 규칙이 아니라 변수가 사용하는 메모리의 내용을 나타냅니다.
cHao

21

무슨 일이 일어나고 있는지 이해하려면 몇 가지 사실이 있습니다.

  • 객체는 항상 힙에 할당됩니다.
  • 힙에는 객체 만 포함됩니다.
  • 값 유형은 스택에 할당되거나 힙에있는 객체의 일부입니다.
  • 배열은 객체입니다.
  • 배열은 값 유형 만 포함 할 수 있습니다.
  • 객체 참조는 값 유형입니다.

따라서 정수 배열이있는 경우 배열은 힙에 할당되고 여기에 포함 된 정수는 힙에서 배열 오브젝트의 일부입니다. 정수는 개별 객체가 아닌 힙의 배열 객체 내부에 상주하므로 박스로 표시되지 않습니다.

문자열 배열이 있으면 실제로 문자열 참조 배열입니다. 참조는 값 유형이므로 힙에서 배열 객체의 일부가됩니다. 배열에 문자열 객체를 넣는 경우 실제로는 배열에 문자열 객체에 대한 참조를 넣고 문자열은 힙에서 별도의 객체입니다.


예, 참조는 값 유형과 똑같이 동작하지만 일반적으로 이러한 방식으로 호출되지 않거나 값 유형에 포함되지 않습니다. 예를 들어 참조 (하지만 훨씬 더이 같은있다) msdn.microsoft.com/en-us/library/s1ax56ch.aspx
헹크 Holterman

@Henk : 예, 당신은 값 타입 변수들 사이에 참조가 나열되어 있지 않다는 것이 맞습니다. 그러나 메모리가 어떻게 할당되는지에 관해서는 그것들이 모든 측면에서 가치 유형에 있으며, 메모리 할당이 어떻게 이해되는지 이해하는 것이 매우 유용합니다 모두 함께 맞습니다. :)
Guffa

"배열은 값 유형 만 포함 할 수 있습니다." 문자열 배열은 어떻습니까? string [] 문자열 = 새 문자열 [4];
Sunil Purushothaman

9

귀하의 질문의 핵심은 참조 및 가치 유형에 대한 오해라고 생각합니다. 이것은 아마도 모든 .NET 및 Java 개발자가 어려움을 겪고있는 것입니다.

배열은 값 목록 일뿐입니다. 참조 유형의 배열 (예 : a string[]) 인 string경우 참조는 참조 유형 의 이므로 힙의 다양한 객체에 대한 참조 목록입니다 . 내부적으로 이러한 참조는 메모리의 주소에 대한 포인터로 구현됩니다. 이것을 시각화하려면 그러한 배열은 메모리 (힙)에서 다음과 같이 보입니다.

[ 00000000, 00000000, 00000000, F8AB56AA ]

이것은 힙에있는 객체에 대한 string4 개의 참조를 포함 하는 배열입니다 string(여기서 숫자는 16 진수 임). 현재 마지막은 string실제로 무엇이든 가리 킵니다 (메모리는 할당 될 때 모두 0으로 초기화됩니다).이 배열은 기본적으로 C # 에서이 코드의 결과입니다.

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

위의 배열은 32 비트 프로그램에 있습니다. 64 비트 프로그램에서 참조 (큰 배 될 F8AB56AA것입니다 00000000F8AB56AA).

당신이 값 형식의 배열이있는 경우 (AN 말 int[]은 AS 다음 배열은 정수의 목록입니다) 값 형식의가 있다 값 자체 (따라서 이름). 이러한 배열의 시각화는 다음과 같습니다.

[ 00000000, 45FF32BB, 00000000, 00000000 ]

이것은 4 개의 정수 배열입니다. 두 번째 int 만 값 (1174352571, 즉 16 진수의 십진수 표시)이 할당되고 나머지 정수는 0이됩니다 (내가 말했듯이 메모리는 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 가 값과 참조 유형의 차이 (행동의 차이)에 고유 한 것이 아니라 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;

"구현 세부 사항"은 글꼴 크기 : 50px이어야한다고 생각합니다. ;)
sisve

2

위의 답변은 @P Daddy의 그림입니다.

여기에 이미지 설명을 입력하십시오

여기에 이미지 설명을 입력하십시오

그리고 나는 내 스타일로 해당 내용을 설명했습니다.

여기에 이미지 설명을 입력하십시오


@P 아빠 그림을 만들었습니다. 잘못된 부분이 있는지 확인하십시오. 그리고 추가 질문이 있습니다. 1. 4 길이 int 형 배열을 만들 때 길이 정보 (4)도 항상 메모리에 저장됩니까?
YoungMin Park

두 번째 그림에서, 복사 된 어레이 주소는 어디에 저장됩니까? intArray 주소가 저장되는 것과 동일한 스택 영역입니까? 다른 스택이지만 같은 종류의 스택입니까? 다른 종류의 스택입니까? 3. 낮은 32 비트 / 높은 32 비트는 무엇을 의미합니까? 4. 새 키워드를 사용하여 스택에 값 유형 (이 예에서는 구조)을 할당 할 때 반환 값은 무엇입니까? 또한 주소입니까? 이 문 Console.WriteLine (valType)으로 확인하면 ConsoleApp.ValType과 같은 객체와 같은 정규화 된 이름이 표시됩니다.
YoungMin Park

valType.I = 200; 이 진술은 valType의 주소를 얻음을 의미합니까?이 주소로 I에 액세스하여 200을 저장하지만 "스택에"저장합니다.
YoungMin Park

1

정수 배열은 힙에 할당됩니다. myIntegers는 정수가 할당 된 섹션의 시작을 나타냅니다. 해당 참조는 스택에 있습니다.

스택에있는 객체 유형과 같은 참조 유형 객체의 배열이있는 경우 myObjects []는 객체 자체를 참조하는 여러 값을 참조합니다.

요약하면, myIntegers를 일부 함수에 전달하면 실제 정수 무리가 할당되는 위치에만 참조를 전달합니다.


1

예제 코드에는 권투가 없습니다.

값 유형은 정수 배열과 마찬가지로 힙에 존재할 수 있습니다. 배열은 힙에 할당되고 값 유형 인 int를 저장합니다. 배열의 내용은 default (int)로 초기화되며 이는 0입니다.

값 유형이 포함 된 클래스를 고려하십시오.


    class HasAnInt
    {
        int i;
    }

    HasAnInt h = new HasAnInt();

변수 h는 힙에있는 HasAnInt 인스턴스를 나타냅니다. 값 유형이 포함되어 있습니다. 그것은 완벽하게 괜찮습니다. 'i'는 수업에 포함되어 있기 때문에 힙에 살았습니다. 이 예에는 권투도 없습니다.


1

모두가 충분히 말했지만 누군가 누군가 힙, 스택, 로컬 변수 및 정적 변수에 대한 명확한 (비공식적) 샘플 및 문서를 찾고 있다면 .NET의 메모리에 관한 Jon Skeet의 전체 기사를 참조하십시오. 어디

발췌 :

  1. 각 로컬 변수 (즉, 메소드에서 선언 된 변수)는 스택에 저장됩니다. 여기에는 참조 유형 변수가 포함됩니다. 변수 자체는 스택에 있지만 참조 유형 변수의 값은 객체 자체가 아니라 참조 (또는 null) 일뿐입니다. 메소드 매개 변수도 로컬 변수로 계산되지만 참조 수정 자로 선언 된 경우 자체 슬롯을 얻지 않고 호출 코드에서 사용 된 변수와 슬롯을 공유합니다. 자세한 내용은 매개 변수 전달에 대한 내 기사를 참조하십시오.

  2. 참조 유형의 인스턴스 변수는 항상 힙에 있습니다. 그곳은 물체 자체가 "살아있는"곳입니다.

  3. 값 유형의 인스턴스 변수는 값 유형을 선언하는 변수와 동일한 컨텍스트에 저장됩니다. 인스턴스의 메모리 슬롯에는 인스턴스 내의 각 필드에 대한 슬롯이 효과적으로 포함됩니다. 즉, 메서드 내에서 선언 된 구조체 변수는 항상 스택에있는 반면 클래스의 인스턴스 필드 인 구조체 변수는 힙에 있습니다.

  4. 모든 정적 변수는 참조 유형 또는 값 유형 내에 선언되는지 여부에 관계없이 힙에 저장됩니다. 생성 된 인스턴스 수에 관계없이 총 하나의 슬롯 만 있습니다. (하나의 슬롯에 대해 생성 된 인스턴스가있을 필요는 없습니다.) 변수가 존재하는 힙의 세부 사항은 복잡하지만 주제에 대한 MSDN 기사에 자세히 설명되어 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.