구조체에서 "new"를 사용하면 힙이나 스택에 할당합니까?


290

new연산자 를 사용하여 클래스의 인스턴스를 만들면 메모리가 힙에 할당됩니다. new연산자 를 사용하여 구조체의 인스턴스를 만들 때 메모리가 할당되는 위치, 힙 또는 스택에서?

답변:


305

좋아, 내가 이것을 더 명확하게 만들 수 있는지 보자.

첫째, Ash가 옳다. 문제는 값 유형 변수 가 어디에 할당 되는지에 관한 것이 아니다 . 그것은 다른 질문입니다. 그리고 그 대답은 단지 "스택"에 있지 않습니다. 그것보다 더 복잡합니다 (그리고 C # 2에 의해 훨씬 더 복잡해졌습니다). 나는 주제에 관한 기사를 가지고 있으며 요청이 있으면 확장 할 것이지만 연산자 만 다루겠습니다 .new

둘째,이 모든 것은 실제로 당신이 말하는 수준에 달려 있습니다. 컴파일러가 생성하는 IL과 관련하여 소스 코드로 컴파일러가 수행하는 작업을 살펴보고 있습니다. JIT 컴파일러가 많은 "논리적"할당을 최적화하는면에서 영리한 일을 할 가능성이 있습니다.

셋째, 나는 실제로 대답을 알지 못하고 부분적으로 너무 복잡하기 때문에 제네릭을 무시하고 있습니다.

마지막으로,이 모든 것은 현재 구현 된 것입니다. C # 스펙은이 중 많은 부분을 지정하지 않습니다. 실제로는 구현 세부 사항입니다. 관리 코드 개발자는 실제로 신경 쓰지 않아야한다고 생각하는 사람들이 있습니다. 나는 그렇게 멀리 갈지 확신하지 못하지만 실제로 모든 지역 변수가 힙에 살고있는 세계를 상상할 가치가 있습니다.


new값 유형에 대한 연산자 에는 두 가지 상황이 있습니다. 매개 변수가없는 생성자 (예 :) new Guid()또는 매개 변수가있는 생성자 (예 :)를 호출 할 수 있습니다 new Guid(someString). 이들은 상당히 다른 IL을 생성합니다. 이유를 이해하려면 C #과 CLI 사양을 비교해야합니다. C #에 따르면 모든 값 유형에는 매개 변수가없는 생성자가 있습니다. CLI 사양에 따르면 값 유형에 매개 변수가없는 생성자가 없습니다 . (반복으로 값 유형의 생성자를 가져 오십시오. 매개 변수가없는 것을 찾을 수 없습니다.)

C #에서는 "0으로 값 초기화"를 생성자로 취급하는 것이 좋습니다. 언어를 일관성있게 유지하기 때문 입니다. 항상 생성자를 호출하는 new(...)것으로 생각할 수 있습니다 . 호출 할 실제 코드가없고 유형별 코드가 없기 때문에 CLI가 다르게 생각하는 것이 합리적입니다.

또한 초기화 후 값으로 수행 할 작업에 차이를 만듭니다. 사용 된 IL

Guid localVariable = new Guid(someString);

다음에 사용 된 IL과 다릅니다.

myInstanceOrStaticVariable = new Guid(someString);

또한 값이 중간 값으로 사용되는 경우 (예 : 메소드 호출에 대한 인수) 상황이 다시 약간 다릅니다. 이러한 모든 차이점을 보여주기 위해 간단한 테스트 프로그램이 있습니다. 그것은 정적 변수와 인스턴스 변수의 차이를 보여주지 않는다 : 일리노이 사이에 다른 것 stfld하고 stsfld있지만, 그게 다야.

using System;

public class Test
{
    static Guid field;

    static void Main() {}
    static void MethodTakingGuid(Guid guid) {}


    static void ParameterisedCtorAssignToField()
    {
        field = new Guid("");
    }

    static void ParameterisedCtorAssignToLocal()
    {
        Guid local = new Guid("");
        // Force the value to be used
        local.ToString();
    }

    static void ParameterisedCtorCallMethod()
    {
        MethodTakingGuid(new Guid(""));
    }

    static void ParameterlessCtorAssignToField()
    {
        field = new Guid();
    }

    static void ParameterlessCtorAssignToLocal()
    {
        Guid local = new Guid();
        // Force the value to be used
        local.ToString();
    }

    static void ParameterlessCtorCallMethod()
    {
        MethodTakingGuid(new Guid());
    }
}

관련없는 비트 (예 : nops)를 제외한 클래스의 IL은 다음과 같습니다.

.class public auto ansi beforefieldinit Test extends [mscorlib]System.Object    
{
    // Removed Test's constructor, Main, and MethodTakingGuid.

    .method private hidebysig static void ParameterisedCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: stsfld valuetype [mscorlib]System.Guid Test::field
        L_0010: ret     
    }

    .method private hidebysig static void ParameterisedCtorAssignToLocal() cil managed
    {
        .maxstack 2
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid    
        L_0003: ldstr ""    
        L_0008: call instance void [mscorlib]System.Guid::.ctor(string)    
        // Removed ToString() call
        L_001c: ret
    }

    .method private hidebysig static void ParameterisedCtorCallMethod() cil  managed    
    {   
        .maxstack 8
        L_0001: ldstr ""
        L_0006: newobj instance void [mscorlib]System.Guid::.ctor(string)
        L_000b: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0011: ret     
    }

    .method private hidebysig static void ParameterlessCtorAssignToField() cil managed
    {
        .maxstack 8
        L_0001: ldsflda valuetype [mscorlib]System.Guid Test::field
        L_0006: initobj [mscorlib]System.Guid
        L_000c: ret 
    }

    .method private hidebysig static void ParameterlessCtorAssignToLocal() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        // Removed ToString() call
        L_0017: ret 
    }

    .method private hidebysig static void ParameterlessCtorCallMethod() cil managed
    {
        .maxstack 1
        .locals init ([0] valuetype [mscorlib]System.Guid guid)    
        L_0001: ldloca.s guid
        L_0003: initobj [mscorlib]System.Guid
        L_0009: ldloc.0 
        L_000a: call void Test::MethodTakingGuid(valuetype [mscorlib]System.Guid)
        L_0010: ret 
    }

    .field private static valuetype [mscorlib]System.Guid field
}

보시다시피 생성자를 호출하는 데 사용되는 여러 가지 지침이 있습니다.

  • newobj: 스택에 값을 할당하고 매개 변수화 된 생성자를 호출합니다. 필드에 할당하거나 메소드 인수로 사용하기 위해 중간 값에 사용됩니다.
  • call instance: 이미 할당 된 저장 위치를 ​​사용합니다 (스택에 있든 없든). 이것은 로컬 변수에 할당하기 위해 위의 코드에서 사용됩니다. 동일한 지역 변수에 여러 번의 new호출을 사용하여 값을 여러 번 할당 하면 이전 값의 상단에서 데이터를 초기화하기 때문에 매번 더 많은 스택 공간을 할당 하지 않습니다 .
  • initobj: 이미 할당 된 저장 위치를 ​​사용하고 데이터를 지 웁니다. 이것은 로컬 변수에 할당되는 것을 포함하여 모든 매개 변수없는 생성자 호출에 사용됩니다. 메소드 호출의 경우 중간 로컬 변수가 효과적으로 도입되고 그 값이에 의해 삭제됩니다 initobj.

나는 이것이 주제가 얼마나 복잡한지를 보여 주면서 동시에 약간의 빛을 비추기를 바란다. 에서 몇 가지 개념 감각, 호출 할 때마다 new스택에 할당 공간 -하지만 우리가 보았 듯이, 정말 심지어 IL 수준에서 무슨 일이 아니다. 하나의 특정 사례를 강조하고 싶습니다. 이 방법을 사용하십시오.

void HowManyStackAllocations()
{
    Guid guid = new Guid();
    // [...] Use guid
    guid = new Guid(someBytes);
    // [...] Use guid
    guid = new Guid(someString);
    // [...] Use guid
}

"논리적으로"는 4 개의 스택 할당 (변수마다 하나씩, 3 개의 new호출 마다 하나씩)이 있지만 실제로는 (특정 코드에 대해) 스택은 한 번만 할당 된 다음 동일한 저장 위치가 재사용됩니다.

편집 : 분명히, 이것은 일부 경우에만 사실입니다 ... 특히 생성자가 예외를 throw guid하면 값이 표시되지 않으므로 GuidC # 컴파일러가 동일한 스택 슬롯을 재사용 할 수 있습니다. 자세한 내용과 적용 되지 않는 경우에 대한 가치 유형 구성에 대한 Eric Lippert의 블로그 게시물 을 참조하십시오 .

이 답변을 작성하면서 많은 것을 배웠습니다. 확실하지 않은 경우 설명을 요청하십시오!


1
Jon, HowManyStackAllocations 예제 코드가 좋습니다. 그러나 Guid 대신 Struct를 사용하도록 변경하거나 새로운 Struct 예제를 추가 할 수 있습니까? 그런 다음 @kedar의 원래 질문을 직접 해결할 것이라고 생각합니다.
Ash

9
Guid는 이미 구조체입니다. 참조 msdn.microsoft.com/en-us/library/system.guid.aspx를 내가 :)이 질문에 대한 참조 형 집어 않았을 것이다
존 소총

1
List<Guid>그 3을 가지고 추가 하면 어떻게됩니까 ? 그것은 3 개의 할당 일 것입니다 (동일한 IL)? 그러나 그들은 어딘가에 마법 유지하고
Arec Barrwin에게

1
@Ani : Eric의 예제에 try / catch 블록이 있다는 사실이 누락되었습니다. 따라서 구조체의 생성자 중에 예외가 발생하면 생성자 전에 값을 볼 수 있어야합니다. 내 예제 에는 그러한 상황 이 없습니다 . 생성자가 예외로 실패하면 guid어쨌든 표시되지 않으므로 값 이 절반으로 덮어 씌여 졌는지 여부는 중요하지 않습니다 .
Jon Skeet

2
@Ani : 에릭은 실제로 이것을 포스트의 맨 아래 근처에서 호출합니다. "이제 Wesner의 요점은 어떻습니까? 네, 사실 그것이 선언 된 스택 할당 로컬 변수 (그리고 클로저의 필드가 아닌 경우)라면 생성자 호출과 동일한 "시도"중첩 수준에서 새 임시를 작성하고 임시를 초기화하고이를 로컬로 복사하는이 rigamarole을 거치지 않습니다. C # 프로그램이 그 차이를 관찰하는 것이 불가능하기 때문에 임시 및 사본 작성! "
Jon Skeet

40

구조체의 필드를 포함하는 메모리는 상황에 따라 스택 또는 힙에 할당 될 수 있습니다. struct-type 변수가 일부 익명 대리자 또는 반복자 클래스에 의해 캡처되지 않은 로컬 변수 또는 매개 변수 인 경우 스택에 할당됩니다. 변수가 일부 클래스의 일부인 경우 힙의 클래스 내에서 변수가 할당됩니다.

구조체가 힙에 할당 된 경우 실제로 메모리를 할당하는 데 새 연산자를 호출 할 필요는 없습니다. 유일한 목적은 생성자에있는 내용에 따라 필드 값을 설정하는 것입니다. 생성자가 호출되지 않으면 모든 필드는 기본값 (0 또는 null)을 갖습니다.

C #에서 모든 로컬 변수를 사용하기 전에 일부 값으로 설정해야한다는 점을 제외하고 스택에 할당 된 구조체와 마찬가지로 사용자 정의 생성자 또는 기본 생성자를 호출해야합니다 (매개 변수가없는 생성자는 항상 사용할 수 있습니다) 구조체).


13

간단히 말해서 new는 구조체의 잘못된 이름이며 new를 호출하면 단순히 생성자를 호출합니다. 구조체의 유일한 저장 위치는 정의 된 위치입니다.

멤버 변수 인 경우 정의 된대로 변수에 직접 저장되고, 로컬 변수 또는 매개 변수 인 경우 스택에 저장됩니다.

이것을 클래스와 대조하십시오. 클래스는 구조체가 완전히 저장된 위치에 대한 참조를 가지고 있지만 참조는 힙의 어딘가에 있습니다. (구성원의 로컬 / 매개 변수 내 멤버)

클래스 / 구조체가 실제로 구별되지 않는 C ++을 조금 보는 것이 도움이 될 수 있습니다. (언어에는 비슷한 이름이 있지만 기본 액세스 가능성 만 나타냅니다.) new를 호출하면 힙 위치에 대한 포인터가 표시되고 포인터가 아닌 참조가 있으면 스택에 직접 저장됩니다. 다른 개체 내에서 ala는 C #으로 구조체를 만듭니다.


5

모든 값 유형과 마찬가지로 구조체는 항상 선언 된 위치로 이동 합니다.

이 질문을 참조하십시오 여기 구조체를 사용하는 경우에 대한 자세한 내용은. 그리고이 질문에 여기 구조체에 대한 좀 더 많은 정보를 원하시면.

편집 : 나는 그들이 항상 스택에 들어가는 것에 대해 잘못 대답했다 . 이것은 잘못된 것 입니다.


"구조체는 항상 선언 된 위치로 이동합니다", 이것은 약간 오해의 소지가 있습니다. 클래스의 구조체 필드는 항상 "유형의 인스턴스가 생성 될 때 동적 메모리"-Jeff Richter에 배치됩니다. 이것은 힙에 간접적으로있을 수 있지만 일반 참조 유형과 전혀 다릅니다.
Ash

아니요, 참조 유형과 동일하지 않더라도 정확히 맞습니다. 변수의 가치는 선언 된 곳에 존재합니다. 참조 유형 변수의 값은 실제 데이터가 아니라 참조입니다.
Jon Skeet

요약하면, 메소드의 어느 곳에서나 값 유형을 작성 (선언) 할 때마다 항상 스택에 작성됩니다.
Ash

2
존, 당신은 내 요점을 그리워합니다. 이 질문이 처음 제기 된 이유는 새 연산자를 사용하여 구조체를 생성하면 구조체가 할당되는 많은 개발자 (CLR Via C #을 읽을 때까지 포함됨)가 명확하지 않기 때문입니다. "구조가 항상 선언 된 위치로 이동"한다고 말하는 것은 명확한 대답이 아닙니다.
Ash

1
@Ash : 시간이 있다면, 일을 할 때 답을 쓰려고 노력할 것입니다. 그것은 기차에 커버하려고 너무 큰 주제입니다 :)하지만
존 소총

4

아마도 여기에 뭔가 빠졌지 만 왜 할당에 관심이 있습니까?

값 형식은 value;)로 전달되므로 정의 된 위치와 다른 범위에서 변경할 수 없습니다. 값을 변경하려면 [ref] 키워드를 추가해야합니다.

참조 유형은 참조에 의해 전달되며 변경 될 수 있습니다.

물론 불변의 참조 유형 문자열이 가장 많이 사용됩니다.

배열 레이아웃 / 초기화 : 값 유형-> 메모리 없음 [name, zip] [name, zip] 참조 유형-> 메모리 없음-> null [ref] [ref]


3
참조 유형은 참조로 전달되지 않습니다. 참조는 값으로 전달됩니다. 매우 다릅니다.
Jon Skeet 2016 년

2

class또는 struct선언은 실행시에 인스턴스 또는 객체를 생성하는 데 사용되는 청사진과 같다. class또는 structPerson 을 정의하면 Person이 유형의 이름입니다. Person 유형의 변수 p를 선언하고 초기화하면 p는 Person의 객체 또는 인스턴스라고합니다. 동일한 Person 유형의 여러 인스턴스를 작성할 수 있으며 각 인스턴스는 properties및 에서 다른 값을 가질 수 있습니다 fields.

A class는 참조 유형입니다. 때의 개체class 가 생성 될 때 객체가 할당 된 변수는 해당 메모리에 대한 참조 만 보유합니다. 객체 참조가 새 변수에 할당되면 새 변수는 원래 객체를 나타냅니다. 하나의 변수를 통해 변경 한 내용은 다른 변수에 반영되므로 둘 다 동일한 데이터를 참조하기 때문입니다.

A struct는 값 유형입니다. a struct가 만들어 질 때 struct지정된 변수 는 구조체의 실제 데이터를 유지합니다. struct가 새 변수에 지정 되면 복사됩니다. 따라서 새 변수와 원래 변수에는 동일한 데이터의 두 개의 별도 사본이 포함됩니다. 한 사본을 변경해도 다른 사본에는 영향을 미치지 않습니다.

일반적으로 classes보다 복잡한 동작 또는 class개체를 만든 후 수정하려는 데이터를 모델링하는 데 사용됩니다 . Structs은 주로 struct생성 된 후 수정되지 않는 데이터를 포함하는 작은 데이터 구조에 가장 적합합니다 .

이상...


1

값 유형으로 간주되는 구조체는 스택에 할당되는 반면 객체는 힙에 할당되는 반면 객체 참조 (포인터)는 스택에 할당됩니다.


1

구조체는 스택에 할당됩니다. 다음은 유용한 설명입니다.

구조

또한 .NET 내에서 인스턴스화되는 클래스는 힙 또는 .NET의 예약 된 메모리 공간에 메모리를 할당합니다. 반면에 구조체는 스택에 할당되어 인스턴스화 될 때 더 많은 효율성을 제공합니다. 또한 구조체 내에서 전달 매개 변수는 값으로 수행됩니다.


5
구조체가 클래스의 일부인 경우에는 오브젝트의 나머지 데이터와 함께 힙에 존재하는 경우에는 다루지 않습니다.
Jon Skeet

1
그렇습니다. 그러나 실제로 질문에 대한 질문에 중점을두고 답변합니다. 투표함.
Ash

... 아직 부정확하고 오해의 소지가 있습니다. 죄송합니다.이 질문에 대한 짧은 답변은 없습니다. Jeffrey 's가 유일한 완전한 답변입니다.
Marc Gravell
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.